diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index d15464e0468f..50df0c9a584b 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -1,6 +1,9 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user::{ + dashboard_metadata::{ + GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, + }, ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate, }; @@ -18,6 +21,10 @@ impl ApiEventMetric for ConnectAccountRequest {} common_utils::impl_misc_api_event_type!( ChangePasswordRequest, + GetMultipleMetaDataPayload, + GetMetaDataResponse, + GetMetaDataRequest, + SetMetaDataRequest, SwitchMerchantIdRequest, CreateInternalUserRequest, UserMerchantCreate diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 00f57a7be296..e0bfa50b4115 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,5 +1,6 @@ use common_utils::pii; use masking::Secret; +pub mod dashboard_metadata; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] pub struct ConnectAccountRequest { diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..04cda3bd7075 --- /dev/null +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -0,0 +1,110 @@ +use masking::Secret; +use strum::EnumString; + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub enum SetMetaDataRequest { + ProductionAgreement(ProductionAgreementRequest), + SetupProcessor(SetupProcessor), + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected(ProcessorConnected), + SecondProcessorConnected(ProcessorConnected), + ConfiguredRouting(ConfiguredRouting), + TestPayment(TestPayment), + IntegrationMethod(IntegrationMethod), + IntegrationCompleted, + SPRoutingConfigured(ConfiguredRouting), + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProductionAgreementRequest { + pub version: String, + #[serde(skip_deserializing)] + pub ip_address: Option>, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct SetupProcessor { + pub connector_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ProcessorConnected { + pub processor_id: String, + pub processor_name: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ConfiguredRouting { + pub routing_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct TestPayment { + pub payment_id: String, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct IntegrationMethod { + pub integration_type: String, +} + +#[derive(Debug, serde::Deserialize, EnumString, serde::Serialize)] +pub enum GetMetaDataRequest { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SPRoutingConfigured, + SPTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(transparent)] +pub struct GetMultipleMetaDataPayload { + pub results: Vec, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct GetMultipleMetaDataRequest { + pub keys: String, +} + +#[derive(Debug, serde::Serialize)] +pub enum GetMetaDataResponse { + ProductionAgreement(bool), + SetupProcessor(Option), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(Option), + SecondProcessorConnected(Option), + ConfiguredRouting(Option), + TestPayment(Option), + IntegrationMethod(Option), + IntegrationCompleted(bool), + StripeConnected(Option), + PaypalConnected(Option), + SPRoutingConfigured(Option), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index dc4a7614f587..3ddd85f37891 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -425,3 +425,39 @@ pub enum UserStatus { #[default] InvitationSent, } + +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + frunk::LabelledGeneric, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum DashboardMetadata { + ProductionAgreement, + SetupProcessor, + ConfigureEndpoint, + SetupComplete, + FirstProcessorConnected, + SecondProcessorConnected, + ConfiguredRouting, + TestPayment, + IntegrationMethod, + IntegrationCompleted, + StripeConnected, + PaypalConnected, + SpRoutingConfigured, + SpTestPayment, + DownloadWoocom, + ConfigureWoocom, + SetupWoocomWebhook, + IsMultipleConfiguration, +} diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index cf5a993c2686..b0537d0a287b 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -6,6 +6,7 @@ pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod events; pub mod file; diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs new file mode 100644 index 000000000000..03e4a2dab38b --- /dev/null +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -0,0 +1,64 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::tracing::{self, instrument}; + +use crate::{ + enums, + query::generics, + schema::dashboard_metadata::dsl, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + PgPooledConn, StorageResult, +}; + +impl DashboardMetadataNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl DashboardMetadata { + pub async fn find_user_scoped_dashboard_metadata( + conn: &PgPooledConn, + user_id: String, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .eq(user_id) + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } + + pub async fn find_merchant_scoped_dashboard_metadata( + conn: &PgPooledConn, + merchant_id: String, + org_id: String, + data_types: Vec, + ) -> StorageResult> { + let predicate = dsl::user_id + .is_null() + .and(dsl::merchant_id.eq(merchant_id)) + .and(dsl::org_id.eq(org_id)) + .and(dsl::data_key.eq_any(data_types)); + + generics::generic_filter::<::Table, _, _, _>( + conn, + predicate, + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 33400635f052..6cab6d5730d0 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -183,6 +183,30 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + dashboard_metadata (id) { + id -> Int4, + #[max_length = 64] + user_id -> Nullable, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + org_id -> Varchar, + #[max_length = 64] + data_key -> Varchar, + data_value -> Json, + #[max_length = 64] + created_by -> Varchar, + created_at -> Timestamp, + #[max_length = 64] + last_modified_by -> Varchar, + last_modified_at -> Timestamp, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -965,6 +989,7 @@ diesel::allow_tables_to_appear_in_same_query!( cards_info, configs, customers, + dashboard_metadata, dispute, events, file_metadata, diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs index 6a2e864b291c..4eec710ea185 100644 --- a/crates/diesel_models/src/user.rs +++ b/crates/diesel_models/src/user.rs @@ -5,6 +5,8 @@ use time::PrimitiveDateTime; use crate::schema::users; +pub mod dashboard_metadata; + #[derive(Clone, Debug, Identifiable, Queryable)] #[diesel(table_name = users)] pub struct User { diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs new file mode 100644 index 000000000000..018808f1c0db --- /dev/null +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -0,0 +1,35 @@ +use diesel::{query_builder::AsChangeset, Identifiable, Insertable, Queryable}; +use time::PrimitiveDateTime; + +use crate::{enums, schema::dashboard_metadata}; + +#[derive(Clone, Debug, Identifiable, Queryable)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadata { + pub id: i32, + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} + +#[derive( + router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset, +)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadataNew { + pub user_id: Option, + pub merchant_id: String, + pub org_id: String, + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub created_by: String, + pub created_at: PrimitiveDateTime, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index f5c3b46b27f2..f9bfcae1ca10 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -503,15 +503,6 @@ impl Default for super::settings::RequiredFields { value: None, } ), - ( - "payment_method_data.card.card_holder_name".to_string(), - RequiredFieldInfo { - required_field: "payment_method_data.card.card_holder_name".to_string(), - display_name: "card_holder_name".to_string(), - field_type: enums::FieldType::UserFullName, - value: None, - } - ), ( "email".to_string(), RequiredFieldInfo { @@ -2418,6 +2409,129 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "payment_method_data.card.card_number".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_number".to_string(), + display_name: "card_number".to_string(), + field_type: enums::FieldType::UserCardNumber, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_month".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_month".to_string(), + display_name: "card_exp_month".to_string(), + field_type: enums::FieldType::UserCardExpiryMonth, + value: None, + } + ), + ( + "payment_method_data.card.card_exp_year".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_exp_year".to_string(), + display_name: "card_exp_year".to_string(), + field_type: enums::FieldType::UserCardExpiryYear, + value: None, + } + ), + ( + "payment_method_data.card.card_cvc".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.card.card_cvc".to_string(), + display_name: "card_cvc".to_string(), + field_type: enums::FieldType::UserCardCvc, + value: None, + } + ), + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ( enums::Connector::Bluesnap, RequiredFieldFinal { @@ -4250,6 +4364,93 @@ impl Default for super::settings::RequiredFields { common: HashMap::new(), } ), + ( + enums::Connector::Bankofamerica, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from( + [ + ( + "email".to_string(), + RequiredFieldInfo { + required_field: "email".to_string(), + display_name: "email".to_string(), + field_type: enums::FieldType::UserEmailAddress, + value: None, + } + ), + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.city".to_string(), + RequiredFieldInfo { + required_field: "billing.address.city".to_string(), + display_name: "city".to_string(), + field_type: enums::FieldType::UserAddressCity, + value: None, + } + ), + ( + "billing.address.state".to_string(), + RequiredFieldInfo { + required_field: "billing.address.state".to_string(), + display_name: "state".to_string(), + field_type: enums::FieldType::UserAddressState, + value: None, + } + ), + ( + "billing.address.zip".to_string(), + RequiredFieldInfo { + required_field: "billing.address.zip".to_string(), + display_name: "zip".to_string(), + field_type: enums::FieldType::UserAddressPincode, + value: None, + } + ), + ( + "billing.address.country".to_string(), + RequiredFieldInfo { + required_field: "billing.address.country".to_string(), + display_name: "country".to_string(), + field_type: enums::FieldType::UserAddressCountry{ + options: vec![ + "ALL".to_string(), + ] + }, + value: None, + } + ), + ( + "billing.address.line1".to_string(), + RequiredFieldInfo { + required_field: "billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, + value: None, + } + ), + ] + ), + common: HashMap::new(), + } + ), ]), }, ), diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 331d1e3ed5ea..ba600917ecca 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -27,14 +27,20 @@ pub enum UserErrors { MerchantAccountCreationError(String), #[error("InvalidEmailError")] InvalidEmailError, - #[error("MerchantIdNotFound")] - MerchantIdNotFound, #[error("DuplicateOrganizationId")] DuplicateOrganizationId, + #[error("MerchantIdNotFound")] + MerchantIdNotFound, + #[error("MetadataAlreadySet")] + MetadataAlreadySet, #[error("InvalidRoleId")] InvalidRoleId, #[error("InvalidRoleOperation")] InvalidRoleOperation, + #[error("IpAddressParsingFailed")] + IpAddressParsingFailed, + #[error("InvalidMetadataRequest")] + InvalidMetadataRequest, #[error("MerchantIdParsingError")] MerchantIdParsingError, } @@ -86,6 +92,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None)) } + Self::MetadataAlreadySet => { + AER::BadRequest(ApiError::new(sub_code, 19, "Metadata already set", None)) + } Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( sub_code, 21, @@ -101,6 +110,15 @@ impl common_utils::errors::ErrorSwitch { + AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None)) + } + Self::InvalidMetadataRequest => AER::BadRequest(ApiError::new( + sub_code, + 26, + "Invalid Metadata Request", + None, + )), Self::MerchantIdParsingError => { AER::BadRequest(ApiError::new(sub_code, 28, "Invalid Merchant Id", None)) } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index eb1a5ad44576..8e7f6c27a7da 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -14,6 +14,8 @@ use crate::{ utils, }; +pub mod dashboard_metadata; + pub async fn connect_account( state: AppState, request: user_api::ConnectAccountRequest, diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs new file mode 100644 index 000000000000..de385fb8ed65 --- /dev/null +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -0,0 +1,537 @@ +use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, +}; +use error_stack::ResultExt; + +use crate::{ + core::errors::{UserErrors, UserResponse, UserResult}, + routes::AppState, + services::{authentication::UserFromToken, ApplicationResponse}, + types::domain::{user::dashboard_metadata as types, MerchantKeyStore}, + utils::user::dashboard_metadata as utils, +}; + +pub async fn set_metadata( + state: AppState, + user: UserFromToken, + request: api::SetMetaDataRequest, +) -> UserResponse<()> { + let metadata_value = parse_set_request(request)?; + let metadata_key = DBEnum::from(&metadata_value); + + insert_metadata(&state, user, metadata_key, metadata_value).await?; + + Ok(ApplicationResponse::StatusOk) +} + +pub async fn get_multiple_metadata( + state: AppState, + user: UserFromToken, + request: GetMultipleMetaDataPayload, +) -> UserResponse> { + let metadata_keys: Vec = request.results.into_iter().map(parse_get_request).collect(); + + let metadata = fetch_metadata(&state, &user, metadata_keys.clone()).await?; + + let mut response = Vec::with_capacity(metadata_keys.len()); + for key in metadata_keys { + let data = metadata.iter().find(|ele| ele.data_key == key); + let resp; + if data.is_none() && utils::is_backfill_required(&key) { + let backfill_data = backfill_metadata(&state, &user, &key).await?; + resp = into_response(backfill_data.as_ref(), &key)?; + } else { + resp = into_response(data, &key)?; + } + response.push(resp); + } + + Ok(ApplicationResponse::Json(response)) +} + +fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult { + match data_enum { + api::SetMetaDataRequest::ProductionAgreement(req) => { + let ip_address = req + .ip_address + .ok_or(UserErrors::InternalServerError.into()) + .attach_printable("Error Getting Ip Address")?; + Ok(types::MetaData::ProductionAgreement( + types::ProductionAgreementValue { + version: req.version, + ip_address, + timestamp: common_utils::date_time::now(), + }, + )) + } + api::SetMetaDataRequest::SetupProcessor(req) => Ok(types::MetaData::SetupProcessor(req)), + api::SetMetaDataRequest::ConfigureEndpoint => Ok(types::MetaData::ConfigureEndpoint(true)), + api::SetMetaDataRequest::SetupComplete => Ok(types::MetaData::SetupComplete(true)), + api::SetMetaDataRequest::FirstProcessorConnected(req) => { + Ok(types::MetaData::FirstProcessorConnected(req)) + } + api::SetMetaDataRequest::SecondProcessorConnected(req) => { + Ok(types::MetaData::SecondProcessorConnected(req)) + } + api::SetMetaDataRequest::ConfiguredRouting(req) => { + Ok(types::MetaData::ConfiguredRouting(req)) + } + api::SetMetaDataRequest::TestPayment(req) => Ok(types::MetaData::TestPayment(req)), + api::SetMetaDataRequest::IntegrationMethod(req) => { + Ok(types::MetaData::IntegrationMethod(req)) + } + api::SetMetaDataRequest::IntegrationCompleted => { + Ok(types::MetaData::IntegrationCompleted(true)) + } + api::SetMetaDataRequest::SPRoutingConfigured(req) => { + Ok(types::MetaData::SPRoutingConfigured(req)) + } + api::SetMetaDataRequest::SPTestPayment => Ok(types::MetaData::SPTestPayment(true)), + api::SetMetaDataRequest::DownloadWoocom => Ok(types::MetaData::DownloadWoocom(true)), + api::SetMetaDataRequest::ConfigureWoocom => Ok(types::MetaData::ConfigureWoocom(true)), + api::SetMetaDataRequest::SetupWoocomWebhook => { + Ok(types::MetaData::SetupWoocomWebhook(true)) + } + api::SetMetaDataRequest::IsMultipleConfiguration => { + Ok(types::MetaData::IsMultipleConfiguration(true)) + } + } +} + +fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum { + match data_enum { + api::GetMetaDataRequest::ProductionAgreement => DBEnum::ProductionAgreement, + api::GetMetaDataRequest::SetupProcessor => DBEnum::SetupProcessor, + api::GetMetaDataRequest::ConfigureEndpoint => DBEnum::ConfigureEndpoint, + api::GetMetaDataRequest::SetupComplete => DBEnum::SetupComplete, + api::GetMetaDataRequest::FirstProcessorConnected => DBEnum::FirstProcessorConnected, + api::GetMetaDataRequest::SecondProcessorConnected => DBEnum::SecondProcessorConnected, + api::GetMetaDataRequest::ConfiguredRouting => DBEnum::ConfiguredRouting, + api::GetMetaDataRequest::TestPayment => DBEnum::TestPayment, + api::GetMetaDataRequest::IntegrationMethod => DBEnum::IntegrationMethod, + api::GetMetaDataRequest::IntegrationCompleted => DBEnum::IntegrationCompleted, + api::GetMetaDataRequest::StripeConnected => DBEnum::StripeConnected, + api::GetMetaDataRequest::PaypalConnected => DBEnum::PaypalConnected, + api::GetMetaDataRequest::SPRoutingConfigured => DBEnum::SpRoutingConfigured, + api::GetMetaDataRequest::SPTestPayment => DBEnum::SpTestPayment, + api::GetMetaDataRequest::DownloadWoocom => DBEnum::DownloadWoocom, + api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom, + api::GetMetaDataRequest::SetupWoocomWebhook => DBEnum::SetupWoocomWebhook, + api::GetMetaDataRequest::IsMultipleConfiguration => DBEnum::IsMultipleConfiguration, + } +} + +fn into_response( + data: Option<&DashboardMetadata>, + data_type: &DBEnum, +) -> UserResult { + match data_type { + DBEnum::ProductionAgreement => Ok(api::GetMetaDataResponse::ProductionAgreement( + data.is_some(), + )), + DBEnum::SetupProcessor => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SetupProcessor(resp)) + } + DBEnum::ConfigureEndpoint => { + Ok(api::GetMetaDataResponse::ConfigureEndpoint(data.is_some())) + } + DBEnum::SetupComplete => Ok(api::GetMetaDataResponse::SetupComplete(data.is_some())), + DBEnum::FirstProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::FirstProcessorConnected(resp)) + } + DBEnum::SecondProcessorConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SecondProcessorConnected(resp)) + } + DBEnum::ConfiguredRouting => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ConfiguredRouting(resp)) + } + DBEnum::TestPayment => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::TestPayment(resp)) + } + DBEnum::IntegrationMethod => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::IntegrationMethod(resp)) + } + DBEnum::IntegrationCompleted => Ok(api::GetMetaDataResponse::IntegrationCompleted( + data.is_some(), + )), + DBEnum::StripeConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::StripeConnected(resp)) + } + DBEnum::PaypalConnected => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::PaypalConnected(resp)) + } + DBEnum::SpRoutingConfigured => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::SPRoutingConfigured(resp)) + } + DBEnum::SpTestPayment => Ok(api::GetMetaDataResponse::SPTestPayment(data.is_some())), + DBEnum::DownloadWoocom => Ok(api::GetMetaDataResponse::DownloadWoocom(data.is_some())), + DBEnum::ConfigureWoocom => Ok(api::GetMetaDataResponse::ConfigureWoocom(data.is_some())), + DBEnum::SetupWoocomWebhook => { + Ok(api::GetMetaDataResponse::SetupWoocomWebhook(data.is_some())) + } + + DBEnum::IsMultipleConfiguration => Ok(api::GetMetaDataResponse::IsMultipleConfiguration( + data.is_some(), + )), + } +} + +async fn insert_metadata( + state: &AppState, + user: UserFromToken, + metadata_key: DBEnum, + metadata_value: types::MetaData, +) -> UserResult { + match metadata_value { + types::MetaData::ProductionAgreement(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupProcessor(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureEndpoint(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupComplete(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::FirstProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SecondProcessorConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfiguredRouting(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::TestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationMethod(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IntegrationCompleted(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::StripeConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::PaypalConnected(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPRoutingConfigured(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SPTestPayment(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::DownloadWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::ConfigureWoocom(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::SetupWoocomWebhook(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + types::MetaData::IsMultipleConfiguration(data) => { + utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + } + } +} + +async fn fetch_metadata( + state: &AppState, + user: &UserFromToken, + metadata_keys: Vec, +) -> UserResult> { + let mut dashboard_metadata = Vec::with_capacity(metadata_keys.len()); + let (merchant_scoped_enums, _) = utils::separate_metadata_type_based_on_scope(metadata_keys); + + if !merchant_scoped_enums.is_empty() { + let mut res = utils::get_merchant_scoped_metadata_from_db( + state, + user.merchant_id.to_owned(), + user.org_id.to_owned(), + merchant_scoped_enums, + ) + .await?; + dashboard_metadata.append(&mut res); + } + + Ok(dashboard_metadata) +} + +pub async fn backfill_metadata( + state: &AppState, + user: &UserFromToken, + key: &DBEnum, +) -> UserResult> { + let key_store = state + .store + .get_merchant_key_store_by_merchant_id( + &user.merchant_id, + &state.store.get_master_key().to_vec().into(), + ) + .await + .change_context(UserErrors::InternalServerError)?; + + match key { + DBEnum::StripeConnected => { + let mca = if let Some(stripe_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Stripe + .to_string() + .as_str(), + &key_store, + ) + .await? + { + stripe_connected + } else if let Some(stripe_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "stripe_test", + &key_store, + ) + .await? + { + stripe_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::StripeConnected, + types::MetaData::StripeConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + DBEnum::PaypalConnected => { + let mca = if let Some(paypal_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + api_models::enums::RoutableConnectors::Paypal + .to_string() + .as_str(), + &key_store, + ) + .await? + { + paypal_connected + } else if let Some(paypal_test_connected) = get_merchant_connector_account_by_name( + state, + &user.merchant_id, + //TODO: Use Enum with proper feature flag + "paypal_test", + &key_store, + ) + .await? + { + paypal_test_connected + } else { + return Ok(None); + }; + + Some( + insert_metadata( + state, + user.to_owned(), + DBEnum::PaypalConnected, + types::MetaData::PaypalConnected(api::ProcessorConnected { + processor_id: mca.merchant_connector_id, + processor_name: mca.connector_name, + }), + ) + .await, + ) + .transpose() + } + _ => Ok(None), + } +} + +pub async fn get_merchant_connector_account_by_name( + state: &AppState, + merchant_id: &str, + connector_name: &str, + key_store: &MerchantKeyStore, +) -> UserResult> { + state + .store + .find_merchant_connector_account_by_merchant_id_connector_name( + merchant_id, + connector_name, + key_store, + ) + .await + .map_err(|e| { + e.change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData") + }) + .map(|data| data.first().cloned()) +} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 549bda78eda8..086a09b805c6 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -6,6 +6,7 @@ pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod ephemeral_key; pub mod events; @@ -68,6 +69,7 @@ pub trait StorageInterface: + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface + + dashboard_metadata::DashboardMetadataInterface + dispute::DisputeInterface + ephemeral_key::EphemeralKeyInterface + events::EventInterface diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs new file mode 100644 index 000000000000..2e8129398ca3 --- /dev/null +++ b/crates/router/src/db/dashboard_metadata.rs @@ -0,0 +1,184 @@ +use diesel_models::{enums, user::dashboard_metadata as storage}; +use error_stack::{IntoReport, ResultExt}; +use storage_impl::MockDb; + +use crate::{ + connection, + core::errors::{self, CustomResult}, + services::Store, +}; + +#[async_trait::async_trait] +pub trait DashboardMetadataInterface { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult; + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for Store { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + metadata + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_user_scoped_dashboard_metadata( + &conn, + user_id.to_owned(), + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::find_merchant_scoped_dashboard_metadata( + &conn, + merchant_id.to_owned(), + org_id.to_owned(), + data_keys, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl DashboardMetadataInterface for MockDb { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + if dashboard_metadata.iter().any(|metadata_inner| { + metadata_inner.user_id == metadata.user_id + && metadata_inner.merchant_id == metadata.merchant_id + && metadata_inner.org_id == metadata.org_id + && metadata_inner.data_key == metadata.data_key + }) { + Err(errors::StorageError::DuplicateValue { + entity: "user_id, merchant_id, org_id and data_key", + key: None, + })? + } + let metadata_new = storage::DashboardMetadata { + id: dashboard_metadata + .len() + .try_into() + .into_report() + .change_context(errors::StorageError::MockDbError)?, + user_id: metadata.user_id, + merchant_id: metadata.merchant_id, + org_id: metadata.org_id, + data_key: metadata.data_key, + data_value: metadata.data_value, + created_by: metadata.created_by, + created_at: metadata.created_at, + last_modified_by: metadata.last_modified_by, + last_modified_at: metadata.last_modified_at, + }; + dashboard_metadata.push(metadata_new.clone()); + Ok(metadata_new) + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner + .user_id + .clone() + .map(|user_id_inner| user_id_inner == user_id) + .unwrap_or(false) + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for user_id = {user_id},\ + merchant_id = {merchant_id}, org_id = {org_id} and data_keys = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } + + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + let dashboard_metadata = self.dashboard_metadata.lock().await; + let query_result = dashboard_metadata + .iter() + .filter(|metadata_inner| { + metadata_inner.user_id.is_none() + && metadata_inner.merchant_id == merchant_id + && metadata_inner.org_id == org_id + && data_keys.contains(&metadata_inner.data_key) + }) + .cloned() + .collect::>(); + + if query_result.is_empty() { + return Err(errors::StorageError::ValueNotFound(format!( + "No dashboard_metadata available for merchant_id = {merchant_id},\ + org_id = {org_id} and data_keyss = {data_keys:?}", + )) + .into()); + } + Ok(query_result) + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 9cf1a7b80b8b..fcceba7fadba 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -6,6 +6,7 @@ use data_models::payments::{ payment_attempt::PaymentAttemptInterface, payment_intent::PaymentIntentInterface, }; use diesel_models::{ + enums, enums::ProcessTrackerStatus, ephemeral_key::{EphemeralKey, EphemeralKeyNew}, reverse_lookup::{ReverseLookup, ReverseLookupNew}, @@ -21,7 +22,10 @@ use scheduler::{ use storage_impl::redis::kv_store::RedisConnInterface; use time::PrimitiveDateTime; -use super::{user::UserInterface, user_role::UserRoleInterface}; +use super::{ + dashboard_metadata::DashboardMetadataInterface, user::UserInterface, + user_role::UserRoleInterface, +}; use crate::{ core::errors::{self, ProcessTrackerError}, db::{ @@ -1915,3 +1919,35 @@ impl UserRoleInterface for KafkaStore { self.diesel_store.list_user_roles_by_user_id(user_id).await } } + +#[async_trait::async_trait] +impl DashboardMetadataInterface for KafkaStore { + async fn insert_metadata( + &self, + metadata: storage::DashboardMetadataNew, + ) -> CustomResult { + self.diesel_store.insert_metadata(metadata).await + } + + async fn find_user_scoped_dashboard_metadata( + &self, + user_id: &str, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_user_scoped_dashboard_metadata(user_id, merchant_id, org_id, data_keys) + .await + } + async fn find_merchant_scoped_dashboard_metadata( + &self, + merchant_id: &str, + org_id: &str, + data_keys: Vec, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys) + .await + } +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index d73d2ed5c8e6..5f0c89ed6b4c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -807,6 +807,11 @@ impl User { .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) .service(web::resource("/change_password").route(web::post().to(change_password))) + .service( + web::resource("/data/merchant") + .route(web::post().to(set_merchant_scoped_dashboard_metadata)), + ) + .service(web::resource("/data").route(web::get().to(get_multiple_dashboard_metadata))) .service(web::resource("/internal_signup").route(web::post().to(internal_user_signup))) .service(web::resource("/switch_merchant").route(web::post().to(switch_merchant_id))) .service( diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index aa74552aa671..552deb85a2e1 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -150,6 +150,8 @@ impl From for ApiIdentifier { Flow::UserConnectAccount | Flow::ChangePassword + | Flow::SetDashboardMetadata + | Flow::GetMutltipleDashboardMetadata | Flow::VerifyPaymentConnector | Flow::InternalUserSignup | Flow::SwitchMerchant diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index e27a030785ae..89c4bd4c90ec 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -1,5 +1,6 @@ use actix_web::{web, HttpRequest, HttpResponse}; -use api_models::user as user_api; +use api_models::{errors::types::ApiErrorResponse, user as user_api}; +use common_utils::errors::ReportSwitchExt; use router_env::Flow; use super::AppState; @@ -10,6 +11,7 @@ use crate::{ authentication::{self as auth}, authorization::permissions::Permission, }, + utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required}, }; pub async fn user_connect_account( @@ -49,6 +51,58 @@ pub async fn change_password( .await } +pub async fn set_merchant_scoped_dashboard_metadata( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SetDashboardMetadata; + let mut payload = json_payload.into_inner(); + + if let Err(e) = common_utils::errors::ReportSwitchExt::<(), ApiErrorResponse>::switch( + set_ip_address_if_required(&mut payload, req.headers()), + ) { + return api::log_and_return_error_response(e); + } + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user_core::dashboard_metadata::set_metadata, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_multiple_dashboard_metadata( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { + let flow = Flow::GetMutltipleDashboardMetadata; + let payload = match ReportSwitchExt::<_, ApiErrorResponse>::switch(parse_string_to_enums( + query.into_inner().keys, + )) { + Ok(payload) => payload, + Err(e) => { + return api::log_and_return_error_response(e); + } + }; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + user_core::dashboard_metadata::get_multiple_metadata, + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + pub async fn internal_user_signup( state: web::Data, http_req: HttpRequest, diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 6bbe9149f4d7..fe99d084223a 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -124,9 +124,10 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> .unwrap_or(merchant_account.redirect_to_merchant_with_http_post), webhook_details: webhook_details.or(merchant_account.webhook_details), metadata: request.metadata, - routing_algorithm: request - .routing_algorithm - .or(merchant_account.routing_algorithm), + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), intent_fulfillment_time: request .intent_fulfillment_time .map(i64::from) diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 10f751d90862..35280e55bb0b 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -33,6 +33,8 @@ use crate::{ utils::user::password, }; +pub mod dashboard_metadata; + #[derive(Clone)] pub struct UserName(Secret); diff --git a/crates/router/src/types/domain/user/dashboard_metadata.rs b/crates/router/src/types/domain/user/dashboard_metadata.rs new file mode 100644 index 000000000000..e65379346ac9 --- /dev/null +++ b/crates/router/src/types/domain/user/dashboard_metadata.rs @@ -0,0 +1,56 @@ +use api_models::user::dashboard_metadata as api; +use diesel_models::enums::DashboardMetadata as DBEnum; +use masking::Secret; +use time::PrimitiveDateTime; + +pub enum MetaData { + ProductionAgreement(ProductionAgreementValue), + SetupProcessor(api::SetupProcessor), + ConfigureEndpoint(bool), + SetupComplete(bool), + FirstProcessorConnected(api::ProcessorConnected), + SecondProcessorConnected(api::ProcessorConnected), + ConfiguredRouting(api::ConfiguredRouting), + TestPayment(api::TestPayment), + IntegrationMethod(api::IntegrationMethod), + IntegrationCompleted(bool), + StripeConnected(api::ProcessorConnected), + PaypalConnected(api::ProcessorConnected), + SPRoutingConfigured(api::ConfiguredRouting), + SPTestPayment(bool), + DownloadWoocom(bool), + ConfigureWoocom(bool), + SetupWoocomWebhook(bool), + IsMultipleConfiguration(bool), +} + +impl From<&MetaData> for DBEnum { + fn from(value: &MetaData) -> Self { + match value { + MetaData::ProductionAgreement(_) => Self::ProductionAgreement, + MetaData::SetupProcessor(_) => Self::SetupProcessor, + MetaData::ConfigureEndpoint(_) => Self::ConfigureEndpoint, + MetaData::SetupComplete(_) => Self::SetupComplete, + MetaData::FirstProcessorConnected(_) => Self::FirstProcessorConnected, + MetaData::SecondProcessorConnected(_) => Self::SecondProcessorConnected, + MetaData::ConfiguredRouting(_) => Self::ConfiguredRouting, + MetaData::TestPayment(_) => Self::TestPayment, + MetaData::IntegrationMethod(_) => Self::IntegrationMethod, + MetaData::IntegrationCompleted(_) => Self::IntegrationCompleted, + MetaData::StripeConnected(_) => Self::StripeConnected, + MetaData::PaypalConnected(_) => Self::PaypalConnected, + MetaData::SPRoutingConfigured(_) => Self::SpRoutingConfigured, + MetaData::SPTestPayment(_) => Self::SpTestPayment, + MetaData::DownloadWoocom(_) => Self::DownloadWoocom, + MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom, + MetaData::SetupWoocomWebhook(_) => Self::SetupWoocomWebhook, + MetaData::IsMultipleConfiguration(_) => Self::IsMultipleConfiguration, + } + } +} +#[derive(Debug, serde::Serialize)] +pub struct ProductionAgreementValue { + pub version: String, + pub ip_address: Secret, + pub timestamp: PrimitiveDateTime, +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index e3e19323357b..a83a405f3554 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -5,6 +5,7 @@ pub mod capture; pub mod cards_info; pub mod configs; pub mod customers; +pub mod dashboard_metadata; pub mod dispute; pub mod enums; pub mod ephemeral_key; @@ -42,11 +43,11 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, dispute::*, - ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, - merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, - payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, - reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, + address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, + dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, + locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, + merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, + process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, }; use crate::types::api::routing; diff --git a/crates/router/src/types/storage/dashboard_metadata.rs b/crates/router/src/types/storage/dashboard_metadata.rs new file mode 100644 index 000000000000..d804dfb1ff8b --- /dev/null +++ b/crates/router/src/types/storage/dashboard_metadata.rs @@ -0,0 +1 @@ +pub use diesel_models::user::dashboard_metadata::*; diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 0705bb8b8949..4dc54ba3f708 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -7,6 +7,7 @@ use crate::{ types::domain::MerchantAccount, }; +pub mod dashboard_metadata; pub mod password; impl UserFromToken { diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs new file mode 100644 index 000000000000..5f354e613f95 --- /dev/null +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -0,0 +1,162 @@ +use std::{net::IpAddr, str::FromStr}; + +use actix_web::http::header::HeaderMap; +use api_models::user::dashboard_metadata::{ + GetMetaDataRequest, GetMultipleMetaDataPayload, SetMetaDataRequest, +}; +use diesel_models::{ + enums::DashboardMetadata as DBEnum, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, +}; +use error_stack::{IntoReport, ResultExt}; +use masking::Secret; + +use crate::{ + core::errors::{UserErrors, UserResult}, + headers, AppState, +}; + +pub async fn insert_merchant_scoped_metadata_to_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + let now = common_utils::date_time::now(); + let data_value = serde_json::to_value(metadata_value) + .into_report() + .change_context(UserErrors::InternalServerError) + .attach_printable("Error Converting Struct To Serde Value")?; + state + .store + .insert_metadata(DashboardMetadataNew { + user_id: None, + merchant_id, + org_id, + data_key: metadata_key, + data_value, + created_by: user_id.clone(), + created_at: now, + last_modified_by: user_id, + last_modified_at: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + return e.change_context(UserErrors::MetadataAlreadySet); + } + e.change_context(UserErrors::InternalServerError) + }) +} + +pub async fn get_merchant_scoped_metadata_from_db( + state: &AppState, + merchant_id: String, + org_id: String, + metadata_keys: Vec, +) -> UserResult> { + match state + .store + .find_merchant_scoped_dashboard_metadata(&merchant_id, &org_id, metadata_keys) + .await + { + Ok(data) => Ok(data), + Err(e) => { + if e.current_context().is_db_not_found() { + return Ok(Vec::with_capacity(0)); + } + Err(e + .change_context(UserErrors::InternalServerError) + .attach_printable("DB Error Fetching DashboardMetaData")) + } + } +} + +pub fn deserialize_to_response(data: Option<&DashboardMetadata>) -> UserResult> +where + T: serde::de::DeserializeOwned, +{ + data.map(|metadata| serde_json::from_value(metadata.data_value.clone())) + .transpose() + .map_err(|_| UserErrors::InternalServerError.into()) + .attach_printable("Error Serializing Metadata from DB") +} + +pub fn separate_metadata_type_based_on_scope( + metadata_keys: Vec, +) -> (Vec, Vec) { + let (mut merchant_scoped, user_scoped) = ( + Vec::with_capacity(metadata_keys.len()), + Vec::with_capacity(metadata_keys.len()), + ); + for key in metadata_keys { + match key { + DBEnum::ProductionAgreement + | DBEnum::SetupProcessor + | DBEnum::ConfigureEndpoint + | DBEnum::SetupComplete + | DBEnum::FirstProcessorConnected + | DBEnum::SecondProcessorConnected + | DBEnum::ConfiguredRouting + | DBEnum::TestPayment + | DBEnum::IntegrationMethod + | DBEnum::IntegrationCompleted + | DBEnum::StripeConnected + | DBEnum::PaypalConnected + | DBEnum::SpRoutingConfigured + | DBEnum::SpTestPayment + | DBEnum::DownloadWoocom + | DBEnum::ConfigureWoocom + | DBEnum::SetupWoocomWebhook + | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key), + } + } + (merchant_scoped, user_scoped) +} + +pub fn is_backfill_required(metadata_key: &DBEnum) -> bool { + matches!( + metadata_key, + DBEnum::StripeConnected | DBEnum::PaypalConnected + ) +} + +pub fn set_ip_address_if_required( + request: &mut SetMetaDataRequest, + headers: &HeaderMap, +) -> UserResult<()> { + if let SetMetaDataRequest::ProductionAgreement(req) = request { + let ip_address_from_request: Secret = headers + .get(headers::X_FORWARDED_FOR) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("X-Forwarded-For header not found")? + .to_str() + .map_err(|_| UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error converting Header Value to Str")? + .split(',') + .next() + .and_then(|ip| { + let ip_addr: Result = ip.parse(); + ip_addr.ok() + }) + .ok_or(UserErrors::IpAddressParsingFailed.into()) + .attach_printable("Error Parsing header value to ip")? + .to_string() + .into(); + req.ip_address = Some(ip_address_from_request) + } + Ok(()) +} + +pub fn parse_string_to_enums(query: String) -> UserResult { + Ok(GetMultipleMetaDataPayload { + results: query + .split(',') + .map(GetMetaDataRequest::from_str) + .collect::, _>>() + .map_err(|_| UserErrors::InvalidMetadataRequest.into()) + .attach_printable("Error Parsing to DashboardMetadata enums")?, + }) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 02f3e5381dfa..eefdc86affad 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -259,6 +259,10 @@ pub enum Flow { DecisionManagerRetrieveConfig, /// Change password flow ChangePassword, + /// Set Dashboard Metadata flow + SetDashboardMetadata, + /// Get Multiple Dashboard Metadata flow + GetMutltipleDashboardMetadata, /// Payment Connector Verify VerifyPaymentConnector, /// Internal user signup diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index 4cdf8e2456bb..e22d39ce70c8 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -43,6 +43,7 @@ pub struct MockDb { pub organizations: Arc>>, pub users: Arc>>, pub user_roles: Arc>>, + pub dashboard_metadata: Arc>>, } impl MockDb { @@ -78,6 +79,7 @@ impl MockDb { organizations: Default::default(), users: Default::default(), user_roles: Default::default(), + dashboard_metadata: Default::default(), }) } } diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql new file mode 100644 index 000000000000..746fb42109e9 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX IF EXISTS dashboard_metadata_index; +DROP TABLE IF EXISTS dashboard_metadata; \ No newline at end of file diff --git a/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql new file mode 100644 index 000000000000..8296f755f543 --- /dev/null +++ b/migrations/2023-11-23-100644_create_dashboard_metadata_table/up.sql @@ -0,0 +1,15 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS dashboard_metadata ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(64), + merchant_id VARCHAR(64) NOT NULL, + org_id VARCHAR(64) NOT NULL, + data_key VARCHAR(64) NOT NULL, + data_value JSON NOT NULL, + created_by VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now(), + last_modified_by VARCHAR(64) NOT NULL, + last_modified_at TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX IF NOT EXISTS dashboard_metadata_index ON dashboard_metadata (COALESCE(user_id,'0'), merchant_id, org_id, data_key); \ No newline at end of file