feat(user): add email apis and new enums for metadata (#3053)

Co-authored-by: Rachit Naithani <rachit.naithani@juspay.in>
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>
This commit is contained in:
Apoorv Dixit
2023-12-05 13:10:17 +05:30
committed by GitHub
parent 5b62731399
commit 1c3d260dc3
19 changed files with 727 additions and 53 deletions

View File

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

View File

@ -174,7 +174,7 @@ pub struct RefundListMetaData {
pub currency: Vec<enums::Currency>,
/// The list of available refund status filters
#[schema(value_type = Vec<RefundStatus>)]
pub status: Vec<enums::RefundStatus>,
pub refund_status: Vec<enums::RefundStatus>,
}
/// The status for refunds

View File

@ -65,6 +65,29 @@ pub struct ChangePasswordRequest {
pub old_password: Secret<String>,
}
#[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<String>,
pub password: Secret<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct InviteUserRequest {
pub email: pii::Email,
pub name: Secret<String>,
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,

View File

@ -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<String>,
pub rating: Option<i32>,
pub category: Option<String>,
}
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub struct ProdIntent {
pub legal_business_name: Option<String>,
pub business_label: Option<String>,
pub business_location: Option<CountryAlpha2>,
pub display_name: Option<String>,
pub poc_email: Option<String>,
pub business_type: Option<String>,
pub business_identifier: Option<String>,
pub business_website: Option<String>,
pub poc_name: Option<String>,
pub poc_contact: Option<String>,
pub comments: Option<String>,
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<ConfiguredRouting>),
TestPayment(Option<TestPayment>),
IntegrationMethod(Option<IntegrationMethod>),
ConfigurationType(Option<ConfigurationType>),
IntegrationCompleted(bool),
StripeConnected(Option<ProcessorConnected>),
PaypalConnected(Option<ProcessorConnected>),
SPRoutingConfigured(Option<ConfiguredRouting>),
Feedback(Option<Feedback>),
ProdIntent(Option<ProdIntent>),
SPTestPayment(bool),
DownloadWoocom(bool),
ConfigureWoocom(bool),

View File

@ -452,10 +452,13 @@ pub enum DashboardMetadata {
ConfiguredRouting,
TestPayment,
IntegrationMethod,
ConfigurationType,
IntegrationCompleted,
StripeConnected,
PaypalConnected,
SpRoutingConfigured,
Feedback,
ProdIntent,
SpTestPayment,
DownloadWoocom,
ConfigureWoocom,

View File

@ -28,6 +28,12 @@ impl DashboardMetadata {
data_key: enums::DashboardMetadata,
dashboard_metadata_update: DashboardMetadataUpdate,
) -> StorageResult<Self> {
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::<
<Self as HasTable>::Table,
_,
@ -35,14 +41,23 @@ impl DashboardMetadata {
_,
>(
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())),
predicate.and(dsl::user_id.eq(uid)),
DashboardMetadataUpdateInternal::from(dashboard_metadata_update),
)
.await
} else {
generics::generic_update_with_unique_predicate_get_result::<
<Self as HasTable>::Table,
_,
_,
_,
>(
conn,
predicate.and(dsl::user_id.is_null()),
DashboardMetadataUpdateInternal::from(dashboard_metadata_update),
)
.await
}
}
pub async fn find_user_scoped_dashboard_metadata(

View File

@ -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<api_models::errors::types::ApiErrorRespon
"Incorrect email or password",
None,
)),
Self::UserNotFound => AER::Unauthorized(ApiError::new(
sub_code,
2,
"Email doesnt 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,

View File

@ -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::<EmailToken>(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<user_api::InviteUserResponse> {
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,

View File

@ -81,12 +81,17 @@ fn parse_set_request(data_enum: api::SetMetaDataRequest) -> UserResult<types::Me
api::SetMetaDataRequest::IntegrationMethod(req) => {
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,7 +302,18 @@ 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.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,
@ -291,6 +322,34 @@ async fn insert_metadata(
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<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);
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)
}

View File

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

View File

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

View File

@ -163,6 +163,9 @@ impl From<Flow> 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 => {

View File

@ -294,3 +294,60 @@ pub async fn get_user_details(state: web::Data<AppState>, req: HttpRequest) -> H
))
.await
}
#[cfg(feature = "email")]
pub async fn forgot_password(
state: web::Data<AppState>,
req: HttpRequest,
payload: web::Json<user_api::ForgotPasswordRequest>,
) -> 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<AppState>,
req: HttpRequest,
payload: web::Json<user_api::ResetPasswordRequest>,
) -> 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<AppState>,
req: HttpRequest,
payload: web::Json<user_api::InviteUserRequest>,
) -> 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
}

View File

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

View File

@ -259,6 +259,15 @@ impl From<UserMerchantCreateRequestWithToken> for NewUserOrganization {
}
}
type InviteeUserRequestWithInvitedUserToken = (user_api::InviteUserRequest, UserFromToken);
impl From<InviteeUserRequestWithInvitedUserToken> 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<user_api::CreateInternalUserRequest> for NewUserMerchant {
}
}
impl TryFrom<InviteeUserRequestWithInvitedUserToken> for NewUserMerchant {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult<Self> {
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<UserMerchantCreateRequestWithToken> for NewUser {
}
}
impl TryFrom<InviteeUserRequestWithInvitedUserToken> for NewUser {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: InviteeUserRequestWithInvitedUserToken) -> UserResult<Self> {
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);

View File

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

View File

@ -50,7 +50,21 @@ impl RefundDbExt for Refund {
.filter(dsl::merchant_id.eq(merchant_id.to_owned()))
.order(dsl::modified_at.desc())
.into_boxed();
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()));
@ -59,6 +73,8 @@ impl RefundDbExt for Refund {
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()));
@ -67,6 +83,7 @@ impl RefundDbExt for Refund {
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,13 +196,29 @@ impl RefundDbExt for Refund {
.filter(dsl::merchant_id.eq(merchant_id.to_owned()))
.into_boxed();
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 !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()));
}

View File

@ -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<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: 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<DBEnum>,
) -> UserResult<Vec<DashboardMetadata>> {
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<DashboardMetadata> {
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<DashboardMetadata> {
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<T>(data: Option<&DashboardMetadata>) -> UserResult<Option<T>>
where
@ -87,7 +203,7 @@ where
pub fn separate_metadata_type_based_on_scope(
metadata_keys: Vec<DBEnum>,
) -> (Vec<DBEnum>, Vec<DBEnum>) {
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<DashboardMetadata>) -> 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,

View File

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