feat(user): add support for dashboard metadata (#3000)

Co-authored-by: Rachit Naithani <81706961+racnan@users.noreply.github.com>
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com>
Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com>
Co-authored-by: Arjun Karthik <m.arjunkarthik@gmail.com>
Co-authored-by: Brian Silah <71752651+unpervertedkid@users.noreply.github.com>
This commit is contained in:
Apoorv Dixit
2023-11-30 14:58:37 +05:30
committed by GitHub
parent b1fe76a82b
commit 6a2e4ab416
28 changed files with 1389 additions and 16 deletions

View File

@ -1,6 +1,11 @@
use common_utils::events::{ApiEventMetric, ApiEventsType};
use crate::user::{ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse};
use crate::user::{
dashboard_metadata::{
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
},
ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse,
};
impl ApiEventMetric for ConnectAccountResponse {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
@ -13,4 +18,10 @@ impl ApiEventMetric for ConnectAccountResponse {
impl ApiEventMetric for ConnectAccountRequest {}
common_utils::impl_misc_api_event_type!(ChangePasswordRequest);
common_utils::impl_misc_api_event_type!(
ChangePasswordRequest,
GetMultipleMetaDataPayload,
GetMetaDataResponse,
GetMetaDataRequest,
SetMetaDataRequest
);

View File

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

View File

@ -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<Secret<String, common_utils::pii::IpAddress>>,
}
#[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<GetMetaDataRequest>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct GetMultipleMetaDataRequest {
pub keys: String,
}
#[derive(Debug, serde::Serialize)]
pub enum GetMetaDataResponse {
ProductionAgreement(bool),
SetupProcessor(Option<SetupProcessor>),
ConfigureEndpoint(bool),
SetupComplete(bool),
FirstProcessorConnected(Option<ProcessorConnected>),
SecondProcessorConnected(Option<ProcessorConnected>),
ConfiguredRouting(Option<ConfiguredRouting>),
TestPayment(Option<TestPayment>),
IntegrationMethod(Option<IntegrationMethod>),
IntegrationCompleted(bool),
StripeConnected(Option<ProcessorConnected>),
PaypalConnected(Option<ProcessorConnected>),
SPRoutingConfigured(Option<ConfiguredRouting>),
SPTestPayment(bool),
DownloadWoocom(bool),
ConfigureWoocom(bool),
SetupWoocomWebhook(bool),
IsMultipleConfiguration(bool),
}

View File

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

View File

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

View File

@ -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<DashboardMetadata> {
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<enums::DashboardMetadata>,
) -> StorageResult<Vec<Self>> {
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::<<Self as HasTable>::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<enums::DashboardMetadata>,
) -> StorageResult<Vec<Self>> {
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::<<Self as HasTable>::Table, _, _, _>(
conn,
predicate,
None,
None,
Some(dsl::last_modified_at.asc()),
)
.await
}
}

View File

@ -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<Varchar>,
#[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,

View File

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

View File

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

View File

@ -27,10 +27,16 @@ pub enum UserErrors {
MerchantAccountCreationError(String),
#[error("InvalidEmailError")]
InvalidEmailError,
#[error("DuplicateOrganizationId")]
DuplicateOrganizationId,
#[error("MerchantIdNotFound")]
MerchantIdNotFound,
#[error("MetadataAlreadySet")]
MetadataAlreadySet,
#[error("DuplicateOrganizationId")]
DuplicateOrganizationId,
#[error("IpAddressParsingFailed")]
IpAddressParsingFailed,
#[error("InvalidMetadataRequest")]
InvalidMetadataRequest,
}
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
@ -77,15 +83,27 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
Self::InvalidEmailError => {
AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None))
}
Self::MerchantIdNotFound => {
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,
"An Organization with the id already exists",
None,
)),
Self::MerchantIdNotFound => {
AER::BadRequest(ApiError::new(sub_code, 18, "Invalid Merchant ID", None))
Self::IpAddressParsingFailed => {
AER::InternalServerError(ApiError::new(sub_code, 24, "Something Went Wrong", None))
}
Self::InvalidMetadataRequest => AER::BadRequest(ApiError::new(
sub_code,
26,
"Invalid Metadata Request",
None,
)),
}
}
}

View File

