From ba392f58b2956d67e93a08853bcf2270a869be27 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 5 Dec 2023 12:47:37 +0530 Subject: [PATCH 1/6] fix: add fallback to reverselookup error (#3025) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 1 + crates/common_utils/src/lib.rs | 1 + crates/common_utils/src/macros.rs | 92 +++++++++++ crates/data_models/Cargo.toml | 1 + crates/data_models/src/errors.rs | 4 +- crates/diesel_models/src/errors.rs | 16 +- crates/router/src/db/refund.rs | 147 +++++++++--------- crates/router/src/db/user/sample_data.rs | 8 +- crates/router/src/lib.rs | 1 + crates/router/src/macros.rs | 72 +-------- crates/storage_impl/src/errors.rs | 10 +- crates/storage_impl/src/lib.rs | 10 +- .../src/payments/payment_attempt.rs | 125 +++++++++------ .../src/payments/payment_intent.rs | 27 ++-- 14 files changed, 288 insertions(+), 227 deletions(-) create mode 100644 crates/common_utils/src/macros.rs diff --git a/Cargo.lock b/Cargo.lock index 4231c62d9499..cb38c0b70b59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2052,6 +2052,7 @@ dependencies = [ "async-trait", "common_enums", "common_utils", + "diesel_models", "error-stack", "masking", "serde", diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 62428dccfb6a..0ac8e886bc06 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -10,6 +10,7 @@ pub mod errors; pub mod events; pub mod ext_traits; pub mod fp_utils; +pub mod macros; pub mod pii; #[allow(missing_docs)] // Todo: add docs pub mod request; diff --git a/crates/common_utils/src/macros.rs b/crates/common_utils/src/macros.rs new file mode 100644 index 000000000000..9d41569384f1 --- /dev/null +++ b/crates/common_utils/src/macros.rs @@ -0,0 +1,92 @@ +#![allow(missing_docs)] + +#[macro_export] +macro_rules! newtype_impl { + ($is_pub:vis, $name:ident, $ty_path:path) => { + impl std::ops::Deref for $name { + type Target = $ty_path; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl std::ops::DerefMut for $name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + impl From<$ty_path> for $name { + fn from(ty: $ty_path) -> Self { + Self(ty) + } + } + + impl $name { + pub fn into_inner(self) -> $ty_path { + self.0 + } + } + }; +} + +#[macro_export] +macro_rules! newtype { + ($is_pub:vis $name:ident = $ty_path:path) => { + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; + + ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { + #[derive($($trt),*)] + $is_pub struct $name(pub $ty_path); + + $crate::newtype_impl!($is_pub, $name, $ty_path); + }; +} + +#[macro_export] +macro_rules! async_spawn { + ($t:block) => { + tokio::spawn(async move { $t }); + }; +} + +#[macro_export] +macro_rules! fallback_reverse_lookup_not_found { + ($a:expr,$b:expr) => { + match $a { + Ok(res) => res, + Err(err) => { + router_env::logger::error!(reverse_lookup_fallback = %err); + match err.current_context() { + errors::StorageError::ValueNotFound(_) => return $b, + errors::StorageError::DatabaseError(data_err) => { + match data_err.current_context() { + diesel_models::errors::DatabaseError::NotFound => return $b, + _ => return Err(err) + } + } + _=> return Err(err) + } + } + }; + }; +} + +#[macro_export] +macro_rules! collect_missing_value_keys { + [$(($key:literal, $option:expr)),+] => { + { + let mut keys: Vec<&'static str> = Vec::new(); + $( + if $option.is_none() { + keys.push($key); + } + )* + keys + } + }; +} diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 857d53b6999e..a86dc3070b4d 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -17,6 +17,7 @@ api_models = { version = "0.1.0", path = "../api_models" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } masking = { version = "0.1.0", path = "../masking" } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } # Third party deps async-trait = "0.1.68" diff --git a/crates/data_models/src/errors.rs b/crates/data_models/src/errors.rs index 4f8229ea0c9b..9616a3a944ca 100644 --- a/crates/data_models/src/errors.rs +++ b/crates/data_models/src/errors.rs @@ -1,3 +1,5 @@ +use diesel_models::errors::DatabaseError; + pub type StorageResult = error_stack::Result; #[derive(Debug, thiserror::Error)] @@ -6,7 +8,7 @@ pub enum StorageError { InitializationError, // TODO: deprecate this error type to use a domain error instead #[error("DatabaseError: {0:?}")] - DatabaseError(String), + DatabaseError(error_stack::Report), #[error("ValueNotFound: {0}")] ValueNotFound(String), #[error("DuplicateValue: {entity} already exists {key:?}")] diff --git a/crates/diesel_models/src/errors.rs b/crates/diesel_models/src/errors.rs index 0a8422131ae2..4a536aad07e4 100644 --- a/crates/diesel_models/src/errors.rs +++ b/crates/diesel_models/src/errors.rs @@ -1,4 +1,4 @@ -#[derive(Debug, thiserror::Error)] +#[derive(Copy, Clone, Debug, thiserror::Error)] pub enum DatabaseError { #[error("An error occurred when obtaining database connection")] DatabaseConnectionError, @@ -14,3 +14,17 @@ pub enum DatabaseError { #[error("An unknown error occurred")] Others, } + +impl From for DatabaseError { + fn from(error: diesel::result::Error) -> Self { + match error { + diesel::result::Error::DatabaseError( + diesel::result::DatabaseErrorKind::UniqueViolation, + _, + ) => Self::UniqueViolation, + diesel::result::Error::NotFound => Self::NotFound, + diesel::result::Error::QueryBuilderError(_) => Self::QueryGenerationFailed, + _ => Self::Others, + } + } +} diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 8ac8bd106eff..f385e1bc5a83 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -267,7 +267,7 @@ mod storage { #[cfg(feature = "kv_store")] mod storage { - use common_utils::date_time; + use common_utils::{date_time, fallback_reverse_lookup_not_found}; use error_stack::{IntoReport, ResultExt}; use redis_interface::HsetnxReply; use storage_impl::redis::kv_store::{kv_wrapper, KvOperation}; @@ -277,7 +277,6 @@ mod storage { connection, core::errors::{self, CustomResult}, db::reverse_lookup::ReverseLookupInterface, - logger, services::Store, types::storage::{self as storage_types, enums, kv}, utils::{self, db_utils}, @@ -304,10 +303,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{internal_reference_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_inter_ref_{merchant_id}_{internal_reference_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -382,6 +383,50 @@ mod storage { }, }; + let mut reverse_lookups = vec![ + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_ref_id_{}_{}", + created_refund.merchant_id, created_refund.refund_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + // [#492]: A discussion is required on whether this is required? + storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_inter_ref_{}_{}", + created_refund.merchant_id, created_refund.internal_reference_id + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }, + ]; + if let Some(connector_refund_id) = created_refund.to_owned().connector_refund_id + { + reverse_lookups.push(storage_types::ReverseLookupNew { + sk_id: field.clone(), + lookup_id: format!( + "ref_connector_{}_{}_{}", + created_refund.merchant_id, + connector_refund_id, + created_refund.connector + ), + pk_id: key.clone(), + source: "refund".to_string(), + updated_by: storage_scheme.to_string(), + }) + }; + let rev_look = reverse_lookups + .into_iter() + .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); + + futures::future::try_join_all(rev_look).await?; + match kv_wrapper::( self, KvOperation::::HSetNx( @@ -400,55 +445,7 @@ mod storage { key: Some(created_refund.refund_id), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - let mut reverse_lookups = vec![ - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, created_refund.refund_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - // [#492]: A discussion is required on whether this is required? - storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}", - created_refund.merchant_id, - created_refund.internal_reference_id - ), - pk_id: key.clone(), - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }, - ]; - if let Some(connector_refund_id) = - created_refund.to_owned().connector_refund_id - { - reverse_lookups.push(storage_types::ReverseLookupNew { - sk_id: field.clone(), - lookup_id: format!( - "{}_{}_{}", - created_refund.merchant_id, - connector_refund_id, - created_refund.connector - ), - pk_id: key, - source: "refund".to_string(), - updated_by: storage_scheme.to_string(), - }) - }; - let rev_look = reverse_lookups - .into_iter() - .map(|rev| self.insert_reverse_lookup(rev, storage_scheme)); - - futures::future::try_join_all(rev_look).await?; - - Ok(created_refund) - } + Ok(HsetnxReply::KeySet) => Ok(created_refund), Err(er) => Err(er).change_context(errors::StorageError::KVError), } } @@ -475,17 +472,14 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_transaction_id}"); - let lookup = match self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await - { - Ok(l) => l, - Err(err) => { - logger::error!(?err); - return Ok(vec![]); - } - }; + let lookup_id = + format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); + let key = &lookup.pk_id; let pattern = db_utils::generate_hscan_pattern_for_refund(&lookup.sk_id); @@ -575,10 +569,12 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{refund_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("ref_ref_id_{merchant_id}_{refund_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( @@ -620,10 +616,13 @@ mod storage { match storage_scheme { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_refund_id}_{connector}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = + format!("ref_connector_{merchant_id}_{connector_refund_id}_{connector}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + database_call().await + ); let key = &lookup.pk_id; Box::pin(db_utils::try_redis_get_else_try_database_get( diff --git a/crates/router/src/db/user/sample_data.rs b/crates/router/src/db/user/sample_data.rs index 11def9026854..ae98332cfc49 100644 --- a/crates/router/src/db/user/sample_data.rs +++ b/crates/router/src/db/user/sample_data.rs @@ -193,13 +193,7 @@ fn diesel_error_to_data_error(diesel_error: Report) -> Report { - StorageError::DatabaseError("No fields to update".to_string()) - } - DatabaseError::QueryGenerationFailed => { - StorageError::DatabaseError("Query generation failed".to_string()) - } - DatabaseError::Others => StorageError::DatabaseError("Others".to_string()), + err => StorageError::DatabaseError(error_stack::report!(*err)), }; diesel_error.change_context(new_err) } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 035314f71dfb..fb8be9636748 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -35,6 +35,7 @@ use storage_impl::errors::ApplicationResult; use tokio::sync::{mpsc, oneshot}; pub use self::env::logger; +pub(crate) use self::macros::*; use crate::{configs::settings, core::errors}; #[cfg(feature = "mimalloc")] diff --git a/crates/router/src/macros.rs b/crates/router/src/macros.rs index 33ed43fcc7ab..e6c9dba7d6e2 100644 --- a/crates/router/src/macros.rs +++ b/crates/router/src/macros.rs @@ -1,68 +1,4 @@ -#[macro_export] -macro_rules! newtype_impl { - ($is_pub:vis, $name:ident, $ty_path:path) => { - impl std::ops::Deref for $name { - type Target = $ty_path; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl std::ops::DerefMut for $name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - impl From<$ty_path> for $name { - fn from(ty: $ty_path) -> Self { - Self(ty) - } - } - - impl $name { - pub fn into_inner(self) -> $ty_path { - self.0 - } - } - }; -} - -#[macro_export] -macro_rules! newtype { - ($is_pub:vis $name:ident = $ty_path:path) => { - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; - - ($is_pub:vis $name:ident = $ty_path:path, derives = ($($trt:path),*)) => { - #[derive($($trt),*)] - $is_pub struct $name(pub $ty_path); - - $crate::newtype_impl!($is_pub, $name, $ty_path); - }; -} - -#[macro_export] -macro_rules! async_spawn { - ($t:block) => { - tokio::spawn(async move { $t }); - }; -} - -#[macro_export] -macro_rules! collect_missing_value_keys { - [$(($key:literal, $option:expr)),+] => { - { - let mut keys: Vec<&'static str> = Vec::new(); - $( - if $option.is_none() { - keys.push($key); - } - )* - keys - } - }; -} +pub use common_utils::{ + async_spawn, collect_missing_value_keys, fallback_reverse_lookup_not_found, newtype, + newtype_impl, +}; diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index bc68986cb8ea..105a93d4beae 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -92,15 +92,7 @@ impl Into for &StorageError { key: None, } } - storage_errors::DatabaseError::NoFieldsToUpdate => { - DataStorageError::DatabaseError("No fields to update".to_string()) - } - storage_errors::DatabaseError::QueryGenerationFailed => { - DataStorageError::DatabaseError("Query generation failed".to_string()) - } - storage_errors::DatabaseError::Others => { - DataStorageError::DatabaseError("Unknown database error".to_string()) - } + err => DataStorageError::DatabaseError(error_stack::report!(*err)), }, StorageError::ValueNotFound(i) => DataStorageError::ValueNotFound(i.clone()), StorageError::DuplicateValue { entity, key } => DataStorageError::DuplicateValue { diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index dc0dea4bb59c..7e2c7f2fc3c5 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -251,14 +251,6 @@ pub(crate) fn diesel_error_to_data_error( entity: "entity ", key: None, }, - diesel_models::errors::DatabaseError::NoFieldsToUpdate => { - StorageError::DatabaseError("No fields to update".to_string()) - } - diesel_models::errors::DatabaseError::QueryGenerationFailed => { - StorageError::DatabaseError("Query generation failed".to_string()) - } - diesel_models::errors::DatabaseError::Others => { - StorageError::DatabaseError("Others".to_string()) - } + _ => StorageError::DatabaseError(error_stack::report!(*diesel_error)), } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 9f351979f289..b524ff1aaa71 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1,5 +1,5 @@ use api_models::enums::{AuthenticationType, Connector, PaymentMethod, PaymentMethodType}; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, fallback_reverse_lookup_not_found}; use data_models::{ errors, mandates::{MandateAmountData, MandateDataType}, @@ -399,6 +399,20 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; + //Reverse lookup for attempt_id + let reverse_lookup = ReverseLookupNew { + lookup_id: format!( + "pa_{}_{}", + &created_attempt.merchant_id, &created_attempt.attempt_id, + ), + pk_id: key.clone(), + sk_id: field.clone(), + source: "payment_attempt".to_string(), + updated_by: storage_scheme.to_string(), + }; + self.insert_reverse_lookup(reverse_lookup, storage_scheme) + .await?; + match kv_wrapper::( self, KvOperation::HSetNx( @@ -417,23 +431,7 @@ impl PaymentAttemptInterface for KVRouterStore { key: Some(key), }) .into_report(), - Ok(HsetnxReply::KeySet) => { - //Reverse lookup for attempt_id - let reverse_lookup = ReverseLookupNew { - lookup_id: format!( - "{}_{}", - &created_attempt.merchant_id, &created_attempt.attempt_id, - ), - pk_id: key, - sk_id: field, - source: "payment_attempt".to_string(), - updated_by: storage_scheme.to_string(), - }; - self.insert_reverse_lookup(reverse_lookup, storage_scheme) - .await?; - - Ok(created_attempt) - } + Ok(HsetnxReply::KeySet) => Ok(created_attempt), Err(error) => Err(error.change_context(errors::StorageError::KVError)), } } @@ -480,16 +478,6 @@ impl PaymentAttemptInterface for KVRouterStore { }, }; - kv_wrapper::<(), _, _>( - self, - KvOperation::Hset::((&field, redis_value), redis_entry), - &key, - ) - .await - .change_context(errors::StorageError::KVError)? - .try_into_hset() - .change_context(errors::StorageError::KVError)?; - match ( old_connector_transaction_id, &updated_attempt.connector_transaction_id, @@ -549,6 +537,16 @@ impl PaymentAttemptInterface for KVRouterStore { (_, _) => {} } + kv_wrapper::<(), _, _>( + self, + KvOperation::Hset::((&field, redis_value), redis_entry), + &key, + ) + .await + .change_context(errors::StorageError::KVError)? + .try_into_hset() + .change_context(errors::StorageError::KVError)?; + Ok(updated_attempt) } } @@ -574,10 +572,20 @@ impl PaymentAttemptInterface for KVRouterStore { } MerchantStorageScheme::RedisKv => { // We assume that PaymentAttempt <=> PaymentIntent is a one-to-one relation for now - let lookup_id = format!("conn_trans_{merchant_id}_{connector_transaction_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_transaction_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id( + connector_transaction_id, + payment_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -707,10 +715,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{connector_txn_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_conn_trans_{merchant_id}_{connector_txn_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_merchant_id_connector_txn_id( + merchant_id, + connector_txn_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -799,10 +815,19 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("{merchant_id}_{attempt_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_{merchant_id}_{attempt_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_attempt_id_merchant_id( + attempt_id, + merchant_id, + storage_scheme, + ) + .await + ); + let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( async { @@ -846,10 +871,18 @@ impl PaymentAttemptInterface for KVRouterStore { .await } MerchantStorageScheme::RedisKv => { - let lookup_id = format!("preprocessing_{merchant_id}_{preprocessing_id}"); - let lookup = self - .get_lookup_by_lookup_id(&lookup_id, storage_scheme) - .await?; + let lookup_id = format!("pa_preprocessing_{merchant_id}_{preprocessing_id}"); + let lookup = fallback_reverse_lookup_not_found!( + self.get_lookup_by_lookup_id(&lookup_id, storage_scheme) + .await, + self.router_store + .find_payment_attempt_by_preprocessing_id_merchant_id( + preprocessing_id, + merchant_id, + storage_scheme, + ) + .await + ); let key = &lookup.pk_id; Box::pin(try_redis_get_else_try_database_get( @@ -1757,7 +1790,7 @@ async fn add_connector_txn_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("conn_trans_{}_{}", merchant_id, connector_transaction_id), + lookup_id: format!("pa_conn_trans_{}_{}", merchant_id, connector_transaction_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), @@ -1779,7 +1812,7 @@ async fn add_preprocessing_id_to_reverse_lookup( ) -> CustomResult { let field = format!("pa_{}", updated_attempt_attempt_id); let reverse_lookup_new = ReverseLookupNew { - lookup_id: format!("preprocessing_{}_{}", merchant_id, preprocessing_id), + lookup_id: format!("pa_preprocessing_{}_{}", merchant_id, preprocessing_id), pk_id: key.to_owned(), sk_id: field.clone(), source: "payment_attempt".to_string(), diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 90bb21190c39..3e695947b8bf 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -494,12 +494,13 @@ impl PaymentIntentInterface for crate::RouterStore { .map(PaymentIntent::from_storage_model) .collect::>() }) - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } #[cfg(feature = "olap")] @@ -646,12 +647,13 @@ impl PaymentIntentInterface for crate::RouterStore { }) .collect() }) - .into_report() .map_err(|er| { - let new_er = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_er) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable("Error filtering payment records") + .into_report() } #[cfg(feature = "olap")] @@ -712,12 +714,13 @@ impl PaymentIntentInterface for crate::RouterStore { db_metrics::DatabaseOperation::Filter, ) .await - .into_report() .map_err(|er| { - let new_err = StorageError::DatabaseError(format!("{er:?}")); - er.change_context(new_err) + StorageError::DatabaseError( + error_stack::report!(diesel_models::errors::DatabaseError::from(er)) + .attach_printable("Error filtering payment records"), + ) }) - .attach_printable_lazy(|| "Error filtering records by predicate") + .into_report() } } From 6e09bc9e2c4bbe14dcb70da4a438850b03b3254c Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 07:48:22 +0000 Subject: [PATCH 2/6] test(postman): update postman collection files --- postman/collection-json/adyen_uk.postman_collection.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/postman/collection-json/adyen_uk.postman_collection.json b/postman/collection-json/adyen_uk.postman_collection.json index 400f04241c27..04a7e39f15e7 100644 --- a/postman/collection-json/adyen_uk.postman_collection.json +++ b/postman/collection-json/adyen_uk.postman_collection.json @@ -13959,10 +13959,10 @@ "// Response body should have value \"invalid_request\" for \"error type\"", "if (jsonData?.error?.message) {", " pm.test(", - " \"[POST]::/payments - Content check if value for 'error.message' matches 'The payment has not succeeded yet. Please pass a successful payment to initiate refund'\",", + " \"[POST]::/payments - Content check if value for 'error.message' matches 'This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"The payment has not succeeded yet. Please pass a successful payment to initiate refund\",", + " \"This Payment could not be refund because it has a status of requires_confirmation. The expected state is succeeded, partially_captured\",", " );", " },", " );", From 5b62731399c8d5b8bfd923fb61706b3a9c4f5ffe Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 5 Dec 2023 07:48:22 +0000 Subject: [PATCH 3/6] chore(version): v1.95.0 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bbdac921fd7..3cd968293c4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,45 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.95.0 (2023-12-05) + +### Features + +- **connector:** [BOA/CYBERSOURCE] Fix Status Mapping for Terminal St… ([#3031](https://github.com/juspay/hyperswitch/pull/3031)) ([`95876b0`](https://github.com/juspay/hyperswitch/commit/95876b0ce03e024edf77909502c53eb4e63a9855)) +- **pm_list:** Add required field for open_banking_uk for Adyen and Volt Connector ([#3032](https://github.com/juspay/hyperswitch/pull/3032)) ([`9d93533`](https://github.com/juspay/hyperswitch/commit/9d935332193dcc9f191a0a5a9e7405316794a418)) +- **router:** + - Add key_value to locker metrics ([#2995](https://github.com/juspay/hyperswitch/pull/2995)) ([`83fcd1a`](https://github.com/juspay/hyperswitch/commit/83fcd1a9deb106a44c8262923c7f1660b0c46bf2)) + - Add payments incremental authorization api ([#3038](https://github.com/juspay/hyperswitch/pull/3038)) ([`a0cfdd3`](https://github.com/juspay/hyperswitch/commit/a0cfdd3fb12f04b603f65551eac985c31e08da85)) +- **types:** Add email types for sending emails ([#3020](https://github.com/juspay/hyperswitch/pull/3020)) ([`c4bd47e`](https://github.com/juspay/hyperswitch/commit/c4bd47eca93a158c9daeeeb18afb1e735eea8c94)) +- **user:** + - Generate and delete sample data ([#2987](https://github.com/juspay/hyperswitch/pull/2987)) ([`092ec73`](https://github.com/juspay/hyperswitch/commit/092ec73b3c65ce6048d379383b078d643f0f35fc)) + - Add user_list and switch_list apis ([#3033](https://github.com/juspay/hyperswitch/pull/3033)) ([`ec15ddd`](https://github.com/juspay/hyperswitch/commit/ec15ddd0d0ed942fedec525406df3005d494b8d4)) +- Calculate surcharge for customer saved card list ([#3039](https://github.com/juspay/hyperswitch/pull/3039)) ([`daf0f09`](https://github.com/juspay/hyperswitch/commit/daf0f09f8e3293ee6a3599a25362d9171fc5b2e7)) + +### Bug Fixes + +- **connector:** [Paypal] Parse response for Cards with no 3DS check ([#3021](https://github.com/juspay/hyperswitch/pull/3021)) ([`d883cd1`](https://github.com/juspay/hyperswitch/commit/d883cd18972c5f9e8350e9a3f4e5cd56ec2c0787)) +- **pm_list:** [Trustpay]Update dynamic fields for trustpay blik ([#3042](https://github.com/juspay/hyperswitch/pull/3042)) ([`9274cef`](https://github.com/juspay/hyperswitch/commit/9274cefbdd29d2ac64baeea2fe504dff2472cb47)) +- **wasm:** Fix wasm function to return the categories for keys with their description respectively ([#3023](https://github.com/juspay/hyperswitch/pull/3023)) ([`2ac5b2c`](https://github.com/juspay/hyperswitch/commit/2ac5b2cd764c0aad53ac7c672dfcc9132fa5668f)) +- Use card bin to get additional card details ([#3036](https://github.com/juspay/hyperswitch/pull/3036)) ([`6c7d3a2`](https://github.com/juspay/hyperswitch/commit/6c7d3a2e8a047ff23b52b76792fe8f28d3b952a4)) +- Transform connector name to lowercase in connector integration script ([#3048](https://github.com/juspay/hyperswitch/pull/3048)) ([`298e362`](https://github.com/juspay/hyperswitch/commit/298e3627c379de5acfcafb074036754661801f1e)) +- Add fallback to reverselookup error ([#3025](https://github.com/juspay/hyperswitch/pull/3025)) ([`ba392f5`](https://github.com/juspay/hyperswitch/commit/ba392f58b2956d67e93a08853bcf2270a869be27)) + +### Refactors + +- **payment_methods:** Add support for passing card_cvc in payment_method_data object along with token ([#3024](https://github.com/juspay/hyperswitch/pull/3024)) ([`3ce04ab`](https://github.com/juspay/hyperswitch/commit/3ce04abae4eddfa27025368f5ef28987cccea43d)) +- **users:** Separate signup and signin ([#2921](https://github.com/juspay/hyperswitch/pull/2921)) ([`80efeb7`](https://github.com/juspay/hyperswitch/commit/80efeb76b1801529766978af1c06e2d2c7de66c0)) +- Create separate struct for surcharge details response ([#3027](https://github.com/juspay/hyperswitch/pull/3027)) ([`57591f8`](https://github.com/juspay/hyperswitch/commit/57591f819c7994099e76cff1affc7bcf3e45a031)) + +### Testing + +- **postman:** Update postman collection files ([`6e09bc9`](https://github.com/juspay/hyperswitch/commit/6e09bc9e2c4bbe14dcb70da4a438850b03b3254c)) + +**Full Changelog:** [`v1.94.0...v1.95.0`](https://github.com/juspay/hyperswitch/compare/v1.94.0...v1.95.0) + +- - - + + ## 1.94.0 (2023-12-01) ### Features From 1c3d260dc3e18fbf6cbd5122122a6c73dceb39a3 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:10:17 +0530 Subject: [PATCH 4/6] feat(user): add email apis and new enums for metadata (#3053) Co-authored-by: Rachit Naithani Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Rachit Naithani <81706961+racnan@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 9 +- crates/api_models/src/refunds.rs | 2 +- crates/api_models/src/user.rs | 23 +++ .../api_models/src/user/dashboard_metadata.rs | 41 +++- crates/diesel_models/src/enums.rs | 3 + .../src/query/dashboard_metadata.rs | 45 +++-- crates/router/src/core/errors/user.rs | 13 ++ crates/router/src/core/user.rs | 181 +++++++++++++++++- .../src/core/user/dashboard_metadata.rs | 136 ++++++++++++- crates/router/src/db/refund.rs | 4 +- crates/router/src/routes/app.rs | 3 + crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/user.rs | 57 ++++++ crates/router/src/services/email/types.rs | 4 + crates/router/src/types/domain/user.rs | 42 ++++ .../types/domain/user/dashboard_metadata.rs | 6 + crates/router/src/types/storage/refund.rs | 75 ++++++-- .../src/utils/user/dashboard_metadata.rs | 129 ++++++++++++- crates/router_env/src/logger/types.rs | 6 + 19 files changed, 728 insertions(+), 54 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 3634b51e0cc0..ca2932725317 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -7,7 +7,8 @@ use crate::user::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, - DashboardEntryResponse, GetUsersResponse, SignUpRequest, SignUpWithMerchantIdRequest, + DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest, + InviteUserResponse, ResetPasswordRequest, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UserMerchantCreate, }; @@ -33,7 +34,11 @@ common_utils::impl_misc_api_event_type!( UserMerchantCreate, GetUsersResponse, AuthorizeResponse, - ConnectAccountRequest + ConnectAccountRequest, + ForgotPasswordRequest, + ResetPasswordRequest, + InviteUserRequest, + InviteUserResponse ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 6fe8be8b5291..e89de9c58934 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -174,7 +174,7 @@ pub struct RefundListMetaData { pub currency: Vec, /// The list of available refund status filters #[schema(value_type = Vec)] - pub status: Vec, + pub refund_status: Vec, } /// The status for refunds diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 287c377eb46a..e5f06fdbfae3 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -65,6 +65,29 @@ pub struct ChangePasswordRequest { pub old_password: Secret, } +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ForgotPasswordRequest { + pub email: pii::Email, +} + +#[derive(serde::Deserialize, Debug, serde::Serialize)] +pub struct ResetPasswordRequest { + pub token: Secret, + pub password: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct InviteUserRequest { + pub email: pii::Email, + pub name: Secret, + pub role_id: String, +} + +#[derive(Debug, serde::Serialize)] +pub struct InviteUserResponse { + pub is_email_sent: bool, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct SwitchMerchantIdRequest { pub merchant_id: String, diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs index 04cda3bd7075..11588bbfbafe 100644 --- a/crates/api_models/src/user/dashboard_metadata.rs +++ b/crates/api_models/src/user/dashboard_metadata.rs @@ -1,3 +1,5 @@ +use common_enums::CountryAlpha2; +use common_utils::pii; use masking::Secret; use strum::EnumString; @@ -12,8 +14,11 @@ pub enum SetMetaDataRequest { ConfiguredRouting(ConfiguredRouting), TestPayment(TestPayment), IntegrationMethod(IntegrationMethod), + ConfigurationType(ConfigurationType), IntegrationCompleted, SPRoutingConfigured(ConfiguredRouting), + Feedback(Feedback), + ProdIntent(ProdIntent), SPTestPayment, DownloadWoocom, ConfigureWoocom, @@ -49,10 +54,38 @@ pub struct TestPayment { pub payment_id: String, } -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] pub struct IntegrationMethod { pub integration_type: String, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub enum ConfigurationType { + Single, + Multiple, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct Feedback { + pub email: pii::Email, + pub description: Option, + pub rating: Option, + pub category: Option, +} +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct ProdIntent { + pub legal_business_name: Option, + pub business_label: Option, + pub business_location: Option, + pub display_name: Option, + pub poc_email: Option, + pub business_type: Option, + pub business_identifier: Option, + pub business_website: Option, + pub poc_name: Option, + pub poc_contact: Option, + pub comments: Option, + pub is_completed: bool, +} #[derive(Debug, serde::Deserialize, EnumString, serde::Serialize)] pub enum GetMetaDataRequest { @@ -65,10 +98,13 @@ pub enum GetMetaDataRequest { ConfiguredRouting, TestPayment, IntegrationMethod, + ConfigurationType, IntegrationCompleted, StripeConnected, PaypalConnected, SPRoutingConfigured, + Feedback, + ProdIntent, SPTestPayment, DownloadWoocom, ConfigureWoocom, @@ -98,10 +134,13 @@ pub enum GetMetaDataResponse { ConfiguredRouting(Option), TestPayment(Option), IntegrationMethod(Option), + ConfigurationType(Option), IntegrationCompleted(bool), StripeConnected(Option), PaypalConnected(Option), SPRoutingConfigured(Option), + Feedback(Option), + ProdIntent(Option), SPTestPayment(bool), DownloadWoocom(bool), ConfigureWoocom(bool), diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 3f8b37cd03f7..17837d2ce5c7 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -452,10 +452,13 @@ pub enum DashboardMetadata { ConfiguredRouting, TestPayment, IntegrationMethod, + ConfigurationType, IntegrationCompleted, StripeConnected, PaypalConnected, SpRoutingConfigured, + Feedback, + ProdIntent, SpTestPayment, DownloadWoocom, ConfigureWoocom, diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs index 44fd24c7acf2..678bcc2fd1f6 100644 --- a/crates/diesel_models/src/query/dashboard_metadata.rs +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -28,21 +28,36 @@ impl DashboardMetadata { data_key: enums::DashboardMetadata, dashboard_metadata_update: DashboardMetadataUpdate, ) -> StorageResult { - generics::generic_update_with_unique_predicate_get_result::< - ::Table, - _, - _, - _, - >( - conn, - dsl::user_id - .eq(user_id.to_owned()) - .and(dsl::merchant_id.eq(merchant_id.to_owned())) - .and(dsl::org_id.eq(org_id.to_owned())) - .and(dsl::data_key.eq(data_key.to_owned())), - DashboardMetadataUpdateInternal::from(dashboard_metadata_update), - ) - .await + let predicate = dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::org_id.eq(org_id.to_owned())) + .and(dsl::data_key.eq(data_key.to_owned())); + + if let Some(uid) = user_id { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + predicate.and(dsl::user_id.eq(uid)), + DashboardMetadataUpdateInternal::from(dashboard_metadata_update), + ) + .await + } else { + generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + predicate.and(dsl::user_id.is_null()), + DashboardMetadataUpdateInternal::from(dashboard_metadata_update), + ) + .await + } } pub async fn find_user_scoped_dashboard_metadata( diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 5e580b003408..9a5308852229 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -12,8 +12,12 @@ pub enum UserErrors { InternalServerError, #[error("InvalidCredentials")] InvalidCredentials, + #[error("UserNotFound")] + UserNotFound, #[error("UserExists")] UserExists, + #[error("LinkInvalid")] + LinkInvalid, #[error("InvalidOldPassword")] InvalidOldPassword, #[error("EmailParsingError")] @@ -60,12 +64,21 @@ impl common_utils::errors::ErrorSwitch AER::Unauthorized(ApiError::new( + sub_code, + 2, + "Email doesn’t exist. Register", + None, + )), Self::UserExists => AER::BadRequest(ApiError::new( sub_code, 3, "An account already exists with this email", None, )), + Self::LinkInvalid => { + AER::Unauthorized(ApiError::new(sub_code, 4, "Invalid or expired link", None)) + } Self::InvalidOldPassword => AER::BadRequest(ApiError::new( sub_code, 6, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index c868530f81af..01947d08d1f9 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -11,7 +11,7 @@ use router_env::logger; use super::errors::{UserErrors, UserResponse}; #[cfg(feature = "email")] -use crate::services::email::types as email_types; +use crate::services::email::{types as email_types, types::EmailToken}; use crate::{ consts, db::user::UserInterface, @@ -235,8 +235,7 @@ pub async fn change_password( user.compare_password(request.old_password) .change_context(UserErrors::InvalidOldPassword)?; - let new_password_hash = - crate::utils::user::password::generate_password_hash(request.new_password)?; + let new_password_hash = utils::user::password::generate_password_hash(request.new_password)?; let _ = UserInterface::update_user_by_user_id( &*state.store, @@ -253,6 +252,182 @@ pub async fn change_password( Ok(ApplicationResponse::StatusOk) } +#[cfg(feature = "email")] +pub async fn forgot_password( + state: AppState, + request: user_api::ForgotPasswordRequest, +) -> UserResponse<()> { + let user_email = domain::UserEmail::from_pii_email(request.email)?; + + let user_from_db = state + .store + .find_user_by_email(user_email.get_secret().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::UserNotFound) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .map(domain::UserFromStorage::from)?; + + let email_contents = email_types::ResetPassword { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + user_name: domain::UserName::new(user_from_db.get_name())?, + subject: "Get back to Hyperswitch - Reset Your Password Now", + }; + + state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await + .map_err(|e| e.change_context(UserErrors::InternalServerError))?; + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "email")] +pub async fn reset_password( + state: AppState, + request: user_api::ResetPasswordRequest, +) -> UserResponse<()> { + let token = auth::decode_jwt::(request.token.expose().as_str(), &state) + .await + .change_context(UserErrors::LinkInvalid)?; + + let password = domain::UserPassword::new(request.password)?; + + let hash_password = utils::user::password::generate_password_hash(password.get_secret())?; + + //TODO: Create Update by email query + let user_id = state + .store + .find_user_by_email(token.get_email()) + .await + .change_context(UserErrors::InternalServerError)? + .user_id; + + state + .store + .update_user_by_user_id( + user_id.as_str(), + storage_user::UserUpdate::AccountUpdate { + name: None, + password: Some(hash_password), + is_verified: Some(true), + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + //TODO: Update User role status for invited user + + Ok(ApplicationResponse::StatusOk) +} + +#[cfg(feature = "email")] +pub async fn invite_user( + state: AppState, + request: user_api::InviteUserRequest, + user_from_token: auth::UserFromToken, +) -> UserResponse { + let inviter_user = state + .store + .find_user_by_id(user_from_token.user_id.as_str()) + .await + .change_context(UserErrors::InternalServerError)?; + + if inviter_user.email == request.email { + return Err(UserErrors::InvalidRoleOperation.into()) + .attach_printable("User Inviting themself"); + } + + utils::user_role::validate_role_id(request.role_id.as_str())?; + let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?; + + let invitee_user = state + .store + .find_user_by_email(invitee_email.clone().get_secret().expose().as_str()) + .await; + + if let Ok(invitee_user) = invitee_user { + let invitee_user_from_db = domain::UserFromStorage::from(invitee_user); + + let now = common_utils::date_time::now(); + use diesel_models::user_role::UserRoleNew; + state + .store + .insert_user_role(UserRoleNew { + user_id: invitee_user_from_db.get_user_id().to_owned(), + merchant_id: user_from_token.merchant_id, + role_id: request.role_id, + org_id: user_from_token.org_id, + status: UserStatus::Active, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id, + created_at: now, + last_modified_at: now, + }) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::UserExists) + } else { + e.change_context(UserErrors::InternalServerError) + } + })?; + + Ok(ApplicationResponse::Json(user_api::InviteUserResponse { + is_email_sent: false, + })) + } else if invitee_user + .as_ref() + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + let new_user = domain::NewUser::try_from((request.clone(), user_from_token))?; + + new_user + .insert_user_in_db(state.store.as_ref()) + .await + .change_context(UserErrors::InternalServerError)?; + new_user + .clone() + .insert_user_role_in_db(state.clone(), request.role_id, UserStatus::InvitationSent) + .await + .change_context(UserErrors::InternalServerError)?; + + let email_contents = email_types::InviteUser { + recipient_email: invitee_email, + user_name: domain::UserName::new(new_user.get_name())?, + settings: state.conf.clone(), + subject: "You have been invited to join Hyperswitch Community!", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; + + logger::info!(?send_email_result); + + Ok(ApplicationResponse::Json(user_api::InviteUserResponse { + is_email_sent: send_email_result.is_ok(), + })) + } else { + Err(UserErrors::InternalServerError.into()) + } +} + pub async fn create_internal_user( state: AppState, request: user_api::CreateInternalUserRequest, diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs index de385fb8ed65..b537aa3ec732 100644 --- a/crates/router/src/core/user/dashboard_metadata.rs +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -81,12 +81,17 @@ fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult { Ok(types::MetaData::IntegrationMethod(req)) } + api::SetMetaDataRequest::ConfigurationType(req) => { + Ok(types::MetaData::ConfigurationType(req)) + } api::SetMetaDataRequest::IntegrationCompleted => { Ok(types::MetaData::IntegrationCompleted(true)) } api::SetMetaDataRequest::SPRoutingConfigured(req) => { Ok(types::MetaData::SPRoutingConfigured(req)) } + api::SetMetaDataRequest::Feedback(req) => Ok(types::MetaData::Feedback(req)), + api::SetMetaDataRequest::ProdIntent(req) => Ok(types::MetaData::ProdIntent(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)), @@ -110,10 +115,13 @@ fn parse_get_request(data_enum: api::GetMetaDataRequest) -> DBEnum { api::GetMetaDataRequest::ConfiguredRouting => DBEnum::ConfiguredRouting, api::GetMetaDataRequest::TestPayment => DBEnum::TestPayment, api::GetMetaDataRequest::IntegrationMethod => DBEnum::IntegrationMethod, + api::GetMetaDataRequest::ConfigurationType => DBEnum::ConfigurationType, api::GetMetaDataRequest::IntegrationCompleted => DBEnum::IntegrationCompleted, api::GetMetaDataRequest::StripeConnected => DBEnum::StripeConnected, api::GetMetaDataRequest::PaypalConnected => DBEnum::PaypalConnected, api::GetMetaDataRequest::SPRoutingConfigured => DBEnum::SpRoutingConfigured, + api::GetMetaDataRequest::Feedback => DBEnum::Feedback, + api::GetMetaDataRequest::ProdIntent => DBEnum::ProdIntent, api::GetMetaDataRequest::SPTestPayment => DBEnum::SpTestPayment, api::GetMetaDataRequest::DownloadWoocom => DBEnum::DownloadWoocom, api::GetMetaDataRequest::ConfigureWoocom => DBEnum::ConfigureWoocom, @@ -158,6 +166,10 @@ fn into_response( let resp = utils::deserialize_to_response(data)?; Ok(api::GetMetaDataResponse::IntegrationMethod(resp)) } + DBEnum::ConfigurationType => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ConfigurationType(resp)) + } DBEnum::IntegrationCompleted => Ok(api::GetMetaDataResponse::IntegrationCompleted( data.is_some(), )), @@ -173,6 +185,14 @@ fn into_response( let resp = utils::deserialize_to_response(data)?; Ok(api::GetMetaDataResponse::SPRoutingConfigured(resp)) } + DBEnum::Feedback => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::Feedback(resp)) + } + DBEnum::ProdIntent => { + let resp = utils::deserialize_to_response(data)?; + Ok(api::GetMetaDataResponse::ProdIntent(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())), @@ -282,15 +302,54 @@ async fn insert_metadata( .await } types::MetaData::IntegrationMethod(data) => { - utils::insert_merchant_scoped_metadata_to_db( + let mut metadata = utils::insert_merchant_scoped_metadata_to_db( state, - user.user_id, - user.merchant_id, - user.org_id, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), metadata_key, - data, + data.clone(), ) - .await + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_merchant_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } + types::MetaData::ConfigurationType(data) => { + let mut metadata = utils::insert_merchant_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_merchant_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata } types::MetaData::IntegrationCompleted(data) => { utils::insert_merchant_scoped_metadata_to_db( @@ -336,6 +395,56 @@ async fn insert_metadata( ) .await } + types::MetaData::Feedback(data) => { + let mut metadata = utils::insert_user_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_user_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } + types::MetaData::ProdIntent(data) => { + let mut metadata = utils::insert_user_scoped_metadata_to_db( + state, + user.user_id.clone(), + user.merchant_id.clone(), + user.org_id.clone(), + metadata_key, + data.clone(), + ) + .await; + + if utils::is_update_required(&metadata) { + metadata = utils::update_user_scoped_metadata( + state, + user.user_id, + user.merchant_id, + user.org_id, + metadata_key, + data, + ) + .await + .change_context(UserErrors::InternalServerError); + } + metadata + } types::MetaData::SPTestPayment(data) => { utils::insert_merchant_scoped_metadata_to_db( state, @@ -400,7 +509,8 @@ async fn fetch_metadata( 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); + let (merchant_scoped_enums, user_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( @@ -413,6 +523,18 @@ async fn fetch_metadata( dashboard_metadata.append(&mut res); } + if !user_scoped_enums.is_empty() { + let mut res = utils::get_user_scoped_metadata_from_db( + state, + user.user_id.to_owned(), + user.merchant_id.to_owned(), + user.org_id.to_owned(), + user_scoped_enums, + ) + .await?; + dashboard_metadata.append(&mut res); + } + Ok(dashboard_metadata) } diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index f385e1bc5a83..1ab5a8360812 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -997,7 +997,7 @@ impl RefundInterface for MockDb { let mut refund_meta_data = api_models::refunds::RefundListMetaData { connector: vec![], currency: vec![], - status: vec![], + refund_status: vec![], }; let mut unique_connectors = HashSet::new(); @@ -1016,7 +1016,7 @@ impl RefundInterface for MockDb { refund_meta_data.connector = unique_connectors.into_iter().collect(); refund_meta_data.currency = unique_currencies.into_iter().collect(); - refund_meta_data.status = unique_statuses.into_iter().collect(); + refund_meta_data.refund_status = unique_statuses.into_iter().collect(); Ok(refund_meta_data) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5d14d1219d32..acf98c658a7c 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -860,6 +860,9 @@ impl User { .service( web::resource("/connect_account").route(web::post().to(user_connect_account)), ) + .service(web::resource("/forgot_password").route(web::post().to(forgot_password))) + .service(web::resource("/reset_password").route(web::post().to(reset_password))) + .service(web::resource("user/invite").route(web::post().to(invite_user))) .service( web::resource("/signup_with_merchant_id") .route(web::post().to(user_signup_with_merchant_id)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 3592506f522b..0c850922fff4 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -163,6 +163,9 @@ impl From for ApiIdentifier { | Flow::DeleteSampleData | Flow::UserMerchantAccountList | Flow::GetUserDetails + | Flow::ForgotPassword + | Flow::ResetPassword + | Flow::InviteUser | Flow::UserSignUpWithMerchantId => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 45fa0ba35c59..c4476d6ed710 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -294,3 +294,60 @@ pub async fn get_user_details(state: web::Data, req: HttpRequest) -> H )) .await } + +#[cfg(feature = "email")] +pub async fn forgot_password( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::ForgotPassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, _, payload| user_core::forgot_password(state, payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn reset_password( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::ResetPassword; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, _, payload| user_core::reset_password(state, payload), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] +pub async fn invite_user( + state: web::Data, + req: HttpRequest, + payload: web::Json, +) -> HttpResponse { + let flow = Flow::InviteUser; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + payload.into_inner(), + |state, user, payload| user_core::invite_user(state, payload, user), + &auth::JWTAuth(Permission::UsersWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index a4a4681c6001..ad91edd8c364 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -66,6 +66,10 @@ impl EmailToken { }; jwt::generate_jwt(&token_payload, settings).await } + + pub fn get_email(&self) -> &str { + self.email.as_str() + } } pub fn get_link_with_token( diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 592195922493..16a00f117034 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -259,6 +259,15 @@ impl From for NewUserOrganization { } } +type InviteeUserRequestWithInvitedUserToken = (user_api::InviteUserRequest, UserFromToken); +impl From for NewUserOrganization { + fn from(_value: InviteeUserRequestWithInvitedUserToken) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + #[derive(Clone)] pub struct MerchantId(String); @@ -420,6 +429,19 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult { + let merchant_id = MerchantId::new(value.clone().1.merchant_id)?; + let new_organization = NewUserOrganization::from(value); + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + type UserMerchantCreateRequestWithToken = (UserFromStorage, user_api::UserMerchantCreate, UserFromToken); @@ -657,6 +679,26 @@ impl TryFrom for NewUser { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.0.email.clone().try_into()?; + let name = UserName::new(value.0.name.clone())?; + let password = password::generate_password_hash(uuid::Uuid::new_v4().to_string().into())?; + let password = UserPassword::new(password)?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + #[derive(Clone)] pub struct UserFromStorage(pub storage_user::User); diff --git a/crates/router/src/types/domain/user/dashboard_metadata.rs b/crates/router/src/types/domain/user/dashboard_metadata.rs index e65379346ac9..5e4017a3cb1a 100644 --- a/crates/router/src/types/domain/user/dashboard_metadata.rs +++ b/crates/router/src/types/domain/user/dashboard_metadata.rs @@ -13,10 +13,13 @@ pub enum MetaData { ConfiguredRouting(api::ConfiguredRouting), TestPayment(api::TestPayment), IntegrationMethod(api::IntegrationMethod), + ConfigurationType(api::ConfigurationType), IntegrationCompleted(bool), StripeConnected(api::ProcessorConnected), PaypalConnected(api::ProcessorConnected), SPRoutingConfigured(api::ConfiguredRouting), + Feedback(api::Feedback), + ProdIntent(api::ProdIntent), SPTestPayment(bool), DownloadWoocom(bool), ConfigureWoocom(bool), @@ -36,10 +39,13 @@ impl From<&MetaData> for DBEnum { MetaData::ConfiguredRouting(_) => Self::ConfiguredRouting, MetaData::TestPayment(_) => Self::TestPayment, MetaData::IntegrationMethod(_) => Self::IntegrationMethod, + MetaData::ConfigurationType(_) => Self::ConfigurationType, MetaData::IntegrationCompleted(_) => Self::IntegrationCompleted, MetaData::StripeConnected(_) => Self::StripeConnected, MetaData::PaypalConnected(_) => Self::PaypalConnected, MetaData::SPRoutingConfigured(_) => Self::SpRoutingConfigured, + MetaData::Feedback(_) => Self::Feedback, + MetaData::ProdIntent(_) => Self::ProdIntent, MetaData::SPTestPayment(_) => Self::SpTestPayment, MetaData::DownloadWoocom(_) => Self::DownloadWoocom, MetaData::ConfigureWoocom(_) => Self::ConfigureWoocom, diff --git a/crates/router/src/types/storage/refund.rs b/crates/router/src/types/storage/refund.rs index 4d5667700122..bb05233173c8 100644 --- a/crates/router/src/types/storage/refund.rs +++ b/crates/router/src/types/storage/refund.rs @@ -50,23 +50,40 @@ impl RefundDbExt for Refund { .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .order(dsl::modified_at.desc()) .into_boxed(); - - match &refund_list_details.payment_id { - Some(pid) => { - filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); - } - None => { - filter = filter.limit(limit).offset(offset); - } - }; - match &refund_list_details.refund_id { - Some(ref_id) => { - filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); - } - None => { - filter = filter.limit(limit).offset(offset); - } + let mut search_by_pay_or_ref_id = false; + + if let (Some(pid), Some(ref_id)) = ( + &refund_list_details.payment_id, + &refund_list_details.refund_id, + ) { + search_by_pay_or_ref_id = true; + filter = filter + .filter(dsl::payment_id.eq(pid.to_owned())) + .or_filter(dsl::refund_id.eq(ref_id.to_owned())) + .limit(limit) + .offset(offset); }; + + if !search_by_pay_or_ref_id { + match &refund_list_details.payment_id { + Some(pid) => { + filter = filter.filter(dsl::payment_id.eq(pid.to_owned())); + } + None => { + filter = filter.limit(limit).offset(offset); + } + }; + } + if !search_by_pay_or_ref_id { + match &refund_list_details.refund_id { + Some(ref_id) => { + filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + } + None => { + filter = filter.limit(limit).offset(offset); + } + }; + } match &refund_list_details.profile_id { Some(profile_id) => { filter = filter @@ -163,7 +180,7 @@ impl RefundDbExt for Refund { let meta = api_models::refunds::RefundListMetaData { connector: filter_connector, currency: filter_currency, - status: filter_status, + refund_status: filter_status, }; Ok(meta) @@ -179,12 +196,28 @@ impl RefundDbExt for Refund { .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .into_boxed(); - if let Some(pay_id) = &refund_list_details.payment_id { - filter = filter.filter(dsl::payment_id.eq(pay_id.to_owned())); + let mut search_by_pay_or_ref_id = false; + + if let (Some(pid), Some(ref_id)) = ( + &refund_list_details.payment_id, + &refund_list_details.refund_id, + ) { + search_by_pay_or_ref_id = true; + filter = filter + .filter(dsl::payment_id.eq(pid.to_owned())) + .or_filter(dsl::refund_id.eq(ref_id.to_owned())); + }; + + if !search_by_pay_or_ref_id { + if let Some(pay_id) = &refund_list_details.payment_id { + filter = filter.filter(dsl::payment_id.eq(pay_id.to_owned())); + } } - if let Some(ref_id) = &refund_list_details.refund_id { - filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + if !search_by_pay_or_ref_id { + if let Some(ref_id) = &refund_list_details.refund_id { + filter = filter.filter(dsl::refund_id.eq(ref_id.to_owned())); + } } if let Some(profile_id) = &refund_list_details.profile_id { filter = filter.filter(dsl::profile_id.eq(profile_id.to_owned())); diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs index 5f354e613f95..40594a6e49f6 100644 --- a/crates/router/src/utils/user/dashboard_metadata.rs +++ b/crates/router/src/utils/user/dashboard_metadata.rs @@ -6,7 +6,7 @@ use api_models::user::dashboard_metadata::{ }; use diesel_models::{ enums::DashboardMetadata as DBEnum, - user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew, DashboardMetadataUpdate}, }; use error_stack::{IntoReport, ResultExt}; use masking::Secret; @@ -50,6 +50,40 @@ pub async fn insert_merchant_scoped_metadata_to_db( e.change_context(UserErrors::InternalServerError) }) } +pub async fn insert_user_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: Some(user_id.clone()), + 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, @@ -73,6 +107,88 @@ pub async fn get_merchant_scoped_metadata_from_db( } } } +pub async fn get_user_scoped_metadata_from_db( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_keys: Vec, +) -> UserResult> { + match state + .store + .find_user_scoped_dashboard_metadata(&user_id, &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 async fn update_merchant_scoped_metadata( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + 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 + .update_metadata( + None, + merchant_id, + org_id, + metadata_key, + DashboardMetadataUpdate::UpdateData { + data_key: metadata_key, + data_value, + last_modified_by: user_id, + }, + ) + .await + .change_context(UserErrors::InternalServerError) +} +pub async fn update_user_scoped_metadata( + state: &AppState, + user_id: String, + merchant_id: String, + org_id: String, + metadata_key: DBEnum, + metadata_value: impl serde::Serialize, +) -> UserResult { + 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 + .update_metadata( + Some(user_id.clone()), + merchant_id, + org_id, + metadata_key, + DashboardMetadataUpdate::UpdateData { + data_key: metadata_key, + data_value, + last_modified_by: user_id, + }, + ) + .await + .change_context(UserErrors::InternalServerError) +} pub fn deserialize_to_response(data: Option<&DashboardMetadata>) -> UserResult> where @@ -87,7 +203,7 @@ where pub fn separate_metadata_type_based_on_scope( metadata_keys: Vec, ) -> (Vec, Vec) { - let (mut merchant_scoped, user_scoped) = ( + let (mut merchant_scoped, mut user_scoped) = ( Vec::with_capacity(metadata_keys.len()), Vec::with_capacity(metadata_keys.len()), ); @@ -102,6 +218,7 @@ pub fn separate_metadata_type_based_on_scope( | DBEnum::ConfiguredRouting | DBEnum::TestPayment | DBEnum::IntegrationMethod + | DBEnum::ConfigurationType | DBEnum::IntegrationCompleted | DBEnum::StripeConnected | DBEnum::PaypalConnected @@ -111,11 +228,19 @@ pub fn separate_metadata_type_based_on_scope( | DBEnum::ConfigureWoocom | DBEnum::SetupWoocomWebhook | DBEnum::IsMultipleConfiguration => merchant_scoped.push(key), + DBEnum::Feedback | DBEnum::ProdIntent => user_scoped.push(key), } } (merchant_scoped, user_scoped) } +pub fn is_update_required(metadata: &UserResult) -> bool { + match metadata { + Ok(_) => false, + Err(e) => matches!(e.current_context(), UserErrors::MetadataAlreadySet), + } +} + pub fn is_backfill_required(metadata_key: &DBEnum) -> bool { matches!( metadata_key, diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index b4e530692319..d35090551de7 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -293,6 +293,12 @@ pub enum Flow { UserMerchantAccountList, /// Get users for merchant account GetUserDetails, + /// Get reset password link + ForgotPassword, + /// Reset password using link + ResetPassword, + /// Invite users + InviteUser, /// Incremental Authorization flow PaymentsIncrementalAuthorization, } From 8b7a7aa6494ff669e1f8bcc92a5160e422d6b26e Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:44:24 +0530 Subject: [PATCH 5/6] docs(test_utils): Update postman docs (#3055) --- crates/test_utils/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/test_utils/README.md b/crates/test_utils/README.md index 2edbc7104c25..a82c74cb59f6 100644 --- a/crates/test_utils/README.md +++ b/crates/test_utils/README.md @@ -22,9 +22,9 @@ The heart of `newman`(with directory support) and `UI-tests` Required fields: -- `--admin_api_key` -- Admin API Key of the environment. `test_admin` is the Admin API Key for running locally -- `--base_url` -- Base URL of the environment. `http://127.0.0.1:8080` / `http://localhost:8080` is the Base URL for running locally -- `--connector_name` -- Name of the connector that you wish to run. Example: `adyen`, `shift4`, `stripe` +- `--admin-api-key` -- Admin API Key of the environment. `test_admin` is the Admin API Key for running locally +- `--base-url` -- Base URL of the environment. `http://127.0.0.1:8080` / `http://localhost:8080` is the Base URL for running locally +- `--connector-name` -- Name of the connector that you wish to run. Example: `adyen`, `shift4`, `stripe` Optional fields: @@ -46,7 +46,7 @@ Optional fields: - Tests can be run with the following command: ```shell - cargo run --package test_utils --bin test_utils -- --connector_name= --base_url= --admin_api_key= \ + cargo run --package test_utils --bin test_utils -- --connector-name= --base-url= --admin-api-key= \ # optionally --folder ",,..." --verbose ``` From 53df543b7f1407a758232025b7de0fb527be8e86 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:03:38 +0530 Subject: [PATCH 6/6] fix: remove redundant call to populate_payment_data function (#3054) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Bernard Eugine <114725419+bernard-eugine@users.noreply.github.com> --- Cargo.lock | 1 - crates/common_utils/Cargo.toml | 1 - crates/router/src/core/payments.rs | 4 ---- 3 files changed, 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb38c0b70b59..d2e8d9dd5df9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1708,7 +1708,6 @@ dependencies = [ "thiserror", "time", "tokio 1.32.0", - "utoipa", ] [[package]] diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3a41b111b39d..3619c93d772c 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -38,7 +38,6 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } -utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 16fda276f6a5..21a2866c9f4e 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -953,10 +953,6 @@ where payment_data, ) .await?; - operation - .to_domain()? - .populate_payment_data(state, payment_data, merchant_account) - .await?; let mut router_data = payment_data .construct_router_data(