@ -13,6 +13,8 @@ use crate::{
types::domain,
};
pub mod dashboard_metadata;
pub async fn connect_account(
state: AppState,
request: api::ConnectAccountRequest,

View File

@ -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<Vec<api::GetMetaDataResponse>> {
let metadata_keys: Vec<DBEnum> = 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<types::MetaData> {
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<api::GetMetaDataResponse> {
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<DashboardMetadata> {
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<DBEnum>,
) -> UserResult<Vec<DashboardMetadata>> {
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<Option<DashboardMetadata>> {
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<Option<crate::types::domain::MerchantConnectorAccount>> {
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())
}

View File

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

View File

@ -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<storage::DashboardMetadata, errors::StorageError>;
async fn find_user_scoped_dashboard_metadata(
&self,
user_id: &str,
merchant_id: &str,
org_id: &str,
data_keys: Vec<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
async fn find_merchant_scoped_dashboard_metadata(
&self,
merchant_id: &str,
org_id: &str,
data_keys: Vec<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError>;
}
#[async_trait::async_trait]
impl DashboardMetadataInterface for Store {
async fn insert_metadata(
&self,
metadata: storage::DashboardMetadataNew,
) -> CustomResult<storage::DashboardMetadata, errors::StorageError> {
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<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, 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<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, 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<storage::DashboardMetadata, errors::StorageError> {
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<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, 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::<Vec<storage::DashboardMetadata>>();
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<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, 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::<Vec<storage::DashboardMetadata>>();
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)
}
}

View File

@ -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<storage::DashboardMetadata, errors::StorageError> {
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<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, 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<enums::DashboardMetadata>,
) -> CustomResult<Vec<storage::DashboardMetadata>, errors::StorageError> {
self.diesel_store
.find_merchant_scoped_dashboard_metadata(merchant_id, org_id, data_keys)
.await
}
}

View File

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

View File

@ -147,9 +147,11 @@ impl From<Flow> for ApiIdentifier {
| Flow::GsmRuleUpdate
| Flow::GsmRuleDelete => Self::Gsm,
Flow::UserConnectAccount | Flow::ChangePassword | Flow::VerifyPaymentConnector => {
Self::User
}
Flow::UserConnectAccount
| Flow::ChangePassword
| Flow::SetDashboardMetadata
| Flow::GetMutltipleDashboardMetadata
| Flow::VerifyPaymentConnector => Self::User,
}
}
}

View File

@ -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;
@ -8,7 +9,9 @@ use crate::{
services::{
api,
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(
@ -47,3 +50,55 @@ pub async fn change_password(
))
.await
}
pub async fn set_merchant_scoped_dashboard_metadata(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_api::dashboard_metadata::SetMetaDataRequest>,
) -> 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::dashboard_metadata::set_metadata,
&auth::JWTAuth(Permission::MerchantAccountWrite),
api_locking::LockAction::NotApplicable,
))
.await
}
pub async fn get_multiple_dashboard_metadata(
state: web::Data<AppState>,
req: HttpRequest,
query: web::Query<user_api::dashboard_metadata::GetMultipleMetaDataRequest>,
) -> 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::dashboard_metadata::get_multiple_metadata,
&auth::DashboardNoPermissionAuth,
api_locking::LockAction::NotApplicable,
))
.await
}

View File

@ -27,6 +27,8 @@ use crate::{
utils::user::password,
};
pub mod dashboard_metadata;
#[derive(Clone)]
pub struct UserName(Secret<String>);

View File

@ -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<String, common_utils::pii::IpAddress>,
pub timestamp: PrimitiveDateTime,
}

View File

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

View File

@ -0,0 +1 @@
pub use diesel_models::user::dashboard_metadata::*;

View File

@ -1 +1,2 @@
pub mod dashboard_metadata;
pub mod password;

View File

@ -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<DashboardMetadata> {
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<DBEnum>,
) -> UserResult<Vec<DashboardMetadata>> {
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<T>(data: Option<&DashboardMetadata>) -> UserResult<Option<T>>
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<DBEnum>,
) -> (Vec<DBEnum>, Vec<DBEnum>) {
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<String, common_utils::pii::IpAddress> = 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<IpAddr, _> = 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<GetMultipleMetaDataPayload> {
Ok(GetMultipleMetaDataPayload {
results: query
.split(',')
.map(GetMetaDataRequest::from_str)
.collect::<Result<Vec<GetMetaDataRequest>, _>>()
.map_err(|_| UserErrors::InvalidMetadataRequest.into())
.attach_printable("Error Parsing to DashboardMetadata enums")?,
})
}

View File

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

View File

@ -43,6 +43,7 @@ pub struct MockDb {
pub organizations: Arc<Mutex<Vec<store::organization::Organization>>>,
pub users: Arc<Mutex<Vec<store::user::User>>>,
pub user_roles: Arc<Mutex<Vec<store::user_role::UserRole>>>,
pub dashboard_metadata: Arc<Mutex<Vec<store::user::dashboard_metadata::DashboardMetadata>>>,
}
impl MockDb {
@ -78,6 +79,7 @@ impl MockDb {
organizations: Default::default(),
users: Default::default(),
user_roles: Default::default(),
dashboard_metadata: Default::default(),
})
}
}

View File

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

View File

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