From ec15ddd0d0ed942fedec525406df3005d494b8d4 Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:07:17 +0530 Subject: [PATCH 01/18] feat(user): add user_list and switch_list apis (#3033) Co-authored-by: Mani Chandra Dulam --- crates/api_models/src/events/user.rs | 5 +- crates/api_models/src/user.rs | 17 +++++ crates/api_models/src/user_role.rs | 6 ++ .../src/query/dashboard_metadata.rs | 30 ++++++++- crates/diesel_models/src/query/user.rs | 47 +++++++++++--- .../src/user/dashboard_metadata.rs | 37 +++++++++++ crates/router/src/core/user.rs | 26 ++++++++ crates/router/src/db/dashboard_metadata.rs | 65 +++++++++++++++++++ crates/router/src/db/kafka_store.rs | 28 ++++++++ crates/router/src/db/user.rs | 60 ++++++----------- crates/router/src/routes/app.rs | 2 + crates/router/src/routes/lock_utils.rs | 4 +- crates/router/src/routes/user.rs | 31 +++++++++ crates/router/src/types/domain/user.rs | 29 ++++++++- crates/router/src/utils/user.rs | 17 +++++ crates/router_env/src/logger/types.rs | 4 ++ 16 files changed, 356 insertions(+), 52 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 3ac65830eb8b..8b7cd02c9350 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -7,7 +7,7 @@ use crate::user::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, - CreateInternalUserRequest, SwitchMerchantIdRequest, UserMerchantCreate, + CreateInternalUserRequest, GetUsersResponse, SwitchMerchantIdRequest, UserMerchantCreate, }; impl ApiEventMetric for ConnectAccountResponse { @@ -29,7 +29,8 @@ common_utils::impl_misc_api_event_type!( SetMetaDataRequest, SwitchMerchantIdRequest, CreateInternalUserRequest, - UserMerchantCreate + UserMerchantCreate, + GetUsersResponse ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index e6e8546c6741..36d730f5118e 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -1,5 +1,7 @@ use common_utils::pii; use masking::Secret; + +use crate::user_role::UserStatus; pub mod dashboard_metadata; #[cfg(feature = "dummy_connector")] pub mod sample_data; @@ -45,3 +47,18 @@ pub struct CreateInternalUserRequest { pub struct UserMerchantCreate { pub company_name: String, } + +#[derive(Debug, serde::Serialize)] +pub struct GetUsersResponse(pub Vec); + +#[derive(Debug, serde::Serialize)] +pub struct UserDetails { + pub user_id: String, + pub email: pii::Email, + pub name: Secret, + pub role_id: String, + pub role_name: String, + pub status: UserStatus, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub last_modified_at: time::PrimitiveDateTime, +} diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 521d17e73428..735cd240b6e7 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -80,3 +80,9 @@ pub struct UpdateUserRoleRequest { pub user_id: String, pub role_id: String, } + +#[derive(Debug, serde::Serialize)] +pub enum UserStatus { + Active, + InvitationSent, +} diff --git a/crates/diesel_models/src/query/dashboard_metadata.rs b/crates/diesel_models/src/query/dashboard_metadata.rs index 03e4a2dab38b..44fd24c7acf2 100644 --- a/crates/diesel_models/src/query/dashboard_metadata.rs +++ b/crates/diesel_models/src/query/dashboard_metadata.rs @@ -5,7 +5,10 @@ use crate::{ enums, query::generics, schema::dashboard_metadata::dsl, - user::dashboard_metadata::{DashboardMetadata, DashboardMetadataNew}, + user::dashboard_metadata::{ + DashboardMetadata, DashboardMetadataNew, DashboardMetadataUpdate, + DashboardMetadataUpdateInternal, + }, PgPooledConn, StorageResult, }; @@ -17,6 +20,31 @@ impl DashboardMetadataNew { } impl DashboardMetadata { + pub async fn update( + conn: &PgPooledConn, + user_id: Option, + merchant_id: String, + org_id: String, + 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 + } + pub async fn find_user_scoped_dashboard_metadata( conn: &PgPooledConn, user_id: String, diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs index aa1d8471d213..b4d5976ba294 100644 --- a/crates/diesel_models/src/query/user.rs +++ b/crates/diesel_models/src/query/user.rs @@ -1,13 +1,24 @@ -use diesel::{associations::HasTable, ExpressionMethods}; -use error_stack::report; -use router_env::tracing::{self, instrument}; +use async_bb8_diesel::AsyncRunQueryDsl; +use diesel::{ + associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods, + JoinOnDsl, QueryDsl, +}; +use error_stack::{report, IntoReport}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; pub mod sample_data; use crate::{ errors::{self}, query::generics, - schema::users::dsl, + schema::{ + user_roles::{self, dsl as user_roles_dsl}, + users::dsl as users_dsl, + }, user::*, + user_role::UserRole, PgPooledConn, StorageResult, }; @@ -22,7 +33,7 @@ impl User { pub async fn find_by_user_email(conn: &PgPooledConn, user_email: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::email.eq(user_email.to_owned()), + users_dsl::email.eq(user_email.to_owned()), ) .await } @@ -30,7 +41,7 @@ impl User { pub async fn find_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), ) .await } @@ -42,7 +53,7 @@ impl User { ) -> StorageResult { generics::generic_update_with_results::<::Table, _, _, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), UserUpdateInternal::from(user), ) .await? @@ -56,8 +67,28 @@ impl User { pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult { generics::generic_delete::<::Table, _>( conn, - dsl::user_id.eq(user_id.to_owned()), + users_dsl::user_id.eq(user_id.to_owned()), ) .await } + + pub async fn find_joined_users_and_roles_by_merchant_id( + conn: &PgPooledConn, + mid: &str, + ) -> StorageResult> { + let query = Self::table() + .inner_join(user_roles::table.on(user_roles_dsl::user_id.eq(users_dsl::user_id))) + .filter(user_roles_dsl::merchant_id.eq(mid.to_owned())); + + logger::debug!(query = %debug_query::(&query).to_string()); + + query + .get_results_async::<(Self, UserRole)>(conn) + .await + .into_report() + .map_err(|err| match err.current_context() { + DieselError::NotFound => err.change_context(errors::DatabaseError::NotFound), + _ => err.change_context(errors::DatabaseError::Others), + }) + } } diff --git a/crates/diesel_models/src/user/dashboard_metadata.rs b/crates/diesel_models/src/user/dashboard_metadata.rs index 018808f1c0db..1eeb61d6135e 100644 --- a/crates/diesel_models/src/user/dashboard_metadata.rs +++ b/crates/diesel_models/src/user/dashboard_metadata.rs @@ -33,3 +33,40 @@ pub struct DashboardMetadataNew { pub last_modified_by: String, pub last_modified_at: PrimitiveDateTime, } + +#[derive( + router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay, AsChangeset, +)] +#[diesel(table_name = dashboard_metadata)] +pub struct DashboardMetadataUpdateInternal { + pub data_key: enums::DashboardMetadata, + pub data_value: serde_json::Value, + pub last_modified_by: String, + pub last_modified_at: PrimitiveDateTime, +} + +pub enum DashboardMetadataUpdate { + UpdateData { + data_key: enums::DashboardMetadata, + data_value: serde_json::Value, + last_modified_by: String, + }, +} + +impl From for DashboardMetadataUpdateInternal { + fn from(metadata_update: DashboardMetadataUpdate) -> Self { + let last_modified_at = common_utils::date_time::now(); + match metadata_update { + DashboardMetadataUpdate::UpdateData { + data_key, + data_value, + last_modified_by, + } => Self { + data_key, + data_value, + last_modified_by, + last_modified_at, + }, + } + } +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index b38fb4cf4ae3..7d0d599cc4ed 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -324,3 +324,29 @@ pub async fn create_merchant_account( Ok(ApplicationResponse::StatusOk) } + +pub async fn list_merchant_ids_for_user( + state: AppState, + user: auth::UserFromToken, +) -> UserResponse> { + Ok(ApplicationResponse::Json( + utils::user::get_merchant_ids_for_user(state, &user.user_id).await?, + )) +} + +pub async fn get_users_for_merchant_account( + state: AppState, + user_from_token: auth::UserFromToken, +) -> UserResponse { + let users = state + .store + .find_users_and_roles_by_merchant_id(user_from_token.merchant_id.as_str()) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("No users for given merchant id")? + .into_iter() + .filter_map(|(user, role)| domain::UserAndRoleJoined(user, role).try_into().ok()) + .collect(); + + Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users))) +} diff --git a/crates/router/src/db/dashboard_metadata.rs b/crates/router/src/db/dashboard_metadata.rs index 2e8129398ca3..ec24b4ed07da 100644 --- a/crates/router/src/db/dashboard_metadata.rs +++ b/crates/router/src/db/dashboard_metadata.rs @@ -14,6 +14,14 @@ pub trait DashboardMetadataInterface { &self, metadata: storage::DashboardMetadataNew, ) -> CustomResult; + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult; async fn find_user_scoped_dashboard_metadata( &self, @@ -44,6 +52,28 @@ impl DashboardMetadataInterface for Store { .into_report() } + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::DashboardMetadata::update( + &conn, + user_id, + merchant_id, + org_id, + data_key, + dashboard_metadata_update, + ) + .await + .map_err(Into::into) + .into_report() + } + async fn find_user_scoped_dashboard_metadata( &self, user_id: &str, @@ -121,6 +151,41 @@ impl DashboardMetadataInterface for MockDb { Ok(metadata_new) } + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + let mut dashboard_metadata = self.dashboard_metadata.lock().await; + + let dashboard_metadata_to_update = dashboard_metadata + .iter_mut() + .find(|metadata| { + metadata.user_id == user_id + && metadata.merchant_id == merchant_id + && metadata.org_id == org_id + && metadata.data_key == data_key + }) + .ok_or(errors::StorageError::MockDbError)?; + + match dashboard_metadata_update { + storage::DashboardMetadataUpdate::UpdateData { + data_key, + data_value, + last_modified_by, + } => { + dashboard_metadata_to_update.data_key = data_key; + dashboard_metadata_to_update.data_value = data_value; + dashboard_metadata_to_update.last_modified_by = last_modified_by; + dashboard_metadata_to_update.last_modified_at = common_utils::date_time::now(); + } + } + Ok(dashboard_metadata_to_update.clone()) + } + async fn find_user_scoped_dashboard_metadata( &self, user_id: &str, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 60a2fb4c2bb3..32548e36b6fb 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1878,6 +1878,15 @@ impl UserInterface for KafkaStore { ) -> CustomResult { self.diesel_store.delete_user_by_user_id(user_id).await } + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_users_and_roles_by_merchant_id(merchant_id) + .await + } } impl RedisConnInterface for KafkaStore { @@ -1930,6 +1939,25 @@ impl DashboardMetadataInterface for KafkaStore { self.diesel_store.insert_metadata(metadata).await } + async fn update_metadata( + &self, + user_id: Option, + merchant_id: String, + org_id: String, + data_key: enums::DashboardMetadata, + dashboard_metadata_update: storage::DashboardMetadataUpdate, + ) -> CustomResult { + self.diesel_store + .update_metadata( + user_id, + merchant_id, + org_id, + data_key, + dashboard_metadata_update, + ) + .await + } + async fn find_user_scoped_dashboard_metadata( &self, user_id: &str, diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs index be0554ec69a0..e3dda965f9c9 100644 --- a/crates/router/src/db/user.rs +++ b/crates/router/src/db/user.rs @@ -1,4 +1,4 @@ -use diesel_models::user as storage; +use diesel_models::{user as storage, user_role::UserRole}; use error_stack::{IntoReport, ResultExt}; use masking::Secret; @@ -37,6 +37,11 @@ pub trait UserInterface { &self, user_id: &str, ) -> CustomResult; + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -97,6 +102,17 @@ impl UserInterface for Store { .map_err(Into::into) .into_report() } + + async fn find_users_and_roles_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::User::find_joined_users_and_roles_by_merchant_id(&conn, merchant_id) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -222,45 +238,11 @@ impl UserInterface for MockDb { users.remove(user_index); Ok(true) } -} -#[cfg(feature = "kafka_events")] -#[async_trait::async_trait] -impl UserInterface for super::KafkaStore { - async fn insert_user( - &self, - user_data: storage::UserNew, - ) -> CustomResult { - self.diesel_store.insert_user(user_data).await - } - async fn find_user_by_email( + async fn find_users_and_roles_by_merchant_id( &self, - user_email: &str, - ) -> CustomResult { - self.diesel_store.find_user_by_email(user_email).await - } - - async fn find_user_by_id( - &self, - user_id: &str, - ) -> CustomResult { - self.diesel_store.find_user_by_id(user_id).await - } - - async fn update_user_by_user_id( - &self, - user_id: &str, - user: storage::UserUpdate, - ) -> CustomResult { - self.diesel_store - .update_user_by_user_id(user_id, user) - .await - } - - async fn delete_user_by_user_id( - &self, - user_id: &str, - ) -> CustomResult { - self.diesel_store.delete_user_by_user_id(user_id).await + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 9c83583bc6af..a145f3e7e5d7 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -839,6 +839,8 @@ impl User { web::resource("/create_merchant") .route(web::post().to(user_merchant_account_create)), ) + .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) + .service(web::resource("/user/list").route(web::get().to(get_user_details))) // User Role APIs .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 04b2b0dc9533..6aa2bbad0b15 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -157,7 +157,9 @@ impl From for ApiIdentifier { | Flow::SwitchMerchant | Flow::UserMerchantAccountCreate | Flow::GenerateSampleData - | Flow::DeleteSampleData => Self::User, + | Flow::DeleteSampleData + | Flow::UserMerchantAccountList + | Flow::GetUserDetails => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 78aecea2444a..97bd7054da9e 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -204,3 +204,34 @@ pub async fn delete_sample_data( )) .await } + +pub async fn list_merchant_ids_for_user( + state: web::Data, + req: HttpRequest, +) -> HttpResponse { + let flow = Flow::UserMerchantAccountList; + Box::pin(api::server_wrap( + flow, + state, + &req, + (), + |state, user, _| user_core::list_merchant_ids_for_user(state, user), + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn get_user_details(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::GetUserDetails; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user, _| user_core::get_users_for_merchant_account(state, user), + &auth::JWTAuth(Permission::UsersRead), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 0c7760f84d36..082b29d80941 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -27,7 +27,7 @@ use crate::{ routes::AppState, services::{ authentication::{AuthToken, UserFromToken}, - authorization::info, + authorization::{info, predefined_permissions}, }, types::transformers::ForeignFrom, utils::user::password, @@ -671,3 +671,30 @@ impl TryFrom for user_role_api::PermissionInfo { }) } } + +pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole); + +impl TryFrom for user_api::UserDetails { + type Error = (); + fn try_from(user_and_role: UserAndRoleJoined) -> Result { + let status = match user_and_role.1.status { + UserStatus::Active => user_role_api::UserStatus::Active, + UserStatus::InvitationSent => user_role_api::UserStatus::InvitationSent, + }; + + let role_id = user_and_role.1.role_id; + let role_name = predefined_permissions::get_role_name_from_id(role_id.as_str()) + .ok_or(())? + .to_string(); + + Ok(Self { + user_id: user_and_role.0.user_id, + email: user_and_role.0.email, + name: user_and_role.0.name, + role_id, + status, + role_name, + last_modified_at: user_and_role.1.last_modified_at, + }) + } +} diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index c29e78c7141e..696aa4090044 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,3 +1,4 @@ +use diesel_models::enums::UserStatus; use error_stack::ResultExt; use crate::{ @@ -51,3 +52,19 @@ impl UserFromToken { Ok(user) } } + +pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserResult> { + Ok(state + .store + .list_user_roles_by_user_id(user_id) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|ele| { + if ele.status == UserStatus::Active { + return Some(ele.merchant_id); + } + None + }) + .collect()) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index c844a6aeded6..f54a5a82baaf 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -283,6 +283,10 @@ pub enum Flow { GenerateSampleData, /// Delete Sample Data DeleteSampleData, + /// List merchant accounts for user + UserMerchantAccountList, + /// Get users for merchant account + GetUserDetails, } /// From 83fcd1a9deb106a44c8262923c7f1660b0c46bf2 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:20:41 +0530 Subject: [PATCH 02/18] feat(router): add key_value to locker metrics (#2995) --- .../router/src/core/payment_methods/cards.rs | 84 ++++++++++++++++--- crates/router/src/routes/metrics.rs | 1 + 2 files changed, 75 insertions(+), 10 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 044e270a7ea9..545733e298ab 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -225,12 +225,21 @@ pub async fn add_card_to_locker( ) .await .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); error }) }, &metrics::CARD_ADD_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], ) .await?; logger::debug!("card added to basilisk locker"); @@ -248,22 +257,45 @@ pub async fn add_card_to_locker( ) .await .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); error }) }, &metrics::CARD_ADD_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], ) .await; match add_card_to_rs_resp { value @ Ok(_) => { - logger::debug!("Card added successfully"); + logger::debug!("card added to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); value } Err(err) => { - logger::debug!(error =? err,"failed to add card"); + logger::debug!(error =? err,"failed to add card to rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "add"), + ], + ); Ok(add_card_to_hs_resp) } } @@ -290,12 +322,19 @@ pub async fn get_card_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); error }) }, &metrics::CARD_GET_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new("locker", "rust")], ) .await; @@ -313,20 +352,45 @@ pub async fn get_card_from_locker( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed while getting card from basilisk_hs") .map_err(|error| { - metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]); + metrics::CARD_LOCKER_FAILURES.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); error }) }, &metrics::CARD_GET_TIME, - &[], + &[router_env::opentelemetry::KeyValue::new( + "locker", "basilisk", + )], ) .await .map(|inner_card| { logger::debug!("card retrieved from basilisk locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "basilisk"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); inner_card }), Ok(_) => { logger::debug!("card retrieved from rust locker"); + let _ = &metrics::CARD_LOCKER_SUCCESSFUL_RESPONSE.add( + &metrics::CONTEXT, + 1, + &[ + router_env::opentelemetry::KeyValue::new("locker", "rust"), + router_env::opentelemetry::KeyValue::new("operation", "get"), + ], + ); get_card_from_rs_locker_resp } } diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index a8e6f9d2a892..192df1a09298 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -85,6 +85,7 @@ counter_metric!(CONNECTOR_HTTP_STATUS_CODE_5XX_COUNT, GLOBAL_METER); // Service Level counter_metric!(CARD_LOCKER_FAILURES, GLOBAL_METER); +counter_metric!(CARD_LOCKER_SUCCESSFUL_RESPONSE, GLOBAL_METER); counter_metric!(TEMP_LOCKER_FAILURES, GLOBAL_METER); histogram_metric!(CARD_ADD_TIME, GLOBAL_METER); histogram_metric!(CARD_GET_TIME, GLOBAL_METER); From 3ce04abae4eddfa27025368f5ef28987cccea43d Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Mon, 4 Dec 2023 12:40:43 +0530 Subject: [PATCH 03/18] refactor(payment_methods): add support for passing card_cvc in payment_method_data object along with token (#3024) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payments.rs | 11 ++++-- crates/router/src/core/payment_methods.rs | 6 --- crates/router/src/core/payments/helpers.rs | 43 ++++++++++++++-------- openapi/openapi_spec.json | 11 +++++- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index fe5ed417f350..49f2781a18a0 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -204,8 +204,9 @@ pub struct PaymentsRequest { #[schema(example = "187282ab-40ef-47a9-9206-5099ba31e432")] pub payment_token: Option, - /// This is used when payment is to be confirmed and the card is not saved - #[schema(value_type = Option)] + /// This is used when payment is to be confirmed and the card is not saved. + /// This field will be deprecated soon, use the CardToken object instead + #[schema(value_type = Option, deprecated)] pub card_cvc: Option>, /// The shipping address for the payment @@ -720,12 +721,16 @@ pub struct Card { pub nick_name: Option>, } -#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, Default)] #[serde(rename_all = "snake_case")] pub struct CardToken { /// The card holder's name #[schema(value_type = String, example = "John Test")] pub card_holder_name: Option>, + + /// The CVC number for the card + #[schema(value_type = Option)] + pub card_cvc: Option>, } #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 1049137a9470..a2dbfb1480c4 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -42,7 +42,6 @@ pub trait PaymentMethodRetrieve { key_store: &domain::MerchantKeyStore, token: &storage::PaymentTokenData, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult>; } @@ -126,7 +125,6 @@ impl PaymentMethodRetrieve for Oss { merchant_key_store: &domain::MerchantKeyStore, token_data: &storage::PaymentTokenData, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult> { match token_data { @@ -135,7 +133,6 @@ impl PaymentMethodRetrieve for Oss { state, &generic_token.token, payment_intent, - card_cvc, merchant_key_store, card_token_data, ) @@ -147,7 +144,6 @@ impl PaymentMethodRetrieve for Oss { state, &generic_token.token, payment_intent, - card_cvc, merchant_key_store, card_token_data, ) @@ -159,7 +155,6 @@ impl PaymentMethodRetrieve for Oss { state, &card_token.token, payment_intent, - card_cvc, card_token_data, ) .await @@ -171,7 +166,6 @@ impl PaymentMethodRetrieve for Oss { state, &card_token.token, payment_intent, - card_cvc, card_token_data, ) .await diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 4d11f6400f44..0cce91bebeeb 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1354,7 +1354,6 @@ pub async fn retrieve_payment_method_with_temporary_token( state: &AppState, token: &str, payment_intent: &PaymentIntent, - card_cvc: Option>, merchant_key_store: &domain::MerchantKeyStore, card_token_data: Option<&CardToken>, ) -> RouterResult> { @@ -1395,10 +1394,13 @@ pub async fn retrieve_payment_method_with_temporary_token( updated_card.card_holder_name = name_on_card; } - if let Some(cvc) = card_cvc { - is_card_updated = true; - updated_card.card_cvc = cvc; + if let Some(token_data) = card_token_data { + if let Some(cvc) = token_data.card_cvc.clone() { + is_card_updated = true; + updated_card.card_cvc = cvc; + } } + if is_card_updated { let updated_pm = api::PaymentMethodData::Card(updated_card); vault::Vault::store_payment_method_data_in_locker( @@ -1444,7 +1446,6 @@ pub async fn retrieve_card_with_permanent_token( state: &AppState, token: &str, payment_intent: &PaymentIntent, - card_cvc: Option>, card_token_data: Option<&CardToken>, ) -> RouterResult { let customer_id = payment_intent @@ -1479,7 +1480,11 @@ pub async fn retrieve_card_with_permanent_token( card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, - card_cvc: card_cvc.unwrap_or_default(), + card_cvc: card_token_data + .cloned() + .unwrap_or_default() + .card_cvc + .unwrap_or_default(), card_issuer: card.card_brand, nick_name: card.nick_name.map(masking::Secret::new), card_network: None, @@ -1501,6 +1506,22 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( Option, )> { let request = &payment_data.payment_method_data.clone(); + + let mut card_token_data = payment_data + .payment_method_data + .clone() + .and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }) + .or(Some(CardToken::default())); + + if let Some(cvc) = payment_data.card_cvc.clone() { + if let Some(token_data) = card_token_data.as_mut() { + token_data.card_cvc = Some(cvc); + } + } + let token = payment_data.token.clone(); let hyperswitch_token = match payment_data.mandate_id { @@ -1560,13 +1581,6 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( } }; - let card_cvc = payment_data.card_cvc.clone(); - - let card_token_data = request.as_ref().and_then(|pmd| match pmd { - api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), - _ => None, - }); - // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { @@ -1575,8 +1589,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( merchant_key_store, &hyperswitch_token, &payment_data.payment_intent, - card_cvc, - card_token_data, + card_token_data.as_ref(), ) .await .attach_printable("in 'make_pm_data'")?; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index f5ad99f05752..b1e313f15baa 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -4316,6 +4316,11 @@ "type": "string", "description": "The card holder's name", "example": "John Test" + }, + "card_cvc": { + "type": "string", + "description": "The CVC number for the card", + "nullable": true } } }, @@ -9545,7 +9550,8 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", + "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", + "deprecated": true, "nullable": true }, "shipping": { @@ -9914,7 +9920,8 @@ }, "card_cvc": { "type": "string", - "description": "This is used when payment is to be confirmed and the card is not saved", + "description": "This is used when payment is to be confirmed and the card is not saved.\nThis field will be deprecated soon, use the CardToken object instead", + "deprecated": true, "nullable": true }, "shipping": { From 9d935332193dcc9f191a0a5a9e7405316794a418 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Mon, 4 Dec 2023 14:39:33 +0530 Subject: [PATCH 04/18] feat(pm_list): Add required field for open_banking_uk for Adyen and Volt Connector (#3032) Co-authored-by: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> --- crates/router/src/configs/defaults.rs | 52 +++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index f9bfcae1ca10..db29e8180089 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4106,6 +4106,58 @@ impl Default for super::settings::RequiredFields { ( enums::PaymentMethod::BankRedirect, PaymentMethodType(HashMap::from([ + ( + enums::PaymentMethodType::OpenBankingUk, + ConnectorFields { + fields: HashMap::from([ + ( + enums::Connector::Volt, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap::from([ + ( + "billing.address.first_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.first_name".to_string(), + display_name: "billing_first_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ( + "billing.address.last_name".to_string(), + RequiredFieldInfo { + required_field: "billing.address.last_name".to_string(), + display_name: "billing_last_name".to_string(), + field_type: enums::FieldType::UserBillingName, + value: None, + } + ), + ]), + common: HashMap::new(), + } + ), + ( + enums::Connector::Adyen, + RequiredFieldFinal { + mandate: HashMap::new(), + non_mandate: HashMap:: from([ + ( + "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + RequiredFieldInfo { + required_field: "payment_method_data.bank_redirect.open_banking_uk.issuer".to_string(), + display_name: "issuer".to_string(), + field_type: enums::FieldType::UserBank, + value: None, + } + ) + ]), + common: HashMap::new(), + } + ) + ]), + }, + ), ( enums::PaymentMethodType::Przelewy24, ConnectorFields { From 80efeb76b1801529766978af1c06e2d2c7de66c0 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:03:44 +0530 Subject: [PATCH 05/18] refactor(users): Separate signup and signin (#2921) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/user.rs | 15 +- crates/api_models/src/user.rs | 40 ++++- crates/router/src/core/user.rs | 215 ++++++++++++++++++------- crates/router/src/routes/app.rs | 21 ++- crates/router/src/routes/lock_utils.rs | 5 +- crates/router/src/routes/user.rs | 59 +++++++ crates/router/src/types/domain/user.rs | 144 ++++++++++++----- crates/router/src/utils/user.rs | 57 ++++++- crates/router_env/src/logger/types.rs | 6 + 9 files changed, 453 insertions(+), 109 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 8b7cd02c9350..3634b51e0cc0 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -6,11 +6,12 @@ use crate::user::{ dashboard_metadata::{ GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest, }, - ChangePasswordRequest, ConnectAccountRequest, ConnectAccountResponse, - CreateInternalUserRequest, GetUsersResponse, SwitchMerchantIdRequest, UserMerchantCreate, + AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest, + DashboardEntryResponse, GetUsersResponse, SignUpRequest, SignUpWithMerchantIdRequest, + SwitchMerchantIdRequest, UserMerchantCreate, }; -impl ApiEventMetric for ConnectAccountResponse { +impl ApiEventMetric for DashboardEntryResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::User { merchant_id: self.merchant_id.clone(), @@ -19,9 +20,9 @@ impl ApiEventMetric for ConnectAccountResponse { } } -impl ApiEventMetric for ConnectAccountRequest {} - common_utils::impl_misc_api_event_type!( + SignUpRequest, + SignUpWithMerchantIdRequest, ChangePasswordRequest, GetMultipleMetaDataPayload, GetMetaDataResponse, @@ -30,7 +31,9 @@ common_utils::impl_misc_api_event_type!( SwitchMerchantIdRequest, CreateInternalUserRequest, UserMerchantCreate, - GetUsersResponse + GetUsersResponse, + AuthorizeResponse, + ConnectAccountRequest ); #[cfg(feature = "dummy_connector")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 36d730f5118e..287c377eb46a 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -7,13 +7,25 @@ pub mod dashboard_metadata; pub mod sample_data; #[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] -pub struct ConnectAccountRequest { +pub struct SignUpWithMerchantIdRequest { + pub name: Secret, + pub email: pii::Email, + pub password: Secret, + pub company_name: String, +} + +pub type SignUpWithMerchantIdResponse = AuthorizeResponse; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct SignUpRequest { pub email: pii::Email, pub password: Secret, } +pub type SignUpResponse = DashboardEntryResponse; + #[derive(serde::Serialize, Debug, Clone)] -pub struct ConnectAccountResponse { +pub struct DashboardEntryResponse { pub token: Secret, pub merchant_id: String, pub name: Secret, @@ -25,6 +37,28 @@ pub struct ConnectAccountResponse { pub user_id: String, } +pub type SignInRequest = SignUpRequest; + +pub type SignInResponse = DashboardEntryResponse; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct ConnectAccountRequest { + pub email: pii::Email, +} + +pub type ConnectAccountResponse = AuthorizeResponse; + +#[derive(serde::Serialize, Debug, Clone)] +pub struct AuthorizeResponse { + pub is_email_sent: bool, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub user_id: String, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub merchant_id: String, +} + #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct ChangePasswordRequest { pub new_password: Secret, @@ -36,6 +70,8 @@ pub struct SwitchMerchantIdRequest { pub merchant_id: String, } +pub type SwitchMerchantResponse = DashboardEntryResponse; + #[derive(serde::Deserialize, Debug, serde::Serialize)] pub struct CreateInternalUserRequest { pub name: Secret, diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 7d0d599cc4ed..c868530f81af 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1,10 +1,17 @@ use api_models::user as user_api; use diesel_models::{enums::UserStatus, user as storage_user}; -use error_stack::{IntoReport, ResultExt}; -use masking::{ExposeInterface, Secret}; +#[cfg(feature = "email")] +use error_stack::IntoReport; +use error_stack::ResultExt; +use masking::ExposeInterface; +#[cfg(feature = "email")] use router_env::env; +#[cfg(feature = "email")] +use router_env::logger; use super::errors::{UserErrors, UserResponse}; +#[cfg(feature = "email")] +use crate::services::email::types as email_types; use crate::{ consts, db::user::UserInterface, @@ -13,11 +20,112 @@ use crate::{ types::domain, utils, }; +pub mod dashboard_metadata; #[cfg(feature = "dummy_connector")] pub mod sample_data; -pub mod dashboard_metadata; +#[cfg(feature = "email")] +pub async fn signup_with_merchant_id( + state: AppState, + request: user_api::SignUpWithMerchantIdRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request.clone())?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + + let email_contents = email_types::ResetPassword { + recipient_email: user_from_db.get_email().try_into()?, + user_name: domain::UserName::new(user_from_db.get_name())?, + settings: state.conf.clone(), + subject: "Get back to Hyperswitch - Reset Your Password Now", + }; + + 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::AuthorizeResponse { + is_email_sent: send_email_result.is_ok(), + user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, + })) +} + +pub async fn signup( + state: AppState, + request: user_api::SignUpRequest, +) -> UserResponse { + let new_user = domain::NewUser::try_from(request)?; + new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + )) +} + +pub async fn signin( + state: AppState, + request: user_api::SignInRequest, +) -> UserResponse { + let user_from_db: domain::UserFromStorage = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await + .map_err(|e| { + if e.current_context().is_db_not_found() { + e.change_context(UserErrors::InvalidCredentials) + } else { + e.change_context(UserErrors::InternalServerError) + } + })? + .into(); + + user_from_db.compare_password(request.password)?; + + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let token = utils::user::generate_jwt_auth_token(state, &user_from_db, &user_role).await?; + + Ok(ApplicationResponse::Json( + utils::user::get_dashboard_entry_response(user_from_db, user_role, token), + )) +} + +#[cfg(feature = "email")] pub async fn connect_account( state: AppState, request: user_api::ConnectAccountRequest, @@ -29,26 +137,34 @@ pub async fn connect_account( if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); + let user_role = user_from_db.get_role_from_db(state.clone()).await?; - user_from_db.compare_password(request.password)?; + let email_contents = email_types::MagicLink { + 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: "Unlock Hyperswitch: Use Your Magic Link to Sign In", + }; + + let send_email_result = state + .email_client + .compose_and_send_email( + Box::new(email_contents), + state.conf.proxy.https_url.as_ref(), + ) + .await; - let user_role = user_from_db.get_role_from_db(state.clone()).await?; - let jwt_token = user_from_db - .get_jwt_auth_token(state.clone(), user_role.org_id) - .await?; + logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, + is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, }, )); } else if find_user + .as_ref() .map_err(|e| e.current_context().is_db_not_found()) .err() .unwrap_or(false) @@ -73,46 +189,35 @@ pub async fn connect_account( UserStatus::Active, ) .await?; - let jwt_token = user_from_db - .get_jwt_auth_token(state.clone(), user_role.org_id) - .await?; - - #[cfg(feature = "email")] - { - use router_env::logger; - - use crate::services::email::types as email_types; - - let email_contents = email_types::VerifyEmail { - recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, - settings: state.conf.clone(), - subject: "Welcome to the 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; + let email_contents = email_types::VerifyEmail { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + settings: state.conf.clone(), + subject: "Welcome to the 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); - } + logger::info!(?send_email_result); return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { - token: Secret::new(jwt_token), - merchant_id: user_role.merchant_id, - name: user_from_db.get_name(), - email: user_from_db.get_email(), - verification_days_left: None, - user_role: user_role.role_id, + is_email_sent: send_email_result.is_ok(), user_id: user_from_db.get_user_id().to_string(), + merchant_id: user_role.merchant_id, }, )); } else { - Err(UserErrors::InternalServerError.into()) + Err(find_user + .err() + .map(|e| e.change_context(UserErrors::InternalServerError)) + .unwrap_or(UserErrors::InternalServerError.into())) } } @@ -215,7 +320,7 @@ pub async fn switch_merchant_id( state: AppState, request: user_api::SwitchMerchantIdRequest, user_from_token: auth::UserFromToken, -) -> UserResponse { +) -> UserResponse { if !utils::user_role::is_internal_role(&user_from_token.role_id) { let merchant_list = utils::user_role::get_merchant_ids_for_user(state.clone(), &user_from_token.user_id) @@ -252,7 +357,7 @@ pub async fn switch_merchant_id( } })?; - let org_id = state + let _org_id = state .store .find_merchant_account_by_merchant_id(request.merchant_id.as_str(), &key_store) .await @@ -272,23 +377,23 @@ pub async fn switch_merchant_id( .await .change_context(UserErrors::InternalServerError)?; - let token = Box::pin(user.get_jwt_auth_token_with_custom_merchant_id( - state.clone(), + let token = utils::user::generate_jwt_auth_token_with_custom_merchant_id( + state, + &user, + &user_role, request.merchant_id.clone(), - org_id, - )) - .await? - .into(); + ) + .await?; Ok(ApplicationResponse::Json( - user_api::ConnectAccountResponse { - merchant_id: request.merchant_id, + user_api::SwitchMerchantResponse { token, name: user.get_name(), email: user.get_email(), user_id: user.get_user_id().to_string(), verification_days_left: None, user_role: user_role.role_id, + merchant_id: user_role.merchant_id, }, )) } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index a145f3e7e5d7..88806b565d3a 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -823,10 +823,7 @@ impl User { let mut route = web::scope("/user").app_data(web::Data::new(state)); route = route - .service(web::resource("/signin").route(web::post().to(user_connect_account))) - .service(web::resource("/signup").route(web::post().to(user_connect_account))) - .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) - .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/signin").route(web::post().to(user_signin))) .service(web::resource("/change_password").route(web::post().to(change_password))) .service( web::resource("/data/merchant") @@ -841,7 +838,6 @@ impl User { ) .service(web::resource("/switch/list").route(web::get().to(list_merchant_ids_for_user))) .service(web::resource("/user/list").route(web::get().to(get_user_details))) - // User Role APIs .service(web::resource("/permission_info").route(web::get().to(get_authorization_info))) .service(web::resource("/user/update_role").route(web::post().to(update_user_role))) .service(web::resource("/role/list").route(web::get().to(list_roles))) @@ -855,6 +851,21 @@ impl User { .route(web::delete().to(delete_sample_data)), ) } + #[cfg(feature = "email")] + { + route = route + .service( + web::resource("/connect_account").route(web::post().to(user_connect_account)), + ) + .service( + web::resource("/signup_with_merchant_id") + .route(web::post().to(user_signup_with_merchant_id)), + ); + } + #[cfg(not(feature = "email"))] + { + route = route.service(web::resource("/signup").route(web::post().to(user_signup))) + } route } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 6aa2bbad0b15..b32dbe3d4b6a 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -149,6 +149,8 @@ impl From for ApiIdentifier { | Flow::GsmRuleDelete => Self::Gsm, Flow::UserConnectAccount + | Flow::UserSignUp + | Flow::UserSignIn | Flow::ChangePassword | Flow::SetDashboardMetadata | Flow::GetMutltipleDashboardMetadata @@ -159,7 +161,8 @@ impl From for ApiIdentifier { | Flow::GenerateSampleData | Flow::DeleteSampleData | Flow::UserMerchantAccountList - | Flow::GetUserDetails => Self::User, + | Flow::GetUserDetails + | Flow::UserSignUpWithMerchantId => Self::User, Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index 97bd7054da9e..45fa0ba35c59 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -19,6 +19,65 @@ use crate::{ utils::user::dashboard_metadata::{parse_string_to_enums, set_ip_address_if_required}, }; +#[cfg(feature = "email")] +pub async fn user_signup_with_merchant_id( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUpWithMerchantId; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup_with_merchant_id(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signup( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignUp; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signup(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn user_signin( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserSignIn; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user_core::signin(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "email")] pub async fn user_connect_account( state: web::Data, http_req: HttpRequest, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 082b29d80941..592195922493 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -26,7 +26,7 @@ use crate::{ db::StorageInterface, routes::AppState, services::{ - authentication::{AuthToken, UserFromToken}, + authentication::UserFromToken, authorization::{info, predefined_permissions}, }, types::transformers::ForeignFrom, @@ -215,6 +215,25 @@ impl NewUserOrganization { } } +impl TryFrom for NewUserOrganization { + type Error = error_stack::Report; + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let new_organization = api_org::OrganizationNew::new(Some( + UserCompanyName::new(value.company_name)?.get_secret(), + )); + let db_organization = ForeignFrom::foreign_from(new_organization); + Ok(Self(db_organization)) + } +} + +impl From for NewUserOrganization { + fn from(_value: user_api::SignUpRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + impl From for NewUserOrganization { fn from(_value: user_api::ConnectAccountRequest) -> Self { let new_organization = api_org::OrganizationNew::new(None); @@ -334,6 +353,24 @@ impl NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + let merchant_id = MerchantId::new(format!( + "merchant_{}", + common_utils::date_time::now_unix_timestamp() + ))?; + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + impl TryFrom for NewUserMerchant { type Error = error_stack::Report; @@ -352,6 +389,21 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let company_name = Some(UserCompanyName::new(value.company_name.clone())?); + let merchant_id = MerchantId::new(value.company_name.clone())?; + let new_organization = NewUserOrganization::try_from(value)?; + + Ok(Self { + company_name, + merchant_id, + new_organization, + }) + } +} + impl TryFrom for NewUserMerchant { type Error = error_stack::Report; @@ -434,10 +486,23 @@ impl NewUser { .attach_printable("Error while inserting user") } + pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> { + if state + .store + .find_user_by_email(self.get_email().into_inner().expose().expose().as_str()) + .await + .is_ok() + { + return Err(UserErrors::UserExists).into_report(); + } + Ok(()) + } + pub async fn insert_user_and_merchant_in_db( &self, state: AppState, ) -> UserResult { + self.check_if_already_exists_in_db(state.clone()).await?; let db = state.store.as_ref(); let merchant_id = self.get_new_merchant().get_merchant_id(); self.new_merchant @@ -495,6 +560,46 @@ impl TryFrom for storage_user::UserNew { } } +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { + let email = value.email.clone().try_into()?; + let name = UserName::new(value.name.clone())?; + let password = UserPassword::new(value.password.clone())?; + let user_id = uuid::Uuid::new_v4().to_string(); + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + name, + email, + password, + user_id, + new_merchant, + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::SignUpRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::try_from(value.email.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + impl TryFrom for NewUser { type Error = error_stack::Report; @@ -502,7 +607,7 @@ impl TryFrom for NewUser { let user_id = uuid::Uuid::new_v4().to_string(); let email = value.email.clone().try_into()?; let name = UserName::try_from(value.email.clone())?; - let password = UserPassword::new(value.password.clone())?; + let password = UserPassword::new(uuid::Uuid::new_v4().to_string().into())?; let new_merchant = NewUserMerchant::try_from(value)?; Ok(Self { @@ -582,41 +687,6 @@ impl UserFromStorage { self.0.email.clone() } - pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult { - let role_id = self.get_role_from_db(state.clone()).await?.role_id; - let merchant_id = state - .store - .find_user_role_by_user_id(self.get_user_id()) - .await - .change_context(UserErrors::InternalServerError)? - .merchant_id; - AuthToken::new_token( - self.0.user_id.clone(), - merchant_id, - role_id, - &state.conf, - org_id, - ) - .await - } - - pub async fn get_jwt_auth_token_with_custom_merchant_id( - &self, - state: AppState, - merchant_id: String, - org_id: String, - ) -> UserResult { - let role_id = self.get_role_from_db(state.clone()).await?.role_id; - AuthToken::new_token( - self.0.user_id.clone(), - merchant_id, - role_id, - &state.conf, - org_id, - ) - .await - } - pub async fn get_role_from_db(&self, state: AppState) -> UserResult { state .store diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 696aa4090044..0403d9b453d0 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -1,11 +1,13 @@ -use diesel_models::enums::UserStatus; +use api_models::user as user_api; +use diesel_models::{enums::UserStatus, user_role::UserRole}; use error_stack::ResultExt; +use masking::Secret; use crate::{ core::errors::{UserErrors, UserResult}, routes::AppState, - services::authentication::UserFromToken, - types::domain::MerchantAccount, + services::authentication::{AuthToken, UserFromToken}, + types::domain::{MerchantAccount, UserFromStorage}, }; pub mod dashboard_metadata; @@ -68,3 +70,52 @@ pub async fn get_merchant_ids_for_user(state: AppState, user_id: &str) -> UserRe }) .collect()) } + +pub async fn generate_jwt_auth_token( + state: AppState, + user: &UserFromStorage, + user_role: &UserRole, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + user_role.merchant_id.clone(), + user_role.role_id.clone(), + &state.conf, + user_role.org_id.clone(), + ) + .await?; + Ok(Secret::new(token)) +} + +pub async fn generate_jwt_auth_token_with_custom_merchant_id( + state: AppState, + user: &UserFromStorage, + user_role: &UserRole, + merchant_id: String, +) -> UserResult> { + let token = AuthToken::new_token( + user.get_user_id().to_string(), + merchant_id, + user_role.role_id.clone(), + &state.conf, + user_role.org_id.to_owned(), + ) + .await?; + Ok(Secret::new(token)) +} + +pub fn get_dashboard_entry_response( + user: UserFromStorage, + user_role: UserRole, + token: Secret, +) -> user_api::DashboardEntryResponse { + user_api::DashboardEntryResponse { + merchant_id: user_role.merchant_id, + token, + name: user.get_name(), + email: user.get_email(), + user_id: user.get_user_id().to_string(), + verification_days_left: None, + user_role: user_role.role_id, + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index f54a5a82baaf..606ae1f2f169 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -249,6 +249,12 @@ pub enum Flow { GsmRuleUpdate, /// Gsm Rule Delete flow GsmRuleDelete, + /// User Sign Up + UserSignUp, + /// User Sign Up + UserSignUpWithMerchantId, + /// User Sign In + UserSignIn, /// User connect account UserConnectAccount, /// Upsert Decision Manager Config From 57591f819c7994099e76cff1affc7bcf3e45a031 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:11:30 +0530 Subject: [PATCH 06/18] refactor: create separate struct for surcharge details response (#3027) --- crates/api_models/src/payment_methods.rs | 120 +------------ crates/api_models/src/payments.rs | 12 -- .../src/surcharge_decision_configs.rs | 10 +- crates/common_utils/src/types.rs | 15 +- crates/diesel_models/src/payment_intent.rs | 1 - .../surcharge_decision_configs.rs | 105 +++++++----- crates/router/src/core/payments.rs | 11 +- crates/router/src/core/payments/helpers.rs | 40 ++++- .../payments/operations/payment_create.rs | 8 +- .../payments/operations/payment_update.rs | 20 ++- crates/router/src/core/payments/types.rs | 158 +++++++++++++++++- crates/router/src/core/utils.rs | 22 ++- crates/router/src/types.rs | 8 +- crates/router/src/types/api.rs | 12 +- 14 files changed, 325 insertions(+), 217 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index dfb8e8999771..3343becaaae6 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -2,8 +2,10 @@ use std::collections::HashMap; use cards::CardNumber; use common_utils::{ - consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, pii, - types::Percentage, + consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, + crypto::OptionalEncryptableName, + pii, + types::{Percentage, Surcharge}, }; use serde::de; use utoipa::ToSchema; @@ -14,7 +16,7 @@ use crate::{ admin, customers::CustomerId, enums as api_enums, - payments::{self, BankCodeResponse, RequestSurchargeDetails}, + payments::{self, BankCodeResponse}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -337,117 +339,11 @@ pub struct SurchargeDetailsResponse { /// tax on surcharge value pub tax_on_surcharge: Option>, /// surcharge amount for this payment - pub surcharge_amount: i64, + pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment - pub tax_on_surcharge_amount: i64, + pub display_tax_on_surcharge_amount: f64, /// sum of original amount, - pub final_amount: i64, -} - -impl SurchargeDetailsResponse { - pub fn is_request_surcharge_matching( - &self, - request_surcharge_details: RequestSurchargeDetails, - ) -> bool { - request_surcharge_details.surcharge_amount == self.surcharge_amount - && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount - } - pub fn get_total_surcharge_amount(&self) -> i64 { - self.surcharge_amount + self.tax_on_surcharge_amount - } -} - -#[derive(Clone, Debug)] -pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetailsResponse, - >, - pub payment_attempt_id: String, -} - -impl SurchargeMetadata { - pub fn new(payment_attempt_id: String) -> Self { - Self { - surcharge_results: HashMap::new(), - payment_attempt_id, - } - } - pub fn is_empty_result(&self) -> bool { - self.surcharge_results.is_empty() - } - pub fn get_surcharge_results_size(&self) -> usize { - self.surcharge_results.len() - } - pub fn insert_surcharge_details( - &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - surcharge_details: SurchargeDetailsResponse, - ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); - } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetailsResponse> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) - } - pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { - format!("surcharge_metadata_{}", payment_attempt_id) - } - pub fn get_individual_surcharge_key_value_pairs( - &self, - ) -> Vec<(String, SurchargeDetailsResponse)> { - self.surcharge_results - .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); - (key, surcharge_details.to_owned()) - }) - .collect() - } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) - } - } -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] -#[serde(rename_all = "snake_case", tag = "type", content = "value")] -pub enum Surcharge { - /// Fixed Surcharge value - Fixed(i64), - /// Surcharge percentage - Rate(Percentage), + pub display_final_amount: f64, } /// Required fields info used while listing the payment_method_data diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 49f2781a18a0..eaf0937ef2a2 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -16,7 +16,6 @@ use crate::{ admin, disputes, enums::{self as api_enums}, ephemeral_key::EphemeralKeyCreateResponse, - payment_methods::{Surcharge, SurchargeDetailsResponse}, refunds, }; @@ -340,17 +339,6 @@ impl RequestSurchargeDetails { pub fn is_surcharge_zero(&self) -> bool { self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 } - pub fn get_surcharge_details_object(&self, original_amount: i64) -> SurchargeDetailsResponse { - let surcharge_amount = self.surcharge_amount; - let tax_on_surcharge_amount = self.tax_amount.unwrap_or(0); - SurchargeDetailsResponse { - surcharge: Surcharge::Fixed(self.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount, - tax_on_surcharge_amount, - final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, - } - } pub fn get_total_surcharge_amount(&self) -> i64 { self.surcharge_amount + self.tax_amount.unwrap_or(0) } diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs index 3ebf8f42744e..7ead27945584 100644 --- a/crates/api_models/src/surcharge_decision_configs.rs +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -7,21 +7,21 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub struct SurchargeDetails { - pub surcharge: Surcharge, +pub struct SurchargeDetailsOutput { + pub surcharge: SurchargeOutput, pub tax_on_surcharge: Option>, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case", tag = "type", content = "value")] -pub enum Surcharge { - Fixed(i64), +pub enum SurchargeOutput { + Fixed { amount: i64 }, Rate(Percentage), } #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct SurchargeDecisionConfigs { - pub surcharge_details: Option, + pub surcharge_details: Option, } impl EuclidDirFilter for SurchargeDecisionConfigs { const ALLOWED: &'static [DirKeyKind] = &[ diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 111f0f43c0f2..cf94f2fe26ce 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -2,7 +2,10 @@ use error_stack::{IntoReport, ResultExt}; use serde::{de::Visitor, Deserialize, Deserializer}; -use crate::errors::{CustomResult, PercentageError}; +use crate::{ + consts, + errors::{CustomResult, PercentageError}, +}; /// Represents Percentage Value between 0 and 100 both inclusive #[derive(Clone, Default, Debug, PartialEq, serde::Serialize)] @@ -136,3 +139,13 @@ impl<'de, const PRECISION: u8> Deserialize<'de> for Percentage { data.deserialize_map(PercentageVisitor:: {}) } } + +/// represents surcharge type and value +#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(Percentage<{ consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH }>), +} diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 8d752466103e..40d8fd92caeb 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -106,7 +106,6 @@ pub struct PaymentIntentNew { pub merchant_decision: Option, pub payment_link_id: Option, pub payment_confirm_source: Option, - pub updated_by: String, pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 9a65ec76f2a5..38ae71754b87 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -1,12 +1,10 @@ use api_models::{ - payment_methods::{self, SurchargeDetailsResponse, SurchargeMetadata}, + payment_methods::SurchargeDetailsResponse, payments::Address, routing, - surcharge_decision_configs::{ - self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord, SurchargeDetails, - }, + surcharge_decision_configs::{self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord}, }; -use common_utils::{ext_traits::StringExt, static_cache::StaticCache}; +use common_utils::{ext_traits::StringExt, static_cache::StaticCache, types as common_utils_types}; use error_stack::{self, IntoReport, ResultExt}; use euclid::{ backend, @@ -14,7 +12,11 @@ use euclid::{ }; use router_env::{instrument, tracing}; -use crate::{core::payments::PaymentData, db::StorageInterface, types::storage as oss_storage}; +use crate::{ + core::payments::{types, PaymentData}, + db::StorageInterface, + types::{storage as oss_storage, transformers::ForeignTryFrom}, +}; static CONF_CACHE: StaticCache = StaticCache::new(); use crate::{ core::{ @@ -55,10 +57,10 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( billing_address: Option
, response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled], ) -> ConditionalConfigResult<( - SurchargeMetadata, + types::SurchargeMetadata, surcharge_decision_configs::MerchantSurchargeConfigs, )> { - let mut surcharge_metadata = SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { id } else { @@ -101,20 +103,27 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( Some(card_network_type.card_network.clone()); let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + // let surcharge_details = card_network_type.surcharge_details = surcharge_output .surcharge_details .map(|surcharge_details| { - get_surcharge_details_response(surcharge_details, payment_attempt).map( - |surcharge_details_response| { - surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - Some(&card_network_type.card_network), - surcharge_details_response.clone(), - ); - surcharge_details_response - }, - ) + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + Some(&card_network_type.card_network), + surcharge_details.clone(), + ); + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") }) .transpose()?; } @@ -124,17 +133,23 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_method_type_response.surcharge_details = surcharge_output .surcharge_details .map(|surcharge_details| { - get_surcharge_details_response(surcharge_details, payment_attempt).map( - |surcharge_details_response| { - surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - None, - surcharge_details_response.clone(), - ); - surcharge_details_response - }, - ) + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + &payment_methods_enabled.payment_method, + &payment_method_type_response.payment_method_type, + None, + surcharge_details.clone(), + ); + SurchargeDetailsResponse::foreign_try_from(( + &surcharge_details, + payment_attempt, + )) + .into_report() + .change_context(ConfigError::DslExecutionError) + .attach_printable("Error while constructing Surcharge response type") }) .transpose()?; } @@ -148,12 +163,12 @@ pub async fn perform_surcharge_decision_management_for_session_flow( algorithm_ref: routing::RoutingAlgorithmRef, payment_data: &mut PaymentData, payment_method_type_list: &Vec, -) -> ConditionalConfigResult +) -> ConditionalConfigResult where O: Send + Clone, { let mut surcharge_metadata = - SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); + types::SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone()); let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { id } else { @@ -186,8 +201,10 @@ where let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; if let Some(surcharge_details) = surcharge_output.surcharge_details { - let surcharge_details_response = - get_surcharge_details_response(surcharge_details, &payment_data.payment_attempt)?; + let surcharge_details_response = get_surcharge_details_from_surcharge_output( + surcharge_details, + &payment_data.payment_attempt, + )?; surcharge_metadata.insert_surcharge_details( &payment_method_type.to_owned().into(), payment_method_type, @@ -199,13 +216,13 @@ where Ok(surcharge_metadata) } -fn get_surcharge_details_response( - surcharge_details: SurchargeDetails, +fn get_surcharge_details_from_surcharge_output( + surcharge_details: surcharge_decision_configs::SurchargeDetailsOutput, payment_attempt: &oss_storage::PaymentAttempt, -) -> ConditionalConfigResult { +) -> ConditionalConfigResult { let surcharge_amount = match surcharge_details.surcharge.clone() { - surcharge_decision_configs::Surcharge::Fixed(value) => value, - surcharge_decision_configs::Surcharge::Rate(percentage) => percentage + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => amount, + surcharge_decision_configs::SurchargeOutput::Rate(percentage) => percentage .apply_and_ceil_result(payment_attempt.amount) .change_context(ConfigError::DslExecutionError) .attach_printable("Failed to Calculate surcharge amount by applying percentage")?, @@ -221,13 +238,13 @@ fn get_surcharge_details_response( }) .transpose()? .unwrap_or(0); - Ok(SurchargeDetailsResponse { + Ok(types::SurchargeDetails { surcharge: match surcharge_details.surcharge { - surcharge_decision_configs::Surcharge::Fixed(surcharge_amount) => { - payment_methods::Surcharge::Fixed(surcharge_amount) + surcharge_decision_configs::SurchargeOutput::Fixed { amount } => { + common_utils_types::Surcharge::Fixed(amount) } - surcharge_decision_configs::Surcharge::Rate(percentage) => { - payment_methods::Surcharge::Rate(percentage) + surcharge_decision_configs::SurchargeOutput::Rate(percentage) => { + common_utils_types::Surcharge::Rate(percentage) } }, tax_on_surcharge: surcharge_details.tax_on_surcharge, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 33afa29397e1..67b1dbcc98b2 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -15,10 +15,9 @@ use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoI use api_models::{ self, enums, - payment_methods::{Surcharge, SurchargeDetailsResponse}, payments::{self, HeaderPayload}, }; -use common_utils::{ext_traits::AsyncExt, pii}; +use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; use data_models::mandates::MandateData; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; use error_stack::{IntoReport, ResultExt}; @@ -42,6 +41,7 @@ use self::{ helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, + types::SurchargeDetails, }; use super::{ errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, @@ -475,8 +475,7 @@ where .payment_attempt .get_surcharge_details() .map(|surcharge_details| { - surcharge_details - .get_surcharge_details_object(payment_data.payment_attempt.amount) + SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)) }); payment_data.surcharge_details = surcharge_details; } @@ -509,7 +508,7 @@ where let final_amount = payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( - SurchargeDetailsResponse { + SurchargeDetails { surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -1882,7 +1881,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, } diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 0cce91bebeeb..59b2b53c654d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use api_models::payments::{CardToken, GetPaymentMethodType}; +use api_models::payments::{CardToken, GetPaymentMethodType, RequestSurchargeDetails}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -572,6 +572,7 @@ pub fn validate_merchant_id( pub fn validate_request_amount_and_amount_to_capture( op_amount: Option, op_amount_to_capture: Option, + surcharge_details: Option, ) -> CustomResult<(), errors::ApiErrorResponse> { match (op_amount, op_amount_to_capture) { (None, _) => Ok(()), @@ -581,7 +582,11 @@ pub fn validate_request_amount_and_amount_to_capture( api::Amount::Value(amount_inner) => { // If both amount and amount to capture is present // then amount to be capture should be less than or equal to request amount - utils::when(!amount_to_capture.le(&amount_inner.get()), || { + let total_capturable_amount = amount_inner.get() + + surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .unwrap_or(0); + utils::when(!amount_to_capture.le(&total_capturable_amount), || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { message: format!( "amount_to_capture is greater than amount capture_amount: {amount_to_capture:?} request_amount: {amount:?}" @@ -603,13 +608,34 @@ pub fn validate_request_amount_and_amount_to_capture( /// if capture method = automatic, amount_to_capture(if provided) must be equal to amount #[instrument(skip_all)] -pub fn validate_amount_to_capture_in_create_call_request( +pub fn validate_amount_to_capture_and_capture_method( + payment_attempt: Option<&PaymentAttempt>, request: &api_models::payments::PaymentsRequest, ) -> CustomResult<(), errors::ApiErrorResponse> { - if request.capture_method.unwrap_or_default() == api_enums::CaptureMethod::Automatic { - let total_capturable_amount = request.get_total_capturable_amount(); - if let Some((amount_to_capture, total_capturable_amount)) = - request.amount_to_capture.zip(total_capturable_amount) + let capture_method = request + .capture_method + .or(payment_attempt + .map(|payment_attempt| payment_attempt.capture_method.unwrap_or_default())) + .unwrap_or_default(); + if capture_method == api_enums::CaptureMethod::Automatic { + let original_amount = request + .amount + .map(|amount| amount.into()) + .or(payment_attempt.map(|payment_attempt| payment_attempt.amount)); + let surcharge_amount = request + .surcharge_details + .map(|surcharge_details| surcharge_details.get_total_surcharge_amount()) + .or_else(|| { + payment_attempt.map(|payment_attempt| { + payment_attempt.surcharge_amount.unwrap_or(0) + + payment_attempt.tax_amount.unwrap_or(0) + }) + }) + .unwrap_or(0); + let total_capturable_amount = + original_amount.map(|original_amount| original_amount + surcharge_amount); + if let Some((total_capturable_amount, amount_to_capture)) = + total_capturable_amount.zip(request.amount_to_capture) { utils::when(amount_to_capture != total_capturable_amount, || { Err(report!(errors::ApiErrorResponse::PreconditionFailed { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index ac387076d1d1..bb7d0a931e1b 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -286,8 +286,8 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(MandateData::from); - let surcharge_details = request.surcharge_details.map(|surcharge_details| { - surcharge_details.get_surcharge_details_object(payment_attempt.amount) + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { + payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { @@ -540,14 +540,14 @@ impl ValidateRequest &[ storage_enums::IntentStatus::Failed, storage_enums::IntentStatus::Succeeded, + storage_enums::IntentStatus::PartiallyCaptured, storage_enums::IntentStatus::RequiresCapture, ], "update", @@ -134,6 +135,20 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + helpers::validate_amount_to_capture_and_capture_method(Some(&payment_attempt), request)?; + + helpers::validate_request_amount_and_amount_to_capture( + request.amount, + request.amount_to_capture, + request + .surcharge_details + .or(payment_attempt.get_surcharge_details()), + ) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "amount_to_capture".to_string(), + expected_format: "amount_to_capture lesser than or equal to amount".to_string(), + })?; + currency = request .currency .or(payment_attempt.currency) @@ -322,7 +337,7 @@ impl })?; let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) + payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { @@ -629,6 +644,7 @@ impl ValidateRequest>, + /// surcharge amount for this payment + pub surcharge_amount: i64, + /// tax on surcharge amount for this payment + pub tax_on_surcharge_amount: i64, + /// sum of original amount, + pub final_amount: i64, +} + +impl From<(&RequestSurchargeDetails, &PaymentAttempt)> for SurchargeDetails { + fn from( + (request_surcharge_details, payment_attempt): (&RequestSurchargeDetails, &PaymentAttempt), + ) -> Self { + let surcharge_amount = request_surcharge_details.surcharge_amount; + let tax_on_surcharge_amount = request_surcharge_details.tax_amount.unwrap_or(0); + Self { + surcharge: common_types::Surcharge::Fixed(request_surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount, + } + } +} + +impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsResponse { + type Error = TryFromIntError; + fn foreign_try_from( + (surcharge_details, payment_attempt): (&SurchargeDetails, &PaymentAttempt), + ) -> Result { + let currency = payment_attempt.currency.unwrap_or_default(); + let display_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.surcharge_amount)?; + let display_tax_on_surcharge_amount = + currency.to_currency_base_unit_asf64(surcharge_details.tax_on_surcharge_amount)?; + let display_final_amount = + currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; + Ok(Self { + surcharge: surcharge_details.surcharge.clone(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), + display_surcharge_amount, + display_tax_on_surcharge_amount, + display_final_amount, + }) + } +} + +impl SurchargeDetails { + pub fn is_request_surcharge_matching( + &self, + request_surcharge_details: RequestSurchargeDetails, + ) -> bool { + request_surcharge_details.surcharge_amount == self.surcharge_amount + && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount + } + pub fn get_total_surcharge_amount(&self) -> i64 { + self.surcharge_amount + self.tax_on_surcharge_amount + } +} + +#[derive(Clone, Debug)] +pub struct SurchargeMetadata { + surcharge_results: HashMap< + ( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), + SurchargeDetails, + >, + pub payment_attempt_id: String, +} + +impl SurchargeMetadata { + pub fn new(payment_attempt_id: String) -> Self { + Self { + surcharge_results: HashMap::new(), + payment_attempt_id, + } + } + pub fn is_empty_result(&self) -> bool { + self.surcharge_results.is_empty() + } + pub fn get_surcharge_results_size(&self) -> usize { + self.surcharge_results.len() + } + pub fn insert_surcharge_details( + &mut self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + surcharge_details: SurchargeDetails, + ) { + let key = ( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.insert(key, surcharge_details); + } + pub fn get_surcharge_details( + &self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> Option<&SurchargeDetails> { + let key = &( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.get(key) + } + pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { + format!("surcharge_metadata_{}", payment_attempt_id) + } + pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { + self.surcharge_results + .iter() + .map(|((pm, pmt, card_network), surcharge_details)| { + let key = + Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + (key, surcharge_details.to_owned()) + }) + .collect() + } + pub fn get_surcharge_details_redis_hashset_key( + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> String { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 670c25c814ed..6d82c44d803a 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,9 +1,6 @@ use std::{marker::PhantomData, str::FromStr}; -use api_models::{ - enums::{DisputeStage, DisputeStatus}, - payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, -}; +use api_models::enums::{DisputeStage, DisputeStatus}; use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; @@ -17,7 +14,7 @@ use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; -use super::payments::{helpers, PaymentAddress}; +use super::payments::{helpers, types as payments_types, PaymentAddress}; #[cfg(feature = "payouts")] use super::payouts::PayoutData; #[cfg(feature = "payouts")] @@ -1075,7 +1072,7 @@ pub fn get_flow_name() -> RouterResult { pub async fn persist_individual_surcharge_details_in_redis( state: &AppState, merchant_account: &domain::MerchantAccount, - surcharge_metadata: &SurchargeMetadata, + surcharge_metadata: &payments_types::SurchargeMetadata, ) -> RouterResult<()> { if !surcharge_metadata.is_empty_result() { let redis_conn = state @@ -1083,7 +1080,7 @@ pub async fn persist_individual_surcharge_details_in_redis( .get_redis_conn() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key( + let redis_key = payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key( &surcharge_metadata.payment_attempt_id, ); @@ -1094,7 +1091,7 @@ pub async fn persist_individual_surcharge_details_in_redis( { value_list.push(( key, - Encode::::encode_to_string_of_json(&value) + Encode::::encode_to_string_of_json(&value) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to encode to string of json")?, )); @@ -1118,20 +1115,21 @@ pub async fn get_individual_surcharge_detail_from_redis( payment_method_type: &euclid_enums::PaymentMethodType, card_network: Option, payment_attempt_id: &str, -) -> CustomResult { +) -> CustomResult { let redis_conn = state .store .get_redis_conn() .attach_printable("Failed to get redis connection")?; - let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = SurchargeMetadata::get_surcharge_details_redis_hashset_key( + let redis_key = + payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = payments_types::SurchargeMetadata::get_surcharge_details_redis_hashset_key( payment_method, payment_method_type, card_network.as_ref(), ); redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") .await } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index c267a54cc57b..595f487e7079 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -30,7 +30,7 @@ use crate::core::utils::IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_DISPUTE_FLO use crate::{ core::{ errors::{self, RouterResult}, - payments::{PaymentData, RecurringMandatePaymentData}, + payments::{types, PaymentData, RecurringMandatePaymentData}, }, services, types::{storage::payment_attempt::PaymentAttemptExt, transformers::ForeignFrom}, @@ -379,7 +379,7 @@ pub struct PaymentsAuthorizeData { pub related_transaction_id: Option, pub payment_experience: Option, pub payment_method_type: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub customer_id: Option, pub request_incremental_authorization: bool, } @@ -441,7 +441,7 @@ pub struct PaymentsPreProcessingData { pub router_return_url: Option, pub webhook_url: Option, pub complete_authorize_url: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub browser_info: Option, pub connector_transaction_id: Option, } @@ -517,7 +517,7 @@ pub struct PaymentsSessionData { pub amount: i64, pub currency: storage_enums::Currency, pub country: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub order_details: Option>, } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 96bcaca3ed5d..ea2ea8b701da 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -19,7 +19,6 @@ pub mod webhooks; use std::{fmt::Debug, str::FromStr}; -use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ @@ -30,7 +29,10 @@ use super::ErrorResponse; use crate::{ configs::settings::Connectors, connector, consts, - core::errors::{self, CustomResult}, + core::{ + errors::{self, CustomResult}, + payments::types as payments_types, + }, services::{request, ConnectorIntegration, ConnectorRedirectResponse, ConnectorValidation}, types::{self, api::enums as api_enums}, }; @@ -222,9 +224,9 @@ pub struct SessionConnectorData { /// Session Surcharge type pub enum SessionSurchargeDetails { /// Surcharge is calculated by hyperswitch - Calculated(SurchargeMetadata), + Calculated(payments_types::SurchargeMetadata), /// Surcharge is sent by merchant - PreDetermined(SurchargeDetailsResponse), + PreDetermined(payments_types::SurchargeDetails), } impl SessionSurchargeDetails { @@ -233,7 +235,7 @@ impl SessionSurchargeDetails { payment_method: &enums::PaymentMethod, payment_method_type: &enums::PaymentMethodType, card_network: Option<&enums::CardNetwork>, - ) -> Option { + ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata .get_surcharge_details(payment_method, payment_method_type, card_network) From 9274cefbdd29d2ac64baeea2fe504dff2472cb47 Mon Sep 17 00:00:00 2001 From: Prasunna Soppa <70575890+prasunna09@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:49:15 +0530 Subject: [PATCH 07/18] fix(pm_list): [Trustpay]Update dynamic fields for trustpay blik (#3042) --- crates/router/src/configs/defaults.rs | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index db29e8180089..d529ae034a86 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -4293,24 +4293,6 @@ impl Default for super::settings::RequiredFields { mandate: HashMap::new(), non_mandate: HashMap::new(), common: HashMap::from([ - ( - "billing.address.first_name".to_string(), - RequiredFieldInfo { - required_field: "billing.address.first_name".to_string(), - display_name: "billing_first_name".to_string(), - field_type: enums::FieldType::UserBillingName, - value: None, - } - ), - ( - "billing.address.last_name".to_string(), - RequiredFieldInfo { - required_field: "billing.address.last_name".to_string(), - display_name: "billing_last_name".to_string(), - field_type: enums::FieldType::UserBillingName, - value: None, - } - ), ( "email".to_string(), RequiredFieldInfo { @@ -4324,7 +4306,7 @@ impl Default for super::settings::RequiredFields { "billing.address.first_name".to_string(), RequiredFieldInfo { required_field: "billing.address.first_name".to_string(), - display_name: "card_holder_name".to_string(), + display_name: "billing_first_name".to_string(), field_type: enums::FieldType::UserBillingName, value: None, } @@ -4333,7 +4315,7 @@ impl Default for super::settings::RequiredFields { "billing.address.last_name".to_string(), RequiredFieldInfo { required_field: "billing.address.last_name".to_string(), - display_name: "card_holder_name".to_string(), + display_name: "billing_last_name".to_string(), field_type: enums::FieldType::UserBillingName, value: None, } From a0cfdd3fb12f04b603f65551eac985c31e08da85 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:34:51 +0530 Subject: [PATCH 08/18] feat(router): add payments incremental authorization api (#3038) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/events/payment.rs | 13 +- crates/api_models/src/payments.rs | 36 ++ crates/common_enums/src/enums.rs | 26 ++ crates/common_utils/src/request.rs | 1 + crates/data_models/src/payments.rs | 1 + .../src/payments/payment_attempt.rs | 4 + .../src/payments/payment_intent.rs | 18 + crates/diesel_models/src/authorization.rs | 78 +++++ crates/diesel_models/src/lib.rs | 1 + crates/diesel_models/src/payment_attempt.rs | 12 + crates/diesel_models/src/payment_intent.rs | 21 ++ crates/diesel_models/src/query.rs | 1 + .../diesel_models/src/query/authorization.rs | 79 +++++ crates/diesel_models/src/schema.rs | 27 ++ crates/router/src/connector/cybersource.rs | 116 ++++++- .../src/connector/cybersource/transformers.rs | 152 +++++++- crates/router/src/core/payments.rs | 19 +- crates/router/src/core/payments/flows.rs | 80 +++++ .../flows/incremental_authorization_flow.rs | 118 +++++++ crates/router/src/core/payments/helpers.rs | 17 + crates/router/src/core/payments/operations.rs | 2 + .../payments/operations/payment_approve.rs | 2 + .../payments/operations/payment_cancel.rs | 2 + .../payments/operations/payment_capture.rs | 2 + .../operations/payment_complete_authorize.rs | 2 + .../payments/operations/payment_confirm.rs | 2 + .../payments/operations/payment_create.rs | 3 + .../payments/operations/payment_reject.rs | 2 + .../payments/operations/payment_response.rs | 139 +++++++- .../payments/operations/payment_session.rs | 2 + .../core/payments/operations/payment_start.rs | 2 + .../payments/operations/payment_status.rs | 16 + .../payments/operations/payment_update.rs | 2 + .../payments_incremental_authorization.rs | 328 ++++++++++++++++++ .../router/src/core/payments/transformers.rs | 62 +++- crates/router/src/db.rs | 2 + crates/router/src/db/authorization.rs | 104 ++++++ crates/router/src/db/kafka_store.rs | 36 ++ crates/router/src/openapi.rs | 2 + crates/router/src/routes/app.rs | 3 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/payments.rs | 77 ++++ crates/router/src/services/api.rs | 4 + crates/router/src/types.rs | 26 ++ crates/router/src/types/api/payments.rs | 24 +- crates/router/src/types/storage.rs | 3 +- .../router/src/types/storage/authorization.rs | 1 + crates/router/src/types/transformers.rs | 13 + crates/router/src/utils/user/sample_data.rs | 1 + crates/router/tests/connectors/utils.rs | 2 + crates/router_derive/src/macros/operation.rs | 12 +- crates/router_env/src/logger/types.rs | 2 + .../src/mock_db/payment_intent.rs | 1 + .../src/payments/payment_attempt.rs | 14 + .../src/payments/payment_intent.rs | 13 + .../down.sql | 2 + .../up.sql | 16 + .../down.sql | 2 + .../up.sql | 2 + openapi/openapi_spec.json | 61 ++++ 60 files changed, 1792 insertions(+), 22 deletions(-) create mode 100644 crates/diesel_models/src/authorization.rs create mode 100644 crates/diesel_models/src/query/authorization.rs create mode 100644 crates/router/src/core/payments/flows/incremental_authorization_flow.rs create mode 100644 crates/router/src/core/payments/operations/payments_incremental_authorization.rs create mode 100644 crates/router/src/db/authorization.rs create mode 100644 crates/router/src/types/storage/authorization.rs create mode 100644 migrations/2023-11-30-170902_add-authorizations-table/down.sql create mode 100644 migrations/2023-11-30-170902_add-authorizations-table/up.sql create mode 100644 migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql create mode 100644 migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index 2f3336fc2777..f718dc1ca4dd 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -8,8 +8,9 @@ use crate::{ payments::{ PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentsApproveRequest, PaymentsCancelRequest, - PaymentsCaptureRequest, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsRetrieveRequest, PaymentsStartRequest, RedirectionResponse, + PaymentsCaptureRequest, PaymentsIncrementalAuthorizationRequest, PaymentsRejectRequest, + PaymentsRequest, PaymentsResponse, PaymentsRetrieveRequest, PaymentsStartRequest, + RedirectionResponse, }, }; impl ApiEventMetric for PaymentsRetrieveRequest { @@ -149,3 +150,11 @@ impl ApiEventMetric for PaymentListResponseV2 { } impl ApiEventMetric for RedirectionResponse {} + +impl ApiEventMetric for PaymentsIncrementalAuthorizationRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index eaf0937ef2a2..0da6822f1501 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2209,6 +2209,12 @@ pub struct PaymentsResponse { /// If true incremental authorization can be performed on this payment pub incremental_authorization_allowed: Option, + + /// Total number of authorizations happened in an incremental_authorization payment + pub authorization_count: Option, + + /// List of incremental authorizations happened to the payment + pub incremental_authorizations: Option>, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] @@ -2277,6 +2283,24 @@ pub struct PaymentListResponse { // The list of payments response objects pub data: Vec, } + +#[derive(Setter, Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct IncrementalAuthorizationResponse { + /// The unique identifier of authorization + pub authorization_id: String, + /// Amount the authorization has been made for + pub amount: i64, + #[schema(value_type= AuthorizationStatus)] + /// The status of the authorization + pub status: common_enums::AuthorizationStatus, + /// Error code sent by the connector for authorization + pub error_code: Option, + /// Error message sent by the connector for authorization + pub error_message: Option, + /// Previously authorized amount for the payment + pub previously_authorized_amount: i64, +} + #[derive(Clone, Debug, serde::Serialize)] pub struct PaymentListResponseV2 { /// The number of payments included in the list for given constraints @@ -2985,6 +3009,18 @@ pub struct PaymentsCancelRequest { pub merchant_connector_details: Option, } +#[derive(Default, Debug, serde::Serialize, serde::Deserialize, Clone, ToSchema)] +pub struct PaymentsIncrementalAuthorizationRequest { + /// The identifier for the payment + #[serde(skip)] + pub payment_id: String, + /// The total amount including previously authorized amount and additional amount + #[schema(value_type = i64, example = 6540)] + pub amount: i64, + /// Reason for incremental authorization + pub reason: Option, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct PaymentsApproveRequest { /// The identifier for the payment diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 8da4a2da54cc..7615c0cc8804 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -246,6 +246,32 @@ pub enum CaptureStatus { Failed, } +#[derive( + Default, + Clone, + Debug, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum AuthorizationStatus { + Success, + Failure, + // Processing state is before calling connector + #[default] + Processing, + // Requires merchant action + Unresolved, +} + #[derive( Clone, Copy, diff --git a/crates/common_utils/src/request.rs b/crates/common_utils/src/request.rs index 64bce8649d97..d6d9281a4a05 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -17,6 +17,7 @@ pub enum Method { Post, Put, Delete, + Patch, } #[derive(Deserialize, Serialize, Debug)] diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index af2076bfa10d..7a4787fcf0a0 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -52,4 +52,5 @@ pub struct PaymentIntent { pub surcharge_applicable: Option, pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 44aa48b142ad..f7b849f1d4e1 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -359,6 +359,10 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } impl ForeignIDRef for PaymentAttempt { diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index d8f927a4e2c5..d7edcfdf1791 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -109,6 +109,7 @@ pub struct PaymentIntentNew { pub surcharge_applicable: Option, pub request_incremental_authorization: storage_enums::RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -186,6 +187,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: bool, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default)] @@ -218,6 +225,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } impl From for PaymentIntentUpdateInternal { @@ -381,6 +389,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/authorization.rs b/crates/diesel_models/src/authorization.rs new file mode 100644 index 000000000000..64fd1c65187d --- /dev/null +++ b/crates/diesel_models/src/authorization.rs @@ -0,0 +1,78 @@ +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{enums as storage_enums, schema::incremental_authorization}; + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Serialize, Deserialize, Hash)] +#[diesel(table_name = incremental_authorization)] +#[diesel(primary_key(authorization_id, merchant_id))] +pub struct Authorization { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: PrimitiveDateTime, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub modified_at: PrimitiveDateTime, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationNew { + pub authorization_id: String, + pub merchant_id: String, + pub payment_id: String, + pub amount: i64, + pub status: storage_enums::AuthorizationStatus, + pub error_code: Option, + pub error_message: Option, + pub connector_authorization_id: Option, + pub previously_authorized_amount: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthorizationUpdate { + StatusUpdate { + status: storage_enums::AuthorizationStatus, + error_code: Option, + error_message: Option, + connector_authorization_id: Option, + }, +} + +#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = incremental_authorization)] +pub struct AuthorizationUpdateInternal { + pub status: Option, + pub error_code: Option, + pub error_message: Option, + pub modified_at: Option, + pub connector_authorization_id: Option, +} + +impl From for AuthorizationUpdateInternal { + fn from(authorization_child_update: AuthorizationUpdate) -> Self { + let now = Some(common_utils::date_time::now()); + match authorization_child_update { + AuthorizationUpdate::StatusUpdate { + status, + error_code, + error_message, + connector_authorization_id, + } => Self { + status: Some(status), + error_code, + error_message, + connector_authorization_id, + modified_at: now, + }, + } + } +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 781099662a50..fa32fb84a15d 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; pub mod customers; pub mod dispute; pub mod encryption; diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 216801fa8fb1..b1e8e144a9e3 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -269,6 +269,10 @@ pub enum PaymentAttemptUpdate { connector: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + amount_capturable: i64, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -679,6 +683,14 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, + PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self { + amount: Some(amount), + amount_capturable: Some(amount_capturable), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 40d8fd92caeb..1bd5c73a96ca 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -54,6 +54,7 @@ pub struct PaymentIntent { pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive( @@ -110,6 +111,7 @@ pub struct PaymentIntentNew { pub surcharge_applicable: Option, pub request_incremental_authorization: RequestIncrementalAuthorization, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -187,6 +189,12 @@ pub enum PaymentIntentUpdate { surcharge_applicable: Option, updated_by: String, }, + IncrementalAuthorizationAmountUpdate { + amount: i64, + }, + AuthorizationCountUpdate { + authorization_count: i32, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -220,6 +228,7 @@ pub struct PaymentIntentUpdateInternal { pub updated_by: String, pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, + pub authorization_count: Option, } impl PaymentIntentUpdate { @@ -251,6 +260,7 @@ impl PaymentIntentUpdate { updated_by, surcharge_applicable, incremental_authorization_allowed, + authorization_count, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -282,6 +292,7 @@ impl PaymentIntentUpdate { surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), incremental_authorization_allowed, + authorization_count, ..source } } @@ -448,6 +459,16 @@ impl From for PaymentIntentUpdateInternal { updated_by, ..Default::default() }, + PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } => Self { + amount: Some(amount), + ..Default::default() + }, + PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + } => Self { + authorization_count: Some(authorization_count), + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index b0537d0a287b..3a3dee47a854 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -5,6 +5,7 @@ mod capture; pub mod cards_info; pub mod configs; +pub mod authorization; pub mod customers; pub mod dashboard_metadata; pub mod dispute; diff --git a/crates/diesel_models/src/query/authorization.rs b/crates/diesel_models/src/query/authorization.rs new file mode 100644 index 000000000000..dc9515bda55e --- /dev/null +++ b/crates/diesel_models/src/query/authorization.rs @@ -0,0 +1,79 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + authorization::{ + Authorization, AuthorizationNew, AuthorizationUpdate, AuthorizationUpdateInternal, + }, + errors, + schema::incremental_authorization::dsl, + PgPooledConn, StorageResult, +}; + +impl AuthorizationNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Authorization { + #[instrument(skip(conn))] + pub async fn update_by_merchant_id_authorization_id( + conn: &PgPooledConn, + merchant_id: String, + authorization_id: String, + authorization_update: AuthorizationUpdate, + ) -> StorageResult { + match generics::generic_update_with_unique_predicate_get_result::< + ::Table, + _, + _, + _, + >( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + AuthorizationUpdateInternal::from(authorization_update), + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NotFound => Err(error.attach_printable( + "Authorization with the given Authorization ID does not exist", + )), + errors::DatabaseError::NoFieldsToUpdate => { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::authorization_id.eq(authorization_id.to_owned())), + ) + .await + } + _ => Err(error), + }, + result => result, + } + } + + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_payment_id( + conn: &PgPooledConn, + merchant_id: &str, + payment_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::payment_id.eq(payment_id.to_owned())), + None, + None, + Some(dsl::created_at.asc()), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 13b001ecc6d1..9baf613d9233 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -362,6 +362,31 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + incremental_authorization (authorization_id, merchant_id) { + #[max_length = 64] + authorization_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + payment_id -> Varchar, + amount -> Int8, + created_at -> Timestamp, + modified_at -> Timestamp, + #[max_length = 64] + status -> Varchar, + #[max_length = 255] + error_code -> Nullable, + error_message -> Nullable, + #[max_length = 64] + connector_authorization_id -> Nullable, + previously_authorized_amount -> Int8, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -680,6 +705,7 @@ diesel::table! { surcharge_applicable -> Nullable, request_incremental_authorization -> RequestIncrementalAuthorization, incremental_authorization_allowed -> Nullable, + authorization_count -> Nullable, } } @@ -997,6 +1023,7 @@ diesel::allow_tables_to_appear_in_same_query!( file_metadata, fraud_check, gateway_status_map, + incremental_authorization, locker_mock_up, mandate, merchant_account, diff --git a/crates/router/src/connector/cybersource.rs b/crates/router/src/connector/cybersource.rs index 1de107af086d..631b2f8c97ed 100644 --- a/crates/router/src/connector/cybersource.rs +++ b/crates/router/src/connector/cybersource.rs @@ -217,7 +217,10 @@ where ("Host".to_string(), host.to_string().into()), ("Signature".to_string(), signature.into_masked()), ]; - if matches!(http_method, services::Method::Post | services::Method::Put) { + if matches!( + http_method, + services::Method::Post | services::Method::Put | services::Method::Patch + ) { headers.push(( "Digest".to_string(), format!("SHA-256={sha256}").into_masked(), @@ -232,6 +235,7 @@ impl api::PaymentAuthorize for Cybersource {} impl api::PaymentSync for Cybersource {} impl api::PaymentVoid for Cybersource {} impl api::PaymentCapture for Cybersource {} +impl api::PaymentIncrementalAuthorization for Cybersource {} impl api::MandateSetup for Cybersource {} impl api::ConnectorAccessToken for Cybersource {} impl api::PaymentToken for Cybersource {} @@ -872,6 +876,116 @@ impl ConnectorIntegration for Cybersource +{ + fn get_headers( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let connector_payment_id = req.request.connector_transaction_id.clone(); + Ok(format!( + "{}pts/v2/payments/{}", + self.base_url(connectors), + connector_payment_id + )) + } + + fn get_request_body( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let connector_router_data = cybersource::CybersourceRouterData::try_from(( + &self.get_currency_unit(), + req.request.currency, + req.request.additional_amount, + req, + ))?; + let connector_request = + cybersource::CybersourcePaymentsIncrementalAuthorizationRequest::try_from( + &connector_router_data, + )?; + let cybersource_payments_incremental_authorization_request = + types::RequestBody::log_and_get_request_body( + &connector_request, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(cybersource_payments_incremental_authorization_request)) + } + fn build_request( + &self, + req: &types::PaymentsIncrementalAuthorizationRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Patch) + .url(&types::IncrementalAuthorizationType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::IncrementalAuthorizationType::get_headers( + self, req, connectors, + )?) + .body(types::IncrementalAuthorizationType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + fn handle_response( + &self, + data: &types::PaymentsIncrementalAuthorizationRouterData, + res: types::Response, + ) -> CustomResult< + types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + errors::ConnectorError, + > { + let response: cybersource::CybersourcePaymentsIncrementalAuthorizationResponse = res + .response + .parse_struct("Cybersource PaymentResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + types::RouterData::try_from(( + types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }, + true, + )) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + fn get_error_response( + &self, + res: types::Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + #[async_trait::async_trait] impl api::IncomingWebhook for Cybersource { fn get_webhook_object_reference_id( diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 953f82c76a83..d3f542d2013a 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -77,9 +77,11 @@ impl TryFrom<&types::SetupMandateRouterData> for CybersourceZeroMandateRequest { Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { - initiator_type: CybersourcePaymentInitiatorTypes::Customer, - credential_stored_on_file: true, + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, }, + merchant_intitiated_transaction: None, }), ); @@ -158,14 +160,22 @@ pub enum CybersourceActionsTokenType { #[serde(rename_all = "camelCase")] pub struct CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator, + merchant_intitiated_transaction: Option, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantInitiatedTransaction { + reason: String, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct CybersourcePaymentInitiator { #[serde(rename = "type")] - initiator_type: CybersourcePaymentInitiatorTypes, - credential_stored_on_file: bool, + initiator_type: Option, + credential_stored_on_file: Option, + stored_credential_used: Option, } #[derive(Debug, Serialize)] @@ -229,6 +239,12 @@ pub struct OrderInformationWithBill { bill_to: Option, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct OrderInformation { @@ -242,6 +258,13 @@ pub struct Amount { currency: String, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AdditionalAmount { + additional_amount: String, + currency: String, +} + #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct BillTo { @@ -305,9 +328,11 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> Some(vec![CybersourceActionsTokenType::InstrumentIdentifier]), Some(CybersourceAuthorizationOptions { initiator: CybersourcePaymentInitiator { - initiator_type: CybersourcePaymentInitiatorTypes::Customer, - credential_stored_on_file: true, + initiator_type: Some(CybersourcePaymentInitiatorTypes::Customer), + credential_stored_on_file: Some(true), + stored_credential_used: None, }, + merchant_intitiated_transaction: None, }), ) } else { @@ -390,6 +415,13 @@ pub struct CybersourcePaymentsCaptureRequest { order_information: OrderInformationWithBill, } +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationRequest { + processing_information: ProcessingInformation, + order_information: OrderInformationIncrementalAuthorization, +} + impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> for CybersourcePaymentsCaptureRequest { @@ -420,6 +452,41 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsCaptureRouterData>> } } +impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>> + for CybersourcePaymentsIncrementalAuthorizationRequest +{ + type Error = error_stack::Report; + fn try_from( + item: &CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRouterData>, + ) -> Result { + Ok(Self { + processing_information: ProcessingInformation { + action_list: None, + action_token_types: None, + authorization_options: Some(CybersourceAuthorizationOptions { + initiator: CybersourcePaymentInitiator { + initiator_type: None, + credential_stored_on_file: None, + stored_credential_used: Some(true), + }, + merchant_intitiated_transaction: Some(MerchantInitiatedTransaction { + reason: "5".to_owned(), + }), + }), + commerce_indicator: CybersourceCommerceIndicator::Internet, + capture: None, + capture_options: None, + }, + order_information: OrderInformationIncrementalAuthorization { + amount_details: AdditionalAmount { + additional_amount: item.amount.clone(), + currency: item.router_data.request.currency.to_string(), + }, + }, + }) + } +} + pub struct CybersourceAuthType { pub(super) api_key: Secret, pub(super) merchant_account: Secret, @@ -461,6 +528,14 @@ pub enum CybersourcePaymentStatus { Processing, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum CybersourceIncrementalAuthorizationStatus { + Authorized, + Declined, + AuthorizedPendingReview, +} + impl From for enums::AttemptStatus { fn from(item: CybersourcePaymentStatus) -> Self { match item { @@ -477,6 +552,16 @@ impl From for enums::AttemptStatus { } } +impl From for common_enums::AuthorizationStatus { + fn from(item: CybersourceIncrementalAuthorizationStatus) -> Self { + match item { + CybersourceIncrementalAuthorizationStatus::Authorized + | CybersourceIncrementalAuthorizationStatus::AuthorizedPendingReview => Self::Success, + CybersourceIncrementalAuthorizationStatus::Declined => Self::Failure, + } + } +} + impl From for enums::RefundStatus { fn from(item: CybersourcePaymentStatus) -> Self { match item { @@ -499,6 +584,13 @@ pub struct CybersourcePaymentsResponse { token_information: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CybersourcePaymentsIncrementalAuthorizationResponse { + status: CybersourceIncrementalAuthorizationStatus, + error_information: Option, +} + #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceSetupMandatesResponse { @@ -654,6 +746,54 @@ impl } } +impl + TryFrom<( + types::ResponseRouterData< + F, + CybersourcePaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, + bool, + )> for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + data: ( + types::ResponseRouterData< + F, + CybersourcePaymentsIncrementalAuthorizationResponse, + T, + types::PaymentsResponseData, + >, + bool, + ), + ) -> Result { + let item = data.0; + Ok(Self { + response: match item.response.error_information { + Some(error) => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus::Failure, + error_code: Some(error.reason), + error_message: Some(error.message), + connector_authorization_id: None, + }, + ), + _ => Ok( + types::PaymentsResponseData::IncrementalAuthorizationResponse { + status: item.response.status.into(), + error_code: None, + error_message: None, + connector_authorization_id: None, + }, + ), + }, + ..item.data + }) + } +} + #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CybersourceTransactionResponse { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 67b1dbcc98b2..8bc251f9c3d3 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -32,8 +32,9 @@ use scheduler::{db::process_tracker::ProcessTrackerExt, errors as sch_errors, ut use time; pub use self::operations::{ - PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentReject, - PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, + PaymentIncrementalAuthorization, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, + PaymentUpdate, }; use self::{ conditional_configs::perform_decision_management, @@ -1884,6 +1885,16 @@ where pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, + pub incremental_authorization_details: Option, + pub authorizations: Vec, +} + +#[derive(Debug, Default, Clone)] +pub struct IncrementalAuthorizationDetails { + pub additional_amount: i64, + pub total_amount: i64, + pub reason: Option, + pub authorization_id: Option, } #[derive(Debug, Default, Clone)] @@ -1984,6 +1995,10 @@ pub fn should_call_connector( "CompleteAuthorize" => true, "PaymentApprove" => true, "PaymentSession" => true, + "PaymentIncrementalAuthorization" => matches!( + payment_data.payment_intent.status, + storage_enums::IntentStatus::RequiresCapture + ), _ => false, } } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 9be6f5905b8b..94b8bc1ff5d4 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -3,6 +3,7 @@ pub mod authorize_flow; pub mod cancel_flow; pub mod capture_flow; pub mod complete_authorize_flow; +pub mod incremental_authorization_flow; pub mod psync_flow; pub mod reject_flow; pub mod session_flow; @@ -1577,3 +1578,82 @@ default_imp_for_reject!( connector::Worldpay, connector::Zen ); + +macro_rules! default_imp_for_incremental_authorization { + ($($path:ident::$connector:ident),*) => { + $( + impl api::PaymentIncrementalAuthorization for $path::$connector {} + impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::PaymentIncrementalAuthorization for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_incremental_authorization!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); diff --git a/crates/router/src/core/payments/flows/incremental_authorization_flow.rs b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs new file mode 100644 index 000000000000..387916bab7c9 --- /dev/null +++ b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs @@ -0,0 +1,118 @@ +use async_trait::async_trait; + +use super::ConstructFlowSpecificData; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + payments::{self, access_token, helpers, transformers, Feature, PaymentData}, + }, + routes::AppState, + services, + types::{self, api, domain}, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult { + Box::pin(transformers::construct_payment_router_data::< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + >( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + merchant_connector_account, + )) + .await + } +} + +#[async_trait] +impl Feature + for types::RouterData< + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > +{ + async fn decide_flows<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + _customer: &Option, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, + connector_request: Option, + _key_store: &domain::MerchantKeyStore, + ) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &self, + call_connector_action, + connector_request, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) + } + + async fn add_access_token<'a>( + &self, + state: &AppState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self).await + } + + async fn build_flow_specific_connector_request( + &mut self, + state: &AppState, + connector: &api::ConnectorData, + call_connector_action: payments::CallConnectorAction, + ) -> RouterResult<(Option, bool)> { + let request = match call_connector_action { + payments::CallConnectorAction::Trigger => { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + connector_integration + .build_request(self, &state.conf.connectors) + .to_payment_failed_response()? + } + _ => None, + }; + + Ok((request, true)) + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 59b2b53c654d..3e01a7b193d0 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2417,6 +2417,20 @@ pub async fn get_merchant_fullfillment_time( } } +pub(crate) fn validate_payment_status_against_allowed_statuses( + intent_status: &storage_enums::IntentStatus, + allowed_statuses: &[storage_enums::IntentStatus], + action: &'static str, +) -> Result<(), errors::ApiErrorResponse> { + fp_utils::when(!allowed_statuses.contains(intent_status), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "You cannot {action} this payment because it has status {intent_status}", + ), + }) + }) +} + pub(crate) fn validate_payment_status_against_not_allowed_statuses( intent_status: &storage_enums::IntentStatus, not_allowed_statuses: &[storage_enums::IntentStatus], @@ -2612,6 +2626,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(900); @@ -2665,6 +2680,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -2718,6 +2734,7 @@ mod tests { request_incremental_authorization: common_enums::RequestIncrementalAuthorization::default(), incremental_authorization_allowed: None, + authorization_count: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 809c9e925de0..93db8f03ff5c 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -10,6 +10,7 @@ pub mod payment_session; pub mod payment_start; pub mod payment_status; pub mod payment_update; +pub mod payments_incremental_authorization; use api_models::enums::FrmSuggestion; use async_trait::async_trait; @@ -22,6 +23,7 @@ pub use self::{ payment_create::PaymentCreate, payment_reject::PaymentReject, payment_response::PaymentResponse, payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, + payments_incremental_authorization::PaymentIncrementalAuthorization, }; use super::{helpers, CustomerDetails, PaymentData}; use crate::{ diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index f51d7a93ee5e..fee0326df03c 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -251,6 +251,8 @@ impl surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index ae7810971896..7c8fbcc34979 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -171,6 +171,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 5b89cfdbcf0b..65b91f0401cf 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -215,6 +215,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 8b264edbb3d1..17b71172a349 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -245,6 +245,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index d718db79a6d0..91dd5be40f64 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -479,6 +479,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index bb7d0a931e1b..f3cd726a17c7 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -323,6 +323,8 @@ impl surcharge_details, frm_message: None, payment_link_data, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { @@ -757,6 +759,7 @@ impl PaymentCreate { updated_by: merchant_account.storage_scheme.to_string(), request_incremental_authorization, incremental_authorization_allowed: None, + authorization_count: None, }) } diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index ae606187a0a1..03bf6dd46b60 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -158,6 +158,8 @@ impl surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 9781ad651ee2..f92487d74a7b 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; use async_trait::async_trait; +use common_enums::AuthorizationStatus; use data_models::payments::payment_attempt::PaymentAttempt; -use error_stack::ResultExt; +use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; use router_derive; use router_env::{instrument, tracing}; @@ -36,7 +37,7 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( operations = "post_update_tracker", - flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data" + flow = "sync_data, authorize_data, cancel_data, capture_data, complete_authorize_data, approve_data, reject_data, setup_mandate_data, session_data,incremental_authorization_data" )] pub struct PaymentResponse; @@ -76,6 +77,138 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData } } +#[async_trait] +impl PostUpdateTracker, types::PaymentsIncrementalAuthorizationData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + db: &'b AppState, + _payment_id: &api::PaymentIdType, + mut payment_data: PaymentData, + router_data: types::RouterData< + F, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, + >, + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send, + { + let incremental_authorization_details = payment_data + .incremental_authorization_details + .clone() + .ok_or_else(|| { + report!(errors::ApiErrorResponse::InternalServerError) + .attach_printable("missing incremental_authorization_details in payment_data") + })?; + // Update payment_intent and payment_attempt 'amount' if incremental_authorization is successful + let (option_payment_attempt_update, option_payment_intent_update) = + match router_data.response.clone() { + Err(_) => (None, None), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + .. + }) => { + if status == AuthorizationStatus::Success { + (Some( + storage::PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + amount_capturable: incremental_authorization_details.total_amount, + }, + ), Some( + storage::PaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { + amount: incremental_authorization_details.total_amount, + }, + )) + } else { + (None, None) + } + } + _ => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow")?, + }; + //payment_attempt update + if let Some(payment_attempt_update) = option_payment_attempt_update { + payment_data.payment_attempt = db + .store + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // payment_intent update + if let Some(payment_intent_update) = option_payment_intent_update { + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + payment_intent_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + // Update the status of authorization record + let authorization_update = match &router_data.response { + Err(res) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: AuthorizationStatus::Failure, + error_code: Some(res.code.clone()), + error_message: Some(res.message.clone()), + connector_authorization_id: None, + }), + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { + status, + error_code, + error_message, + connector_authorization_id, + }) => Ok(storage::AuthorizationUpdate::StatusUpdate { + status: status.clone(), + error_code: error_code.clone(), + error_message: error_message.clone(), + connector_authorization_id: connector_authorization_id.clone(), + }), + Ok(_) => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("unexpected response in incremental_authorization flow"), + }?; + let authorization_id = incremental_authorization_details + .authorization_id + .clone() + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing authorization_id in incremental_authorization_details in payment_data", + ), + )?; + db.store + .update_authorization_by_merchant_id_authorization_id( + router_data.merchant_id.clone(), + authorization_id, + authorization_update, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while updating authorization")?; + //Fetch all the authorizations of the payment and send in incremental authorization response + let authorizations = db + .store + .find_all_authorizations_by_merchant_id_payment_id( + &router_data.merchant_id, + &payment_data.payment_intent.payment_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed while retrieving authorizations")?; + payment_data.authorizations = authorizations; + Ok(payment_data) + } +} + #[async_trait] impl PostUpdateTracker, types::PaymentsSyncData> for PaymentResponse { async fn update_tracker<'b>( @@ -544,6 +677,7 @@ async fn payment_response_update_tracker( types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), + types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => (None, None), types::PaymentsResponseData::MultipleCaptureResponse { capture_sync_response_list, } => match payment_data.multiple_capture_data { @@ -637,6 +771,7 @@ async fn payment_response_update_tracker( payment_data.payment_attempt.status, ), updated_by: storage_scheme.to_string(), + // make this false only if initial payment fails, if incremental authorization call fails don't make it false incremental_authorization_allowed: Some(false), }, Ok(_) => storage::PaymentIntentUpdate::ResponseUpdate { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 6097a5e430ce..572bc710b963 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -195,6 +195,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 3a4ae2c2e0de..887edd030d13 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -169,6 +169,8 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index d0cd4b32d3c2..0320cf50663e 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -314,6 +314,20 @@ async fn get_tracker_for_sync< ) })?; + let authorizations = db + .find_all_authorizations_by_merchant_id_payment_id( + &merchant_account.merchant_id, + &payment_id_str, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable_lazy(|| { + format!( + "Failed while getting authorizations list for, payment_id: {}, merchant_id: {}", + &payment_id_str, merchant_account.merchant_id + ) + })?; + let disputes = db .find_disputes_by_merchant_id_payment_id(&merchant_account.merchant_id, &payment_id_str) .await @@ -407,6 +421,8 @@ async fn get_tracker_for_sync< payment_link_data: None, surcharge_details: None, frm_message: frm_response.ok(), + incremental_authorization_details: None, + authorizations, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 68f064c539d1..e1c373171682 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -373,6 +373,8 @@ impl surcharge_details, frm_message: None, payment_link_data: None, + incremental_authorization_details: None, + authorizations: vec![], }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs new file mode 100644 index 000000000000..7346c46df120 --- /dev/null +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -0,0 +1,328 @@ +use std::marker::PhantomData; + +use api_models::{enums::FrmSuggestion, payments::PaymentsIncrementalAuthorizationRequest}; +use async_trait::async_trait; +use common_utils::errors::CustomResult; +use diesel_models::authorization::AuthorizationNew; +use error_stack::{report, IntoReport, ResultExt}; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payment_methods::PaymentMethodRetrieve, + payments::{ + self, helpers, operations, CustomerDetails, IncrementalAuthorizationDetails, + PaymentAddress, + }, + }, + routes::{app::StorageInterface, AppState}, + services, + types::{ + api::{self, PaymentIdTypeExt}, + domain, + storage::{self, enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] +#[operation(operations = "all", flow = "incremental_authorization")] +pub struct PaymentIncrementalAuthorization; + +#[async_trait] +impl + GetTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_id: &api::PaymentIdType, + request: &PaymentsIncrementalAuthorizationRequest, + _mandate_type: Option, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + _auth_flow: services::AuthFlow, + ) -> RouterResult< + operations::GetTrackerResponse<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + > { + let db = &*state.store; + let merchant_id = &merchant_account.merchant_id; + let storage_scheme = merchant_account.storage_scheme; + let payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::validate_payment_status_against_allowed_statuses( + &payment_intent.status, + &[enums::IntentStatus::RequiresCapture], + "increment authorization", + )?; + + if payment_intent.incremental_authorization_allowed != Some(true) { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: + "You cannot increment authorization this payment because it is not allowed for incremental_authorization".to_owned(), + })? + } + + if request.amount < payment_intent.amount { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Amount should be greater than original authorized amount".to_owned(), + })? + } + + let attempt_id = payment_intent.active_attempt.get_id().clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_intent.payment_id.as_str(), + merchant_id, + attempt_id.clone().as_str(), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let currency = payment_attempt.currency.get_required_value("currency")?; + let amount = payment_attempt.amount; + + let profile_id = payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("'profile_id' not set in payment intent")?; + + let business_profile = state + .store + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + })?; + + let payment_data = payments::PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount: amount.into(), + email: None, + mandate_id: None, + mandate_connector: None, + setup_mandate: None, + token: None, + address: PaymentAddress { + billing: None, + shipping: None, + }, + confirm: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + surcharge_details: None, + frm_message: None, + payment_link_data: None, + incremental_authorization_details: Some(IncrementalAuthorizationDetails { + additional_amount: request.amount - amount, + total_amount: request.amount, + reason: request.reason.clone(), + authorization_id: None, + }), + authorizations: vec![], + }; + + let get_trackers_response = operations::GetTrackerResponse { + operation: Box::new(self), + customer_details: None, + payment_data, + business_profile, + }; + + Ok(get_trackers_response) + } +} + +#[async_trait] +impl + UpdateTracker, PaymentsIncrementalAuthorizationRequest, Ctx> + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + db: &'b AppState, + mut payment_data: payments::PaymentData, + _customer: Option, + storage_scheme: enums::MerchantStorageScheme, + _updated_customer: Option, + _mechant_key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + _header_payload: api::HeaderPayload, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + payments::PaymentData, + )> + where + F: 'b + Send, + { + let new_authorization_count = payment_data + .payment_intent + .authorization_count + .map(|count| count + 1) + .unwrap_or(1); + // Create new authorization record + let authorization_new = AuthorizationNew { + authorization_id: format!( + "{}_{}", + common_utils::generate_id_with_default_len("auth"), + new_authorization_count + ), + merchant_id: payment_data.payment_intent.merchant_id.clone(), + payment_id: payment_data.payment_intent.payment_id.clone(), + amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + status: common_enums::AuthorizationStatus::Processing, + error_code: None, + error_message: None, + connector_authorization_id: None, + previously_authorized_amount: payment_data.payment_intent.amount, + }; + let authorization = db + .store + .insert_authorization(authorization_new.clone()) + .await + .to_duplicate_response(errors::ApiErrorResponse::GenericDuplicateError { + message: format!( + "Authorization with authorization_id {} already exists", + authorization_new.authorization_id + ), + }) + .attach_printable("failed while inserting new authorization")?; + // Update authorization_count in payment_intent + payment_data.payment_intent = db + .store + .update_payment_intent( + payment_data.payment_intent.clone(), + storage::PaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count: new_authorization_count, + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update authorization_count in Payment Intent")?; + match &payment_data.incremental_authorization_details { + Some(details) => { + payment_data.incremental_authorization_details = + Some(IncrementalAuthorizationDetails { + authorization_id: Some(authorization.authorization_id), + ..details.clone() + }); + } + None => Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("missing incremental_authorization_details in payment_data")?, + } + Ok((Box::new(self), payment_data)) + } +} + +impl + ValidateRequest + for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &PaymentsIncrementalAuthorizationRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + operations::ValidateResult<'a>, + )> { + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: &merchant_account.merchant_id, + payment_id: api::PaymentIdType::PaymentIntentId(request.payment_id.to_owned()), + mandate_type: None, + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }, + )) + } +} + +#[async_trait] +impl + Domain for PaymentIncrementalAuthorization +{ + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + _db: &dyn StorageInterface, + _payment_data: &mut payments::PaymentData, + _request: Option, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult< + ( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + ), + errors::StorageError, + > { + Ok((Box::new(self), None)) + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a AppState, + _payment_data: &mut payments::PaymentData, + _storage_scheme: enums::MerchantStorageScheme, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> RouterResult<( + BoxedOperation<'a, F, PaymentsIncrementalAuthorizationRequest, Ctx>, + Option, + )> { + Ok((Box::new(self), None)) + } + + async fn get_connector<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + state: &AppState, + _request: &PaymentsIncrementalAuthorizationRequest, + _payment_intent: &storage::PaymentIntent, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + helpers::get_connector_default(state, None).await + } +} diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 51e139c97988..bd6d03e5625a 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -4,7 +4,7 @@ use api_models::payments::{FrmMessage, RequestSurchargeDetails}; use common_enums::RequestIncrementalAuthorization; use common_utils::{consts::X_HS_LATENCY, fp_utils}; use diesel_models::ephemeral_key; -use error_stack::{IntoReport, ResultExt}; +use error_stack::{report, IntoReport, ResultExt}; use router_env::{instrument, tracing}; use super::{flows::Feature, PaymentData}; @@ -392,6 +392,18 @@ where ) }; + let incremental_authorizations_response = if payment_data.authorizations.is_empty() { + None + } else { + Some( + payment_data + .authorizations + .into_iter() + .map(ForeignInto::foreign_into) + .collect(), + ) + }; + let attempts_response = payment_data.attempts.map(|attempts| { attempts .into_iter() @@ -692,6 +704,8 @@ where .set_incremental_authorization_allowed( payment_intent.incremental_authorization_allowed, ) + .set_authorization_count(payment_intent.authorization_count) + .set_incremental_authorizations(incremental_authorizations_response) .to_owned(), headers, )) @@ -755,6 +769,8 @@ where unified_code: payment_attempt.unified_code, unified_message: payment_attempt.unified_message, incremental_authorization_allowed: payment_intent.incremental_authorization_allowed, + authorization_count: payment_intent.authorization_count, + incremental_authorizations: incremental_authorizations_response, ..Default::default() }, headers, @@ -1078,6 +1094,50 @@ impl TryFrom> for types::PaymentsSyncData } } +impl TryFrom> + for types::PaymentsIncrementalAuthorizationData +{ + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data; + let connector = api::ConnectorData::get_connector_by_name( + &additional_data.state.conf.connectors, + &additional_data.connector_name, + api::GetToken::Connector, + payment_data.payment_attempt.merchant_connector_id.clone(), + )?; + Ok(Self { + total_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.total_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + additional_amount: payment_data + .incremental_authorization_details + .clone() + .map(|details| details.additional_amount) + .ok_or( + report!(errors::ApiErrorResponse::InternalServerError).attach_printable( + "missing incremental_authorization_details in payment_data", + ), + )?, + reason: payment_data + .incremental_authorization_details + .and_then(|details| details.reason), + currency: payment_data.currency, + connector_transaction_id: connector + .connector + .connector_transaction_id(payment_data.payment_attempt.clone())? + .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, + }) + } +} + impl api::ConnectorTransactionId for Helcim { fn connector_transaction_id( &self, diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 6558cc6ace50..0cd4cb218810 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod authorization; pub mod business_profile; pub mod cache; pub mod capture; @@ -100,6 +101,7 @@ pub trait StorageInterface: + gsm::GsmInterface + user::UserInterface + user_role::UserRoleInterface + + authorization::AuthorizationInterface + user::sample_data::BatchSampleDataInterface + 'static { diff --git a/crates/router/src/db/authorization.rs b/crates/router/src/db/authorization.rs new file mode 100644 index 000000000000..f24daaf718ad --- /dev/null +++ b/crates/router/src/db/authorization.rs @@ -0,0 +1,104 @@ +use error_stack::IntoReport; + +use super::{MockDb, Store}; +use crate::{ + connection, + core::errors::{self, CustomResult}, + types::storage, +}; + +#[async_trait::async_trait] +pub trait AuthorizationInterface { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult; + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError>; + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl AuthorizationInterface for Store { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + authorization + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::Authorization::find_by_merchant_id_payment_id(&conn, merchant_id, payment_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Authorization::update_by_merchant_id_authorization_id( + &conn, + merchant_id, + authorization_id, + authorization, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl AuthorizationInterface for MockDb { + async fn insert_authorization( + &self, + _authorization: storage::AuthorizationNew, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + _merchant_id: &str, + _payment_id: &str, + ) -> CustomResult, errors::StorageError> { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + _merchant_id: String, + _authorization_id: String, + _authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + // TODO: Implement function for `MockDb` + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 32548e36b6fb..db94c1bcbca9 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -32,6 +32,7 @@ use crate::{ db::{ address::AddressInterface, api_keys::ApiKeyInterface, + authorization::AuthorizationInterface, business_profile::BusinessProfileInterface, capture::CaptureInterface, cards_info::CardsInfoInterface, @@ -2095,3 +2096,38 @@ impl BatchSampleDataInterface for KafkaStore { Ok(refunds_list) } } + +#[async_trait::async_trait] +impl AuthorizationInterface for KafkaStore { + async fn insert_authorization( + &self, + authorization: storage::AuthorizationNew, + ) -> CustomResult { + self.diesel_store.insert_authorization(authorization).await + } + + async fn find_all_authorizations_by_merchant_id_payment_id( + &self, + merchant_id: &str, + payment_id: &str, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_all_authorizations_by_merchant_id_payment_id(merchant_id, payment_id) + .await + } + + async fn update_authorization_by_merchant_id_authorization_id( + &self, + merchant_id: String, + authorization_id: String, + authorization: storage::AuthorizationUpdate, + ) -> CustomResult { + self.diesel_store + .update_authorization_by_merchant_id_authorization_id( + merchant_id, + authorization_id, + authorization, + ) + .await + } +} diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index cfb0268a9f80..82c98304c62b 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -175,6 +175,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::CaptureStatus, api_models::enums::ReconStatus, api_models::enums::ConnectorStatus, + api_models::enums::AuthorizationStatus, api_models::admin::MerchantConnectorCreate, api_models::admin::MerchantConnectorUpdate, api_models::admin::PrimaryBusinessDetails, @@ -315,6 +316,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::RequestSurchargeDetails, api_models::payments::PaymentAttemptResponse, api_models::payments::CaptureResponse, + api_models::payments::IncrementalAuthorizationResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, api_models::refunds::RefundListRequest, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 88806b565d3a..5d14d1219d32 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -343,6 +343,9 @@ impl Payments { web::resource("/{payment_id}/{merchant_id}/redirect/complete/{connector}") .route(web::get().to(payments_complete_authorize)) .route(web::post().to(payments_complete_authorize)), + ) + .service( + web::resource("/{payment_id}/incremental_authorization").route(web::post().to(payments_incremental_authorization)), ); } route diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index b32dbe3d4b6a..3592506f522b 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -96,7 +96,8 @@ impl From for ApiIdentifier { | Flow::PaymentsSessionToken | Flow::PaymentsStart | Flow::PaymentsList - | Flow::PaymentsRedirect => Self::Payments, + | Flow::PaymentsRedirect + | Flow::PaymentsIncrementalAuthorization => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 979b15a3d7f2..e424e93c78ed 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -986,6 +986,67 @@ where } } +/// Payments - Incremental Authorization +/// +/// Authorized amount for a payment can be incremented if it is in status: requires_capture +#[utoipa::path( + post, + path = "/payments/{payment_id}/incremental_authorization", + request_body=PaymentsIncrementalAuthorizationRequest, + params( + ("payment_id" = String, Path, description = "The identifier for payment") + ), + responses( + (status = 200, description = "Payment authorized amount incremented"), + (status = 400, description = "Missing mandatory fields") + ), + tag = "Payments", + operation_id = "Increment authorized amount for a Payment", + security(("api_key" = [])) +)] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsIncrementalAuthorization))] +pub async fn payments_incremental_authorization( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsIncrementalAuthorization; + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + payload.payment_id = payment_id; + let locking_action = payload.get_locking_input(flow.clone()); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth, req| { + payments::payments_core::< + api_types::IncrementalAuthorization, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentIncrementalAuthorization, + req, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + HeaderPayload::default(), + ) + }, + &auth::ApiKeyAuth, + locking_action, + )) + .await +} + pub fn get_or_generate_payment_id( payload: &mut payment_types::PaymentsRequest, ) -> errors::RouterResult<()> { @@ -1135,3 +1196,19 @@ impl GetLockingInput for payment_types::PaymentsCaptureRequest { } } } + +impl GetLockingInput for payment_types::PaymentsIncrementalAuthorizationRequest { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 1ff46474db59..ee5727bbda90 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -545,6 +545,9 @@ pub async fn send_request( Method::Put => client .put(url) .body(request.payload.expose_option().unwrap_or_default()), // If payload needs processing the body cannot have default + Method::Patch => client + .patch(url) + .body(request.payload.expose_option().unwrap_or_default()), Method::Delete => client.delete(url), } .add_headers(headers) @@ -1186,6 +1189,7 @@ impl Authenticate for api_models::payments::PaymentsSessionRequest { impl Authenticate for api_models::payments::PaymentsRetrieveRequest {} impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} +impl Authenticate for api_models::payments::PaymentsIncrementalAuthorizationRequest {} impl Authenticate for api_models::payments::PaymentsStartRequest {} pub fn build_redirection_form( diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 595f487e7079..08cbb36952e3 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -52,6 +52,11 @@ pub type PaymentsBalanceRouterData = pub type PaymentsSyncRouterData = RouterData; pub type PaymentsCaptureRouterData = RouterData; +pub type PaymentsIncrementalAuthorizationRouterData = RouterData< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type PaymentsCancelRouterData = RouterData; pub type PaymentsRejectRouterData = RouterData; @@ -142,6 +147,11 @@ pub type TokenizationType = dyn services::ConnectorIntegration< PaymentMethodTokenizationData, PaymentsResponseData, >; +pub type IncrementalAuthorizationType = dyn services::ConnectorIntegration< + api::IncrementalAuthorization, + PaymentsIncrementalAuthorizationData, + PaymentsResponseData, +>; pub type ConnectorCustomerType = dyn services::ConnectorIntegration< api::CreateConnectorCustomer, @@ -395,6 +405,15 @@ pub struct PaymentsCaptureData { pub browser_info: Option, } +#[derive(Debug, Clone, Default)] +pub struct PaymentsIncrementalAuthorizationData { + pub total_amount: i64, + pub additional_amount: i64, + pub currency: storage_enums::Currency, + pub reason: Option, + pub connector_transaction_id: String, +} + #[allow(dead_code)] #[derive(Debug, Clone, Default)] pub struct MultipleCaptureRequestData { @@ -599,6 +618,7 @@ impl Capturable for PaymentsCancelData { impl Capturable for PaymentsApproveData {} impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} +impl Capturable for PaymentsIncrementalAuthorizationData {} impl Capturable for PaymentsSyncData { fn get_capture_amount(&self, payment_data: &PaymentData) -> Option where @@ -707,6 +727,12 @@ pub enum PaymentsResponseData { session_token: Option, connector_response_reference_id: Option, }, + IncrementalAuthorizationResponse { + status: common_enums::AuthorizationStatus, + connector_authorization_id: Option, + error_code: Option, + error_message: Option, + }, } #[derive(Debug, Clone)] diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index b00a7f0cbdac..2acf42fa479d 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -5,11 +5,12 @@ pub use api_models::payments::{ PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, PaymentMethodDataResponse, PaymentOp, PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, - PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsRedirectRequest, - PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, - PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, - PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, - TimeRange, UrlDetails, VerifyRequest, VerifyResponse, WalletData, + PaymentsApproveRequest, PaymentsCancelRequest, PaymentsCaptureRequest, + PaymentsIncrementalAuthorizationRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, + PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, + PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, + PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, TimeRange, UrlDetails, + VerifyRequest, VerifyResponse, WalletData, }; use error_stack::{IntoReport, ResultExt}; @@ -81,6 +82,9 @@ pub struct SetupMandate; #[derive(Debug, Clone)] pub struct PreProcessing; +#[derive(Debug, Clone)] +pub struct IncrementalAuthorization; + pub trait PaymentIdTypeExt { fn get_payment_intent_id(&self) -> errors::CustomResult; } @@ -164,6 +168,15 @@ pub trait MandateSetup: { } +pub trait PaymentIncrementalAuthorization: + api::ConnectorIntegration< + IncrementalAuthorization, + types::PaymentsIncrementalAuthorizationData, + types::PaymentsResponseData, +> +{ +} + pub trait PaymentsCompleteAuthorize: api::ConnectorIntegration< CompleteAuthorize, @@ -215,6 +228,7 @@ pub trait Payment: + PaymentToken + PaymentsPreProcessing + ConnectorCustomer + + PaymentIncrementalAuthorization { } diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index a83a405f3554..c8cc7f9c010f 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod authorization; pub mod business_profile; pub mod capture; pub mod cards_info; @@ -43,7 +44,7 @@ pub use data_models::payments::{ }; pub use self::{ - address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, + address::*, api_keys::*, authorization::*, capture::*, cards_info::*, configs::*, customers::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, diff --git a/crates/router/src/types/storage/authorization.rs b/crates/router/src/types/storage/authorization.rs new file mode 100644 index 000000000000..678cd64f8810 --- /dev/null +++ b/crates/router/src/types/storage/authorization.rs @@ -0,0 +1 @@ +pub use diesel_models::authorization::{Authorization, AuthorizationNew, AuthorizationUpdate}; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 99096864a000..0244d8dc18ef 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -685,6 +685,19 @@ impl ForeignFrom for api_models::disputes::DisputeResponse { } } +impl ForeignFrom for payments::IncrementalAuthorizationResponse { + fn foreign_from(authorization: storage::Authorization) -> Self { + Self { + authorization_id: authorization.authorization_id, + amount: authorization.amount, + status: authorization.status, + error_code: authorization.error_code, + error_message: authorization.error_message, + previously_authorized_amount: authorization.previously_authorized_amount, + } + } +} + impl ForeignFrom for api_models::disputes::DisputeResponsePaymentsRetrieve { fn foreign_from(dispute: storage::Dispute) -> Self { Self { diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 7a9cf6d2b7db..543e3cd2aa5f 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -196,6 +196,7 @@ pub async fn generate_sample_data( surcharge_applicable: Default::default(), request_incremental_authorization: Default::default(), incremental_authorization_allowed: Default::default(), + authorization_count: Default::default(), }; let payment_attempt = PaymentAttemptBatchNew { attempt_id: attempt_id.clone(), diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 823b3eae497d..7e5cfeb43974 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -542,6 +542,7 @@ pub trait ConnectorActions: Connector { Ok(types::PaymentsResponseData::PreProcessingResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } @@ -1029,6 +1030,7 @@ pub fn get_connector_transaction_id( Ok(types::PaymentsResponseData::ConnectorCustomerResponse { .. }) => None, Ok(types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. }) => None, Ok(types::PaymentsResponseData::MultipleCaptureResponse { .. }) => None, + Ok(types::PaymentsResponseData::IncrementalAuthorizationResponse { .. }) => None, Err(_) => None, } } diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index 370e03b984ba..e743a2d9cc52 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -27,6 +27,8 @@ pub enum Derives { Verify, Session, SessionData, + IncrementalAuthorization, + IncrementalAuthorizationData, } impl Derives { @@ -95,6 +97,12 @@ impl Conversion { } Derives::Session => syn::Ident::new("PaymentsSessionRequest", Span::call_site()), Derives::SessionData => syn::Ident::new("PaymentsSessionData", Span::call_site()), + Derives::IncrementalAuthorization => { + syn::Ident::new("PaymentsIncrementalAuthorizationRequest", Span::call_site()) + } + Derives::IncrementalAuthorizationData => { + syn::Ident::new("PaymentsIncrementalAuthorizationData", Span::call_site()) + } } } @@ -414,6 +422,7 @@ pub fn operation_derive_inner(input: DeriveInput) -> syn::Result syn::Result DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } @@ -1728,6 +1735,13 @@ impl DataModelExt for PaymentAttemptUpdate { connector, updated_by, }, + DieselPaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + } => Self::IncrementalAuthorizationAmountUpdate { + amount, + amount_capturable, + }, } } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index fdf9875bc1ff..90bb21190c39 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -99,6 +99,7 @@ impl PaymentIntentInterface for KVRouterStore { surcharge_applicable: new.surcharge_applicable, request_incremental_authorization: new.request_incremental_authorization, incremental_authorization_allowed: new.incremental_authorization_allowed, + authorization_count: new.authorization_count, }; let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -762,6 +763,7 @@ impl DataModelExt for PaymentIntentNew { surcharge_applicable: self.surcharge_applicable, request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, + authorization_count: self.authorization_count, } } @@ -804,6 +806,7 @@ impl DataModelExt for PaymentIntentNew { surcharge_applicable: storage_model.surcharge_applicable, request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, + authorization_count: storage_model.authorization_count, } } } @@ -851,6 +854,7 @@ impl DataModelExt for PaymentIntent { surcharge_applicable: self.surcharge_applicable, request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, + authorization_count: self.authorization_count, } } @@ -894,6 +898,7 @@ impl DataModelExt for PaymentIntent { surcharge_applicable: storage_model.surcharge_applicable, request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, + authorization_count: storage_model.authorization_count, } } } @@ -1038,6 +1043,14 @@ impl DataModelExt for PaymentIntentUpdate { surcharge_applicable: Some(surcharge_applicable), updated_by, }, + Self::IncrementalAuthorizationAmountUpdate { amount } => { + DieselPaymentIntentUpdate::IncrementalAuthorizationAmountUpdate { amount } + } + Self::AuthorizationCountUpdate { + authorization_count, + } => DieselPaymentIntentUpdate::AuthorizationCountUpdate { + authorization_count, + }, } } diff --git a/migrations/2023-11-30-170902_add-authorizations-table/down.sql b/migrations/2023-11-30-170902_add-authorizations-table/down.sql new file mode 100644 index 000000000000..476f16a52aab --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS incremental_authorization; \ No newline at end of file diff --git a/migrations/2023-11-30-170902_add-authorizations-table/up.sql b/migrations/2023-11-30-170902_add-authorizations-table/up.sql new file mode 100644 index 000000000000..ade615877dc2 --- /dev/null +++ b/migrations/2023-11-30-170902_add-authorizations-table/up.sql @@ -0,0 +1,16 @@ +-- Your SQL goes here + +CREATE TABLE IF NOT EXISTS incremental_authorization ( + authorization_id VARCHAR(64) NOT NULL, + merchant_id VARCHAR(64) NOT NULL, + payment_id VARCHAR(64) NOT NULL, + amount BIGINT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + modified_at TIMESTAMP NOT NULL DEFAULT now()::TIMESTAMP, + status VARCHAR(64) NOT NULL, + error_code VARCHAR(255), + error_message TEXT, + connector_authorization_id VARCHAR(64), + previously_authorized_amount BIGINT NOT NULL, + PRIMARY KEY (authorization_id, merchant_id) +); \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql new file mode 100644 index 000000000000..8d4d0e6d81d2 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS authorization_count; \ No newline at end of file diff --git a/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql new file mode 100644 index 000000000000..741135c6a1a3 --- /dev/null +++ b/migrations/2023-12-01-090834_add-authorization_count-in-payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS authorization_count INTEGER; diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index b1e313f15baa..4f26589ba029 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -2888,6 +2888,15 @@ "no_three_ds" ] }, + "AuthorizationStatus": { + "type": "string", + "enum": [ + "success", + "failure", + "processing", + "unresolved" + ] + }, "BacsBankTransfer": { "type": "object", "required": [ @@ -6419,6 +6428,44 @@ } } }, + "IncrementalAuthorizationResponse": { + "type": "object", + "required": [ + "authorization_id", + "amount", + "status", + "previously_authorized_amount" + ], + "properties": { + "authorization_id": { + "type": "string", + "description": "The unique identifier of authorization" + }, + "amount": { + "type": "integer", + "format": "int64", + "description": "Amount the authorization has been made for" + }, + "status": { + "$ref": "#/components/schemas/AuthorizationStatus" + }, + "error_code": { + "type": "string", + "description": "Error code sent by the connector for authorization", + "nullable": true + }, + "error_message": { + "type": "string", + "description": "Error message sent by the connector for authorization", + "nullable": true + }, + "previously_authorized_amount": { + "type": "integer", + "format": "int64", + "description": "Previously authorized amount for the payment" + } + } + }, "IndomaretVoucherData": { "type": "object", "required": [ @@ -10540,6 +10587,20 @@ "type": "boolean", "description": "If true incremental authorization can be performed on this payment", "nullable": true + }, + "authorization_count": { + "type": "integer", + "format": "int32", + "description": "Total number of authorizations happened in an incremental_authorization payment", + "nullable": true + }, + "incremental_authorizations": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IncrementalAuthorizationResponse" + }, + "description": "List of incremental authorizations happened to the payment", + "nullable": true } } }, From 6c7d3a2e8a047ff23b52b76792fe8f28d3b952a4 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:51:41 +0530 Subject: [PATCH 09/18] fix: use card bin to get additional card details (#3036) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/payment_methods.rs | 2 + crates/api_models/src/payments.rs | 42 ++++++++++ .../src/surcharge_decision_configs.rs | 1 - crates/router/src/core/payments.rs | 50 ++---------- crates/router/src/core/payments/helpers.rs | 29 ++----- crates/router/src/core/payments/operations.rs | 1 - .../payments/operations/payment_confirm.rs | 20 ++++- .../payments/operations/payment_create.rs | 81 +++++++++++-------- crates/router/src/core/payments/types.rs | 2 + 9 files changed, 125 insertions(+), 103 deletions(-) diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 3343becaaae6..b3c6b049d5d9 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -342,6 +342,8 @@ pub struct SurchargeDetailsResponse { pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment pub display_tax_on_surcharge_amount: f64, + /// sum of display_surcharge_amount and display_tax_on_surcharge_amount + pub display_total_surcharge_amount: f64, /// sum of original amount, pub display_final_amount: f64, } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 0da6822f1501..93c97cbd443c 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -709,6 +709,33 @@ pub struct Card { pub nick_name: Option>, } +impl Card { + fn apply_additional_card_info(&self, additional_card_info: AdditionalCardInfo) -> Self { + Self { + card_number: self.card_number.clone(), + card_exp_month: self.card_exp_month.clone(), + card_exp_year: self.card_exp_year.clone(), + card_holder_name: self.card_holder_name.clone(), + card_cvc: self.card_cvc.clone(), + card_issuer: self + .card_issuer + .clone() + .or(additional_card_info.card_issuer), + card_network: self + .card_network + .clone() + .or(additional_card_info.card_network), + card_type: self.card_type.clone().or(additional_card_info.card_type), + card_issuing_country: self + .card_issuing_country + .clone() + .or(additional_card_info.card_issuing_country), + bank_code: self.bank_code.clone().or(additional_card_info.bank_code), + nick_name: self.nick_name.clone(), + } + } +} + #[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema, Default)] #[serde(rename_all = "snake_case")] pub struct CardToken { @@ -882,6 +909,21 @@ impl PaymentMethodData { | Self::CardToken(_) => None, } } + pub fn apply_additional_payment_data( + &self, + additional_payment_data: AdditionalPaymentData, + ) -> Self { + if let AdditionalPaymentData::Card(additional_card_info) = additional_payment_data { + match self { + Self::Card(card) => { + Self::Card(card.apply_additional_card_info(*additional_card_info)) + } + _ => self.to_owned(), + } + } else { + self.to_owned() + } + } } pub trait GetPaymentMethodType { diff --git a/crates/api_models/src/surcharge_decision_configs.rs b/crates/api_models/src/surcharge_decision_configs.rs index 7ead27945584..0777bde85de0 100644 --- a/crates/api_models/src/surcharge_decision_configs.rs +++ b/crates/api_models/src/surcharge_decision_configs.rs @@ -30,7 +30,6 @@ impl EuclidDirFilter for SurchargeDecisionConfigs { DirKeyKind::PaymentAmount, DirKeyKind::PaymentCurrency, DirKeyKind::BillingCountry, - DirKeyKind::CardType, DirKeyKind::CardNetwork, DirKeyKind::PayLaterType, DirKeyKind::WalletType, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8bc251f9c3d3..2a3ed0f1596f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -13,10 +13,7 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter}; -use api_models::{ - self, enums, - payments::{self, HeaderPayload}, -}; +use api_models::{self, enums, payments::HeaderPayload}; use common_utils::{ext_traits::AsyncExt, pii, types::Surcharge}; use data_models::mandates::MandateData; use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; @@ -176,10 +173,6 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { - operation - .to_domain()? - .populate_payment_data(state, &mut payment_data, &req, &merchant_account) - .await?; payment_data = match connector_details { api::ConnectorCallType::PreDetermined(connector) => { let schedule_time = if should_add_task_to_process_tracker { @@ -406,7 +399,6 @@ where async fn populate_surcharge_details( state: &AppState, payment_data: &mut PaymentData, - request: &payments::PaymentsRequest, ) -> RouterResult<()> where F: Send + Clone, @@ -416,7 +408,7 @@ where .surcharge_applicable .unwrap_or(false) { - let payment_method_data = request + let payment_method_data = payment_data .payment_method_data .clone() .get_required_value("payment_method_data")?; @@ -437,39 +429,7 @@ where Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, }; - let request_surcharge_details = request.surcharge_details; - - match (request_surcharge_details, calculated_surcharge_details) { - (Some(request_surcharge_details), Some(calculated_surcharge_details)) => { - if calculated_surcharge_details - .is_request_surcharge_matching(request_surcharge_details) - { - payment_data.surcharge_details = Some(calculated_surcharge_details); - } else { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), - } - .into()); - } - } - (None, Some(_calculated_surcharge_details)) => { - return Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "surcharge_details", - } - .into()); - } - (Some(request_surcharge_details), None) => { - if request_surcharge_details.is_surcharge_zero() { - return Ok(()); - } else { - return Err(errors::ApiErrorResponse::InvalidRequestData { - message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(), - } - .into()); - } - } - (None, None) => return Ok(()), - }; + payment_data.surcharge_details = calculated_surcharge_details; } else { let surcharge_details = payment_data @@ -978,6 +938,10 @@ 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( diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 3e01a7b193d0..c9ab77c6a332 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3634,31 +3634,16 @@ pub fn get_key_params_for_surcharge_details( )> { match payment_method_data { api_models::payments::PaymentMethodData::Card(card) => { - let card_type = card - .card_type - .get_required_value("payment_method_data.card.card_type")?; let card_network = card .card_network .get_required_value("payment_method_data.card.card_network")?; - match card_type.to_lowercase().as_str() { - "credit" => Ok(( - common_enums::PaymentMethod::Card, - common_enums::PaymentMethodType::Credit, - Some(card_network), - )), - "debit" => Ok(( - common_enums::PaymentMethod::Card, - common_enums::PaymentMethodType::Debit, - Some(card_network), - )), - _ => { - logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable"); - Err(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_method_data.card.card_type", - } - .into()) - } - } + // surcharge generated will always be same for credit as well as debit + // since surcharge conditions cannot be defined on card_type + Ok(( + common_enums::PaymentMethod::Card, + common_enums::PaymentMethodType::Credit, + Some(card_network), + )) } api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok(( common_enums::PaymentMethod::CardRedirect, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 93db8f03ff5c..cf0c0ab294a8 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -159,7 +159,6 @@ pub trait Domain: Send + Sync { &'a self, _state: &AppState, _payment_data: &mut PaymentData, - _request: &R, _merchant_account: &domain::MerchantAccount, ) -> CustomResult<(), errors::ApiErrorResponse> { Ok(()) diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 91dd5be40f64..578395860c9a 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -446,6 +446,21 @@ impl ) .await?; + let additional_pm_data = request + .payment_method_data + .as_ref() + .async_map(|payment_method_data| async { + helpers::get_additional_payment_data(payment_method_data, &*state.store).await + }) + .await; + let payment_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_pm_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -462,7 +477,7 @@ impl billing: billing_address.as_ref().map(|a| a.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: payment_method_data_after_card_bin_call, force_sync: None, refunds: vec![], disputes: vec![], @@ -593,10 +608,9 @@ impl Domain, - request: &api::PaymentsRequest, _merchant_account: &domain::MerchantAccount, ) -> CustomResult<(), errors::ApiErrorResponse> { - populate_surcharge_details(state, payment_data, request).await + populate_surcharge_details(state, payment_data).await } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index f3cd726a17c7..287e4945951b 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -167,7 +167,7 @@ impl ) .await?; - let payment_attempt_new = Self::make_payment_attempt( + let (payment_attempt_new, additional_payment_data) = Self::make_payment_attempt( &payment_id, merchant_id, money, @@ -290,6 +290,14 @@ impl payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); + let payment_method_data_after_card_bin_call = request + .payment_method_data + .as_ref() + .zip(additional_payment_data) + .map(|(payment_method_data, additional_payment_data)| { + payment_method_data.apply_additional_payment_data(additional_payment_data) + }); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -306,7 +314,7 @@ impl billing: billing_address.as_ref().map(|a| a.into()), }, confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + payment_method_data: payment_method_data_after_card_bin_call, refunds: vec![], disputes: vec![], attempts: None, @@ -604,7 +612,10 @@ impl PaymentCreate { request: &api::PaymentsRequest, browser_info: Option, state: &AppState, - ) -> RouterResult { + ) -> RouterResult<( + storage::PaymentAttemptNew, + Option, + )> { let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let status = helpers::payment_attempt_status_fsm(&request.payment_method_data, request.confirm); @@ -616,7 +627,8 @@ impl PaymentCreate { .async_map(|payment_method_data| async { helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) - .await + .await; + let additional_pm_data_value = additional_pm_data .as_ref() .map(Encode::::encode_to_value) .transpose() @@ -631,35 +643,38 @@ impl PaymentCreate { utils::get_payment_attempt_id(payment_id, 1) }; - Ok(storage::PaymentAttemptNew { - payment_id: payment_id.to_string(), - merchant_id: merchant_id.to_string(), - attempt_id, - status, - currency, - amount: amount.into(), - payment_method, - capture_method: request.capture_method, - capture_on: request.capture_on, - confirm: request.confirm.unwrap_or(false), - created_at, - modified_at, - last_synced, - authentication_type: request.authentication_type, - browser_info, - payment_experience: request.payment_experience, - payment_method_type, - payment_method_data: additional_pm_data, - amount_to_capture: request.amount_to_capture, - payment_token: request.payment_token.clone(), - mandate_id: request.mandate_id.clone(), - business_sub_label: request.business_sub_label.clone(), - mandate_details: request - .mandate_data - .as_ref() - .and_then(|inner| inner.mandate_type.clone().map(Into::into)), - ..storage::PaymentAttemptNew::default() - }) + Ok(( + storage::PaymentAttemptNew { + payment_id: payment_id.to_string(), + merchant_id: merchant_id.to_string(), + attempt_id, + status, + currency, + amount: amount.into(), + payment_method, + capture_method: request.capture_method, + capture_on: request.capture_on, + confirm: request.confirm.unwrap_or(false), + created_at, + modified_at, + last_synced, + authentication_type: request.authentication_type, + browser_info, + payment_experience: request.payment_experience, + payment_method_type, + payment_method_data: additional_pm_data_value, + amount_to_capture: request.amount_to_capture, + payment_token: request.payment_token.clone(), + mandate_id: request.mandate_id.clone(), + business_sub_label: request.business_sub_label.clone(), + mandate_details: request + .mandate_data + .as_ref() + .and_then(|inner| inner.mandate_type.clone().map(Into::into)), + ..storage::PaymentAttemptNew::default() + }, + additional_pm_data, + )) } #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index 7a50254e6f05..f97cdc17d724 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -219,6 +219,8 @@ impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsRe tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), display_surcharge_amount, display_tax_on_surcharge_amount, + display_total_surcharge_amount: display_surcharge_amount + + display_tax_on_surcharge_amount, display_final_amount, }) } From daf0f09f8e3293ee6a3599a25362d9171fc5b2e7 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Mon, 4 Dec 2023 22:30:51 +0530 Subject: [PATCH 10/18] feat: calculate surcharge for customer saved card list (#3039) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 1 + crates/api_models/src/payment_methods.rs | 68 +++++---- crates/common_utils/Cargo.toml | 1 + .../router/src/core/payment_methods/cards.rs | 108 ++++++++++++-- .../surcharge_decision_configs.rs | 93 ++++++++++-- crates/router/src/core/payments.rs | 77 ++++++---- crates/router/src/core/payments/helpers.rs | 3 +- .../payments/operations/payment_confirm.rs | 77 +--------- .../payments/operations/payment_create.rs | 2 +- .../payments/operations/payment_update.rs | 2 +- crates/router/src/core/payments/types.rs | 139 ++++++++++++------ crates/router/src/core/utils.rs | 74 +--------- crates/router/src/openapi.rs | 5 +- crates/router/src/types/api.rs | 6 +- openapi/openapi_spec.json | 108 ++++++++++++++ 15 files changed, 477 insertions(+), 287 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8719b29f51d..4231c62d9499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1708,6 +1708,7 @@ dependencies = [ "thiserror", "time", "tokio 1.32.0", + "utoipa", ] [[package]] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index b3c6b049d5d9..84830498b344 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -8,7 +8,7 @@ use common_utils::{ types::{Percentage, Surcharge}, }; use serde::de; -use utoipa::ToSchema; +use utoipa::{schema, ToSchema}; #[cfg(feature = "payouts")] use crate::payouts; @@ -264,19 +264,6 @@ pub struct CardNetworkTypes { pub card_network: api_enums::CardNetwork, /// surcharge details for this card network - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// The list of eligible connectors for a given card network @@ -313,31 +300,19 @@ pub struct ResponsePaymentMethodTypes { pub required_fields: Option>, /// surcharge details for this payment method type if exists - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// auth service connector label for this payment method type, if exists pub pm_auth_connector: Option, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] + +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct SurchargeDetailsResponse { /// surcharge value - pub surcharge: Surcharge, + pub surcharge: SurchargeResponse, /// tax on surcharge value - pub tax_on_surcharge: Option>, + pub tax_on_surcharge: Option, /// surcharge amount for this payment pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment @@ -348,6 +323,36 @@ pub struct SurchargeDetailsResponse { pub display_final_amount: f64, } +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum SurchargeResponse { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(SurchargePercentage), +} + +impl From for SurchargeResponse { + fn from(value: Surcharge) -> Self { + match value { + Surcharge::Fixed(amount) => Self::Fixed(amount), + Surcharge::Rate(percentage) => Self::Rate(percentage.into()), + } + } +} + +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct SurchargePercentage { + percentage: f32, +} + +impl From> for SurchargePercentage { + fn from(value: Percentage) -> Self { + Self { + percentage: value.get_percentage(), + } + } +} /// Required fields info used while listing the payment_method_data #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, ToSchema, Hash)] pub struct RequiredFieldInfo { @@ -716,6 +721,9 @@ pub struct CustomerPaymentMethod { #[schema(example = json!({"mask": "0000"}))] pub bank: Option, + /// Surcharge details for this saved card + pub surcharge_details: Option, + /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3619c93d772c..3a41b111b39d 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -38,6 +38,7 @@ 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/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 545733e298ab..bbcfe45a1d0c 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -25,7 +25,10 @@ use error_stack::{report, IntoReport, ResultExt}; use masking::Secret; use router_env::{instrument, tracing}; -use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list; +use super::surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, +}; use crate::{ configs::settings, core::{ @@ -38,7 +41,6 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, - utils::persist_individual_surcharge_details_in_redis, }, db, logger, pii::prelude::*, @@ -1687,12 +1689,9 @@ pub async fn call_surcharge_decision_management( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing surcharge decision operation")?; if !surcharge_results.is_empty_result() { - persist_individual_surcharge_details_in_redis( - &state, - merchant_account, - &surcharge_results, - ) - .await?; + surcharge_results + .persist_individual_surcharge_details_in_redis(&state, merchant_account) + .await?; let _ = state .store .update_payment_intent( @@ -1711,6 +1710,56 @@ pub async fn call_surcharge_decision_management( } } +pub async fn call_surcharge_decision_management_for_saved_card( + state: &routes::AppState, + merchant_account: &domain::MerchantAccount, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + customer_payment_method_response: &mut api::CustomerPaymentMethodsListResponse, +) -> errors::RouterResult<()> { + if payment_attempt.surcharge_amount.is_some() { + Ok(()) + } else { + let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account + .routing_algorithm + .clone() + .map(|val| val.parse_value("routing algorithm")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not decode the routing algorithm")? + .unwrap_or_default(); + let surcharge_results = perform_surcharge_decision_management_for_saved_cards( + state, + algorithm_ref, + payment_attempt, + &payment_intent, + &mut customer_payment_method_response.customer_payment_methods, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .await?; + let _ = state + .store + .update_payment_intent( + payment_intent, + storage::PaymentIntentUpdate::SurchargeApplicableUpdate { + surcharge_applicable: true, + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable("Failed to update surcharge_applicable in Payment Intent"); + } + Ok(()) + } +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, @@ -2195,12 +2244,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .await } else { let cloned_secret = req.and_then(|r| r.client_secret.as_ref().cloned()); - let payment_intent = helpers::verify_payment_intent_time_and_client_secret( - db, - &merchant_account, - cloned_secret, - ) - .await?; + let payment_intent: Option = + helpers::verify_payment_intent_time_and_client_secret( + db, + &merchant_account, + cloned_secret, + ) + .await?; let customer_id = payment_intent .as_ref() .and_then(|intent| intent.customer_id.to_owned()) @@ -2326,6 +2376,7 @@ pub async fn list_customer_payment_method( created: Some(pm.created_at), bank_transfer: pmd, bank: bank_details, + surcharge_details: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2377,9 +2428,36 @@ pub async fn list_customer_payment_method( } } - let response = api::CustomerPaymentMethodsListResponse { + let mut response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: customer_pms, }; + let payment_attempt = payment_intent + .as_ref() + .async_map(|payment_intent| async { + state + .store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_account.merchant_id, + &payment_intent.active_attempt.get_id(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + }) + .await + .transpose()?; + + if let Some((payment_attempt, payment_intent)) = payment_attempt.zip(payment_intent) { + call_surcharge_decision_management_for_saved_card( + state, + &merchant_account, + &payment_attempt, + payment_intent, + &mut response, + ) + .await?; + } Ok(services::ApplicationResponse::Json(response)) } diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 38ae71754b87..e130795e945a 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -112,9 +112,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - Some(&card_network_type.card_network), + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + Some(card_network_type.card_network.clone()), + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -138,9 +140,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - None, + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + None, + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -201,15 +205,82 @@ where let surcharge_output = execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; if let Some(surcharge_details) = surcharge_output.surcharge_details { - let surcharge_details_response = get_surcharge_details_from_surcharge_output( + let surcharge_details = get_surcharge_details_from_surcharge_output( surcharge_details, &payment_data.payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_method_type.to_owned().into(), - payment_method_type, - None, - surcharge_details_response, + types::SurchargeKey::PaymentMethodData( + payment_method_type.to_owned().into(), + *payment_method_type, + None, + ), + surcharge_details, + ); + } + } + Ok(surcharge_metadata) +} +pub async fn perform_surcharge_decision_management_for_saved_cards( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], +) -> ConditionalConfigResult { + let mut surcharge_metadata = types::SurchargeMetadata::new(payment_attempt.attempt_id.clone()); + let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id { + id + } else { + return Ok(surcharge_metadata); + }; + + let key = ensure_algorithm_cached( + &*state.store, + &payment_attempt.merchant_id, + algorithm_ref.timestamp, + algorithm_id.as_str(), + ) + .await?; + let cached_algo = CONF_CACHE + .retrieve(&key) + .into_report() + .change_context(ConfigError::CacheMiss) + .attach_printable("Unable to retrieve cached routing algorithm even after refresh")?; + let mut backend_input = make_dsl_input_for_surcharge(payment_attempt, payment_intent, None) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + + for customer_payment_method in customer_payment_method_list.iter_mut() { + backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); + backend_input.payment_method.payment_method_type = + customer_payment_method.payment_method_type; + backend_input.payment_method.card_network = customer_payment_method + .card + .as_ref() + .and_then(|card| card.scheme.as_ref()) + .map(|scheme| { + scheme + .clone() + .parse_enum("CardNetwork") + .change_context(ConfigError::DslExecutionError) + }) + .transpose()?; + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + if let Some(surcharge_details_output) = surcharge_output.surcharge_details { + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details_output, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), + surcharge_details.clone(), + ); + customer_payment_method.surcharge_details = Some( + SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) + .into_report() + .change_context(ConfigError::DslParsingError)?, ); } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2a3ed0f1596f..16fda276f6a5 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -39,11 +39,8 @@ use self::{ helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, - types::SurchargeDetails, -}; -use super::{ - errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, }; +use super::{errors::StorageErrorExt, payment_methods::surcharge_decision_configs}; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -408,26 +405,39 @@ where .surcharge_applicable .unwrap_or(false) { - let payment_method_data = payment_data + let raw_card_key = payment_data .payment_method_data - .clone() - .get_required_value("payment_method_data")?; - let (payment_method, payment_method_type, card_network) = - get_key_params_for_surcharge_details(payment_method_data)?; + .as_ref() + .map(get_key_params_for_surcharge_details) + .transpose()? + .map(|(payment_method, payment_method_type, card_network)| { + types::SurchargeKey::PaymentMethodData( + payment_method, + payment_method_type, + card_network, + ) + }); + let saved_card_key = payment_data.token.clone().map(types::SurchargeKey::Token); - let calculated_surcharge_details = match utils::get_individual_surcharge_detail_from_redis( - state, - &payment_method, - &payment_method_type, - card_network, - &payment_data.payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => Some(surcharge_details), - Err(err) if err.current_context() == &RedisError::NotFound => None, - Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?, - }; + let surcharge_key = raw_card_key + .or(saved_card_key) + .get_required_value("payment_method_data or payment_token")?; + logger::debug!(surcharge_key_confirm =? surcharge_key); + + let calculated_surcharge_details = + match types::SurchargeMetadata::get_individual_surcharge_detail_from_redis( + state, + surcharge_key, + &payment_data.payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => Some(surcharge_details), + Err(err) if err.current_context() == &RedisError::NotFound => None, + Err(err) => { + Err(err).change_context(errors::ApiErrorResponse::InternalServerError)? + } + }; payment_data.surcharge_details = calculated_surcharge_details; } else { @@ -436,7 +446,10 @@ where .payment_attempt .get_surcharge_details() .map(|surcharge_details| { - SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)) + types::SurchargeDetails::from(( + &surcharge_details, + &payment_data.payment_attempt, + )) }); payment_data.surcharge_details = surcharge_details; } @@ -469,7 +482,7 @@ where let final_amount = payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( - SurchargeDetails { + types::SurchargeDetails { surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -501,12 +514,9 @@ where .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing surcharge decision operation")?; - core_utils::persist_individual_surcharge_details_in_redis( - state, - merchant_account, - &surcharge_results, - ) - .await?; + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .await?; Ok(if surcharge_results.is_empty_result() { None @@ -917,6 +927,11 @@ where merchant_connector_account.get_mca_id(); } + operation + .to_domain()? + .populate_payment_data(state, payment_data, merchant_account) + .await?; + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( state, operation, @@ -1846,7 +1861,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, pub incremental_authorization_details: Option, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c9ab77c6a332..4e491964e96c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3626,7 +3626,7 @@ impl ApplePayData { } pub fn get_key_params_for_surcharge_details( - payment_method_data: api_models::payments::PaymentMethodData, + payment_method_data: &api_models::payments::PaymentMethodData, ) -> RouterResult<( common_enums::PaymentMethod, common_enums::PaymentMethodType, @@ -3636,6 +3636,7 @@ pub fn get_key_params_for_surcharge_details( api_models::payments::PaymentMethodData::Card(card) => { let card_network = card .card_network + .clone() .get_required_value("payment_method_data.card.card_network")?; // surcharge generated will always be same for credit as well as debit // since surcharge conditions cannot be defined on card_type diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 578395860c9a..af2a9fa49c8b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -5,7 +5,6 @@ use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; -use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; use tracing_futures::Instrument; @@ -19,7 +18,7 @@ use crate::{ self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, }, - utils::{self as core_utils, get_individual_surcharge_detail_from_redis}, + utils::{self as core_utils}, }, db::StorageInterface, routes::AppState, @@ -439,13 +438,6 @@ impl sm }); - Self::validate_request_surcharge_details_with_session_surcharge_details( - state, - &payment_attempt, - request, - ) - .await?; - let additional_pm_data = request .payment_method_data .as_ref() @@ -904,70 +896,3 @@ impl ValidateRequest RouterResult<()> { - match ( - request.surcharge_details, - request.payment_method_data.as_ref(), - ) { - (Some(request_surcharge_details), Some(payment_method_data)) => { - if let Some(payment_method_type) = - payment_method_data.get_payment_method_type_if_session_token_type() - { - let invalid_surcharge_details_error = Err(errors::ApiErrorResponse::InvalidRequestData { - message: "surcharge_details sent in session token flow doesn't match with the one sent in confirm request".into(), - }.into()); - if let Some(attempt_surcharge_amount) = payment_attempt.surcharge_amount { - // payment_attempt.surcharge_amount will be Some if some surcharge was sent in payment create - // if surcharge was sent in payment create call, the same would have been sent to the connector during session call - // So verify the same - if request_surcharge_details.surcharge_amount != attempt_surcharge_amount - || request_surcharge_details.tax_amount != payment_attempt.tax_amount - { - return invalid_surcharge_details_error; - } - } else { - // if not sent in payment create - // verify that any calculated surcharge sent in session flow is same as the one sent in confirm - return match get_individual_surcharge_detail_from_redis( - state, - &payment_method_type.into(), - &payment_method_type, - None, - &payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => utils::when( - !surcharge_details - .is_request_surcharge_matching(request_surcharge_details), - || invalid_surcharge_details_error, - ), - Err(err) if err.current_context() == &RedisError::NotFound => { - utils::when(!request_surcharge_details.is_surcharge_zero(), || { - invalid_surcharge_details_error - }) - } - Err(err) => Err(err) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch redis value"), - }; - } - } - Ok(()) - } - (Some(_request_surcharge_details), None) => { - Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data", - } - .into()) - } - _ => Ok(()), - } - } -} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 287e4945951b..eb7f31ba24d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -287,7 +287,7 @@ impl let setup_mandate = setup_mandate.map(MandateData::from); let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_method_data_after_card_bin_call = request diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index e1c373171682..f1a35cffce87 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -337,7 +337,7 @@ impl })?; let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f97cdc17d724..001082d2c92e 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -1,13 +1,18 @@ use std::{collections::HashMap, num::TryFromIntError}; use api_models::{payment_methods::SurchargeDetailsResponse, payments::RequestSurchargeDetails}; -use common_utils::{consts, types as common_types}; +use common_utils::{consts, errors::CustomResult, ext_traits::Encode, types as common_types}; use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::{IntoReport, ResultExt}; +use redis_interface::errors::RedisError; +use router_env::{instrument, tracing}; use crate::{ + consts as router_consts, core::errors::{self, RouterResult}, + routes::AppState, types::{ + domain, storage::{self, enums as storage_enums}, transformers::ForeignTryFrom, }, @@ -215,8 +220,8 @@ impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsRe let display_final_amount = currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; Ok(Self { - surcharge: surcharge_details.surcharge.clone(), - tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), + surcharge: surcharge_details.surcharge.clone().into(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone().map(Into::into), display_surcharge_amount, display_tax_on_surcharge_amount, display_total_surcharge_amount: display_surcharge_amount @@ -239,16 +244,19 @@ impl SurchargeDetails { } } +#[derive(Eq, Hash, PartialEq, Clone, Debug, strum::Display)] +pub enum SurchargeKey { + Token(String), + PaymentMethodData( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), +} + #[derive(Clone, Debug)] pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetails, - >, + surcharge_results: HashMap, pub payment_attempt_id: String, } @@ -267,30 +275,14 @@ impl SurchargeMetadata { } pub fn insert_surcharge_details( &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, + surcharge_key: SurchargeKey, surcharge_details: SurchargeDetails, ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); + self.surcharge_results + .insert(surcharge_key, surcharge_details); } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetails> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) + pub fn get_surcharge_details(&self, surcharge_key: SurchargeKey) -> Option<&SurchargeDetails> { + self.surcharge_results.get(&surcharge_key) } pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { format!("surcharge_metadata_{}", payment_attempt_id) @@ -298,25 +290,78 @@ impl SurchargeMetadata { pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { self.surcharge_results .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + .map(|(surcharge_key, surcharge_details)| { + let key = Self::get_surcharge_details_redis_hashset_key(surcharge_key); (key, surcharge_details.to_owned()) }) .collect() } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) + pub fn get_surcharge_details_redis_hashset_key(surcharge_key: &SurchargeKey) -> String { + match surcharge_key { + SurchargeKey::Token(token) => { + format!("token_{}", token) + } + SurchargeKey::PaymentMethodData(payment_method, payment_method_type, card_network) => { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } } } + #[instrument(skip_all)] + pub async fn persist_individual_surcharge_details_in_redis( + &self, + state: &AppState, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult<()> { + if !self.is_empty_result() { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(&self.payment_attempt_id); + + let mut value_list = Vec::with_capacity(self.get_surcharge_results_size()); + for (key, value) in self.get_individual_surcharge_key_value_pairs().into_iter() { + value_list.push(( + key, + Encode::::encode_to_string_of_json(&value) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode to string of json")?, + )); + } + let intent_fulfillment_time = merchant_account + .intent_fulfillment_time + .unwrap_or(router_consts::DEFAULT_FULFILLMENT_TIME); + redis_conn + .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to write to redis")?; + } + Ok(()) + } + + #[instrument(skip_all)] + pub async fn get_individual_surcharge_detail_from_redis( + state: &AppState, + surcharge_key: SurchargeKey, + payment_attempt_id: &str, + ) -> CustomResult { + let redis_conn = state + .store + .get_redis_conn() + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = Self::get_surcharge_details_redis_hashset_key(&surcharge_key); + redis_conn + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") + .await + } } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 6d82c44d803a..724a698ff700 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -4,17 +4,12 @@ use api_models::enums::{DisputeStage, DisputeStatus}; use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{ - errors::CustomResult, - ext_traits::{AsyncExt, Encode}, -}; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use error_stack::{report, IntoReport, ResultExt}; -use euclid::enums as euclid_enums; -use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; -use super::payments::{helpers, types as payments_types, PaymentAddress}; +use super::payments::{helpers, PaymentAddress}; #[cfg(feature = "payouts")] use super::payouts::PayoutData; #[cfg(feature = "payouts")] @@ -1068,71 +1063,6 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } -#[instrument(skip_all)] -pub async fn persist_individual_surcharge_details_in_redis( - state: &AppState, - merchant_account: &domain::MerchantAccount, - surcharge_metadata: &payments_types::SurchargeMetadata, -) -> RouterResult<()> { - if !surcharge_metadata.is_empty_result() { - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - let redis_key = payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key( - &surcharge_metadata.payment_attempt_id, - ); - - let mut value_list = Vec::with_capacity(surcharge_metadata.get_surcharge_results_size()); - for (key, value) in surcharge_metadata - .get_individual_surcharge_key_value_pairs() - .into_iter() - { - value_list.push(( - key, - Encode::::encode_to_string_of_json(&value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode to string of json")?, - )); - } - let intent_fulfillment_time = merchant_account - .intent_fulfillment_time - .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME); - redis_conn - .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to write to redis")?; - } - Ok(()) -} - -#[instrument(skip_all)] -pub async fn get_individual_surcharge_detail_from_redis( - state: &AppState, - payment_method: &euclid_enums::PaymentMethod, - payment_method_type: &euclid_enums::PaymentMethodType, - card_network: Option, - payment_attempt_id: &str, -) -> CustomResult { - let redis_conn = state - .store - .get_redis_conn() - .attach_printable("Failed to get redis connection")?; - let redis_key = - payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = payments_types::SurchargeMetadata::get_surcharge_details_redis_hashset_key( - payment_method, - payment_method_type, - card_network.as_ref(), - ); - - redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") - .await -} - pub fn get_request_incremental_authorization_value( request_incremental_authorization: Option, capture_method: Option, diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 82c98304c62b..d83117c59d76 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -319,6 +319,9 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::IncrementalAuthorizationResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, + api_models::payment_methods::SurchargeDetailsResponse, + api_models::payment_methods::SurchargeResponse, + api_models::payment_methods::SurchargePercentage, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, @@ -363,7 +366,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, - api_models::payments::PaymentLinkObject + api_models::payments::PaymentLinkObject, )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index ea2ea8b701da..c74608ea20a1 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -238,7 +238,11 @@ impl SessionSurchargeDetails { ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata - .get_surcharge_details(payment_method, payment_method_type, card_network) + .get_surcharge_details(payments_types::SurchargeKey::PaymentMethodData( + *payment_method, + *payment_method_type, + card_network.cloned(), + )) .cloned(), Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4f26589ba029..d67089aea35f 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5116,6 +5116,14 @@ ], "nullable": true }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -12043,6 +12051,106 @@ } } }, + "SurchargeDetailsResponse": { + "type": "object", + "required": [ + "surcharge", + "display_surcharge_amount", + "display_tax_on_surcharge_amount", + "display_total_surcharge_amount", + "display_final_amount" + ], + "properties": { + "surcharge": { + "$ref": "#/components/schemas/SurchargeResponse" + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargePercentage" + } + ], + "nullable": true + }, + "display_surcharge_amount": { + "type": "number", + "format": "double", + "description": "surcharge amount for this payment" + }, + "display_tax_on_surcharge_amount": { + "type": "number", + "format": "double", + "description": "tax on surcharge amount for this payment" + }, + "display_total_surcharge_amount": { + "type": "number", + "format": "double", + "description": "sum of display_surcharge_amount and display_tax_on_surcharge_amount" + }, + "display_final_amount": { + "type": "number", + "format": "double", + "description": "sum of original amount," + } + } + }, + "SurchargePercentage": { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "number", + "format": "float" + } + } + }, + "SurchargeResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixed" + ] + }, + "value": { + "type": "integer", + "format": "int64", + "description": "Fixed Surcharge value" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "rate" + ] + }, + "value": { + "$ref": "#/components/schemas/SurchargePercentage" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "SwishQrData": { "type": "object" }, From 298e3627c379de5acfcafb074036754661801f1e Mon Sep 17 00:00:00 2001 From: SamraatBansal <55536657+SamraatBansal@users.noreply.github.com> Date: Tue, 5 Dec 2023 01:47:55 +0530 Subject: [PATCH 11/18] fix: transform connector name to lowercase in connector integration script (#3048) --- scripts/add_connector.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/add_connector.sh b/scripts/add_connector.sh index 7ed5e65151e1..1246c51d8eb3 100755 --- a/scripts/add_connector.sh +++ b/scripts/add_connector.sh @@ -25,7 +25,7 @@ function find_prev_connector() { eval "$2='aci'" } -payment_gateway=$1; +payment_gateway=$(echo $1 | tr '[:upper:]' '[:lower:]') base_url=$2; payment_gateway_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${payment_gateway:0:1})${payment_gateway:1}" src="crates/router/src" @@ -49,7 +49,7 @@ git checkout $conn.rs $src/types/api.rs $src/configs/settings.rs config/developm # Add enum for this connector in required places previous_connector='' -find_prev_connector $1 previous_connector +find_prev_connector $payment_gateway previous_connector previous_connector_camelcase="$(tr '[:lower:]' '[:upper:]' <<< ${previous_connector:0:1})${previous_connector:1}" sed -i'' -e "s|pub mod $previous_connector;|pub mod $previous_connector;\npub mod ${payment_gateway};|" $conn.rs sed -i'' -e "s/};/${payment_gateway}::${payment_gateway_camelcase},\n};/" $conn.rs From ba392f58b2956d67e93a08853bcf2270a869be27 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Tue, 5 Dec 2023 12:47:37 +0530 Subject: [PATCH 12/18] 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 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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( From 7bd6e05c0c05ebae9b82a6f410e61ca4409d088b Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:29:10 +0530 Subject: [PATCH 18/18] feat(connector_onboarding): Add Connector onboarding APIs (#3050) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 6 + config/development.toml | 8 +- config/docker_compose.toml | 6 + crates/api_models/src/connector_onboarding.rs | 54 ++++ crates/api_models/src/events.rs | 1 + .../src/events/connector_onboarding.rs | 12 + crates/api_models/src/lib.rs | 1 + crates/router/src/configs/kms.rs | 33 +++ crates/router/src/configs/settings.rs | 17 ++ crates/router/src/core.rs | 2 + .../router/src/core/connector_onboarding.rs | 96 +++++++ .../src/core/connector_onboarding/paypal.rs | 174 ++++++++++++ crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 8 +- crates/router/src/routes/app.rs | 26 +- .../router/src/routes/connector_onboarding.rs | 47 ++++ crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/types/api.rs | 2 + .../src/types/api/connector_onboarding.rs | 1 + .../types/api/connector_onboarding/paypal.rs | 247 ++++++++++++++++++ crates/router/src/utils.rs | 2 + .../router/src/utils/connector_onboarding.rs | 36 +++ .../src/utils/connector_onboarding/paypal.rs | 89 +++++++ crates/router_env/src/logger/types.rs | 4 + loadtest/config/development.toml | 6 + 25 files changed, 876 insertions(+), 6 deletions(-) create mode 100644 crates/api_models/src/connector_onboarding.rs create mode 100644 crates/api_models/src/events/connector_onboarding.rs create mode 100644 crates/router/src/core/connector_onboarding.rs create mode 100644 crates/router/src/core/connector_onboarding/paypal.rs create mode 100644 crates/router/src/routes/connector_onboarding.rs create mode 100644 crates/router/src/types/api/connector_onboarding.rs create mode 100644 crates/router/src/types/api/connector_onboarding/paypal.rs create mode 100644 crates/router/src/utils/connector_onboarding.rs create mode 100644 crates/router/src/utils/connector_onboarding/paypal.rs diff --git a/config/config.example.toml b/config/config.example.toml index d935a4e7f20d..fad4da3e7c36 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -477,3 +477,9 @@ connection_timeout = 10 # Timeout for database connection in seconds [kv_config] # TTL for KV in seconds ttl = 900 + +[paypal_onboarding] +client_id = "paypal_client_id" # Client ID for PayPal onboarding +client_secret = "paypal_secret_key" # Secret key for PayPal onboarding +partner_id = "paypal_partner_id" # Partner ID for PayPal onboarding +enabled = true # Switch to enable or disable PayPal onboarding diff --git a/config/development.toml b/config/development.toml index fa5fddb0d60a..2eb8b00b9c08 100644 --- a/config/development.toml +++ b/config/development.toml @@ -504,4 +504,10 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 -queue_strategy = "Fifo" \ No newline at end of file +queue_strategy = "Fifo" + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 4d50600e1bf8..de90f3c70abd 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -362,3 +362,9 @@ queue_strategy = "Fifo" [kv_config] ttl = 900 # 15 * 60 seconds + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true diff --git a/crates/api_models/src/connector_onboarding.rs b/crates/api_models/src/connector_onboarding.rs new file mode 100644 index 000000000000..759d3cb97f13 --- /dev/null +++ b/crates/api_models/src/connector_onboarding.rs @@ -0,0 +1,54 @@ +use super::{admin, enums}; + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct ActionUrlRequest { + pub connector: enums::Connector, + pub connector_id: String, + pub return_url: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum ActionUrlResponse { + PayPal(PayPalActionUrlResponse), +} + +#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)] +pub struct OnboardingSyncRequest { + pub profile_id: String, + pub connector_id: String, + pub connector: enums::Connector, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalActionUrlResponse { + pub action_url: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum OnboardingStatus { + PayPal(PayPalOnboardingStatus), +} + +#[derive(serde::Serialize, Debug, Clone)] +#[serde(rename_all = "snake_case")] +pub enum PayPalOnboardingStatus { + AccountNotFound, + PaymentsNotReceivable, + PpcpCustomDenied, + MorePermissionsNeeded, + EmailNotVerified, + Success(PayPalOnboardingDone), + ConnectorIntegrated(admin::MerchantConnectorResponse), +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalOnboardingDone { + pub payer_id: String, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct PayPalIntegrationDone { + pub connector_id: String, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index ac7cdeb83d94..457d3fde05b7 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -1,3 +1,4 @@ +pub mod connector_onboarding; pub mod customer; pub mod gsm; mod locker_migration; diff --git a/crates/api_models/src/events/connector_onboarding.rs b/crates/api_models/src/events/connector_onboarding.rs new file mode 100644 index 000000000000..998dc384d620 --- /dev/null +++ b/crates/api_models/src/events/connector_onboarding.rs @@ -0,0 +1,12 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::connector_onboarding::{ + ActionUrlRequest, ActionUrlResponse, OnboardingStatus, OnboardingSyncRequest, +}; + +common_utils::impl_misc_api_event_type!( + ActionUrlRequest, + ActionUrlResponse, + OnboardingSyncRequest, + OnboardingStatus +); diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 056888839a54..ce3c11d9c2f3 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -5,6 +5,7 @@ pub mod api_keys; pub mod bank_accounts; pub mod cards_info; pub mod conditional_configs; +pub mod connector_onboarding; pub mod currency; pub mod customers; pub mod disputes; diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 37f2d15774a5..bf6ee44d28be 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -69,3 +69,36 @@ impl KmsDecrypt for settings::Database { }) } } + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl KmsDecrypt for settings::PayPalOnboarding { + type Output = Self; + + async fn decrypt_inner( + mut self, + kms_client: &KmsClient, + ) -> CustomResult { + self.client_id = kms_client.decrypt(self.client_id.expose()).await?.into(); + self.client_secret = kms_client + .decrypt(self.client_secret.expose()) + .await? + .into(); + self.partner_id = kms_client.decrypt(self.partner_id.expose()).await?.into(); + Ok(self) + } +} + +#[cfg(feature = "olap")] +#[async_trait::async_trait] +impl KmsDecrypt for settings::ConnectorOnboarding { + type Output = Self; + + async fn decrypt_inner( + mut self, + kms_client: &KmsClient, + ) -> CustomResult { + self.paypal = self.paypal.decrypt_inner(kms_client).await?; + Ok(self) + } +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f2d962b0abee..68af91d06612 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -116,6 +116,8 @@ pub struct Settings { #[cfg(feature = "olap")] pub report_download_config: ReportConfig, pub events: EventsConfig, + #[cfg(feature = "olap")] + pub connector_onboarding: ConnectorOnboarding, } #[derive(Debug, Deserialize, Clone)] @@ -884,3 +886,18 @@ impl<'de> Deserialize<'de> for LockSettings { }) } } + +#[cfg(feature = "olap")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ConnectorOnboarding { + pub paypal: PayPalOnboarding, +} + +#[cfg(feature = "olap")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct PayPalOnboarding { + pub client_id: masking::Secret, + pub client_secret: masking::Secret, + pub partner_id: masking::Secret, + pub enabled: bool, +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 08de9cf80384..6a167be48dae 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -5,6 +5,8 @@ pub mod cache; pub mod cards_info; pub mod conditional_config; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; #[cfg(any(feature = "olap", feature = "oltp"))] pub mod currency; pub mod customers; diff --git a/crates/router/src/core/connector_onboarding.rs b/crates/router/src/core/connector_onboarding.rs new file mode 100644 index 000000000000..e48026edc2d5 --- /dev/null +++ b/crates/router/src/core/connector_onboarding.rs @@ -0,0 +1,96 @@ +use api_models::{connector_onboarding as api, enums}; +use error_stack::ResultExt; +use masking::Secret; + +use crate::{ + core::errors::{ApiErrorResponse, RouterResponse, RouterResult}, + services::{authentication as auth, ApplicationResponse}, + types::{self as oss_types}, + utils::connector_onboarding as utils, + AppState, +}; + +pub mod paypal; + +#[async_trait::async_trait] +pub trait AccessToken { + async fn access_token(state: &AppState) -> RouterResult; +} + +pub async fn get_action_url( + state: AppState, + request: api::ActionUrlRequest, +) -> RouterResponse { + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + + match (is_enabled, request.connector) { + (Some(true), enums::Connector::Paypal) => { + let action_url = Box::pin(paypal::get_action_url_from_paypal( + state, + request.connector_id, + request.return_url, + )) + .await?; + Ok(ApplicationResponse::Json(api::ActionUrlResponse::PayPal( + api::PayPalActionUrlResponse { action_url }, + ))) + } + _ => Err(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: request.connector.to_string(), + } + .into()), + } +} + +pub async fn sync_onboarding_status( + state: AppState, + user_from_token: auth::UserFromToken, + request: api::OnboardingSyncRequest, +) -> RouterResponse { + let merchant_account = user_from_token + .get_merchant_account(state.clone()) + .await + .change_context(ApiErrorResponse::MerchantAccountNotFound)?; + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let is_enabled = utils::is_enabled(request.connector, &connector_onboarding_conf); + + match (is_enabled, request.connector) { + (Some(true), enums::Connector::Paypal) => { + let status = Box::pin(paypal::sync_merchant_onboarding_status( + state.clone(), + request.connector_id.clone(), + )) + .await?; + if let api::OnboardingStatus::PayPal(api::PayPalOnboardingStatus::Success( + ref inner_data, + )) = status + { + let connector_onboarding_conf = state.conf.connector_onboarding.clone(); + let auth_details = oss_types::ConnectorAuthType::SignatureKey { + api_key: connector_onboarding_conf.paypal.client_secret, + key1: connector_onboarding_conf.paypal.client_id, + api_secret: Secret::new(inner_data.payer_id.clone()), + }; + let some_data = paypal::update_mca( + &state, + &merchant_account, + request.connector_id.to_owned(), + auth_details, + ) + .await?; + + return Ok(ApplicationResponse::Json(api::OnboardingStatus::PayPal( + api::PayPalOnboardingStatus::ConnectorIntegrated(some_data), + ))); + } + Ok(ApplicationResponse::Json(status)) + } + _ => Err(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: request.connector.to_string(), + } + .into()), + } +} diff --git a/crates/router/src/core/connector_onboarding/paypal.rs b/crates/router/src/core/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..30aa69067b5d --- /dev/null +++ b/crates/router/src/core/connector_onboarding/paypal.rs @@ -0,0 +1,174 @@ +use api_models::{admin::MerchantConnectorUpdate, connector_onboarding as api}; +use common_utils::ext_traits::Encode; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface, Secret}; + +use crate::{ + core::{ + admin, + errors::{ApiErrorResponse, RouterResult}, + }, + services::{send_request, ApplicationResponse, Request}, + types::{self as oss_types, api as oss_api_types, api::connector_onboarding as types}, + utils::connector_onboarding as utils, + AppState, +}; + +fn build_referral_url(state: AppState) -> String { + format!( + "{}v2/customer/partner-referrals", + state.conf.connectors.paypal.base_url + ) +} + +async fn build_referral_request( + state: AppState, + connector_id: String, + return_url: String, +) -> RouterResult { + let access_token = utils::paypal::generate_access_token(state.clone()).await?; + let request_body = types::paypal::PartnerReferralRequest::new(connector_id, return_url); + + utils::paypal::build_paypal_post_request( + build_referral_url(state), + request_body, + access_token.token.expose(), + ) +} + +pub async fn get_action_url_from_paypal( + state: AppState, + connector_id: String, + return_url: String, +) -> RouterResult { + let referral_request = Box::pin(build_referral_request( + state.clone(), + connector_id, + return_url, + )) + .await?; + let referral_response = send_request(&state, referral_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal referrals")?; + + let parsed_response: types::paypal::PartnerReferralResponse = referral_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal response")?; + + parsed_response.extract_action_url() +} + +fn merchant_onboarding_status_url(state: AppState, tracking_id: String) -> String { + let partner_id = state.conf.connector_onboarding.paypal.partner_id.to_owned(); + format!( + "{}v1/customer/partners/{}/merchant-integrations?tracking_id={}", + state.conf.connectors.paypal.base_url, + partner_id.expose(), + tracking_id + ) +} + +pub async fn sync_merchant_onboarding_status( + state: AppState, + tracking_id: String, +) -> RouterResult { + let access_token = utils::paypal::generate_access_token(state.clone()).await?; + + let Some(seller_status_response) = + find_paypal_merchant_by_tracking_id(state.clone(), tracking_id, &access_token).await? + else { + return Ok(api::OnboardingStatus::PayPal( + api::PayPalOnboardingStatus::AccountNotFound, + )); + }; + + let merchant_details_url = seller_status_response + .extract_merchant_details_url(&state.conf.connectors.paypal.base_url)?; + + let merchant_details_request = + utils::paypal::build_paypal_get_request(merchant_details_url, access_token.token.expose())?; + + let merchant_details_response = send_request(&state, merchant_details_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal merchant details")?; + + let parsed_response: types::paypal::SellerStatusDetailsResponse = merchant_details_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal merchant details response")?; + + let eligibity = parsed_response.get_eligibility_status().await?; + Ok(api::OnboardingStatus::PayPal(eligibity)) +} + +async fn find_paypal_merchant_by_tracking_id( + state: AppState, + tracking_id: String, + access_token: &oss_types::AccessToken, +) -> RouterResult> { + let seller_status_request = utils::paypal::build_paypal_get_request( + merchant_onboarding_status_url(state.clone(), tracking_id), + access_token.token.peek().to_string(), + )?; + let seller_status_response = send_request(&state, seller_status_request, None) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to send request to paypal onboarding status")?; + + if seller_status_response.status().is_success() { + return Ok(Some( + seller_status_response + .json() + .await + .into_report() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse paypal onboarding status response")?, + )); + } + Ok(None) +} + +pub async fn update_mca( + state: &AppState, + merchant_account: &oss_types::domain::MerchantAccount, + connector_id: String, + auth_details: oss_types::ConnectorAuthType, +) -> RouterResult { + let connector_auth_json = + Encode::::encode_to_value(&auth_details) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Error while deserializing connector_account_details")?; + + let request = MerchantConnectorUpdate { + connector_type: common_enums::ConnectorType::PaymentProcessor, + connector_account_details: Some(Secret::new(connector_auth_json)), + disabled: Some(false), + status: Some(common_enums::ConnectorStatus::Active), + test_mode: None, + connector_label: None, + payment_methods_enabled: None, + metadata: None, + frm_configs: None, + connector_webhook_details: None, + pm_auth_config: None, + }; + let mca_response = admin::update_payment_connector( + state.clone(), + &merchant_account.merchant_id, + &connector_id, + request, + ) + .await?; + + match mca_response { + ApplicationResponse::Json(mca_data) => Ok(mca_data), + _ => Err(ApiErrorResponse::InternalServerError.into()), + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index fb8be9636748..3b4c7ce9b7d3 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -147,6 +147,7 @@ pub fn mk_app( .service(routes::Gsm::server(state.clone())) .service(routes::PaymentLink::server(state.clone())) .service(routes::User::server(state.clone())) + .service(routes::ConnectorOnboarding::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index b19ef5d7016b..9b3006692d34 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -4,6 +4,8 @@ pub mod app; pub mod cache; pub mod cards_info; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; #[cfg(any(feature = "olap", feature = "oltp"))] pub mod currency; pub mod customers; @@ -47,9 +49,9 @@ pub use self::app::Routing; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ - ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, - Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, MerchantConnectorAccount, - PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, + ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, ConnectorOnboarding, Customers, + Disputes, EphemeralKey, Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, + MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index acf98c658a7c..9739d18864b8 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -26,8 +26,8 @@ use super::routing as cloud_routing; use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] use super::{ - admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, payment_link::*, - user::*, user_role::*, + admin::*, api_keys::*, connector_onboarding::*, disputes::*, files::*, gsm::*, + locker_migration, payment_link::*, user::*, user_role::*, }; use super::{cache::*, health::*}; #[cfg(any(feature = "olap", feature = "oltp"))] @@ -185,6 +185,16 @@ impl AppState { } }; + #[cfg(all(feature = "kms", feature = "olap"))] + #[allow(clippy::expect_used)] + { + conf.connector_onboarding = conf + .connector_onboarding + .decrypt_inner(kms_client) + .await + .expect("Failed to decrypt connector onboarding credentials"); + } + #[cfg(feature = "olap")] let pool = crate::analytics::AnalyticsProvider::from_conf(&conf.analytics).await; @@ -888,3 +898,15 @@ impl LockerMigrate { ) } } + +pub struct ConnectorOnboarding; + +#[cfg(feature = "olap")] +impl ConnectorOnboarding { + pub fn server(state: AppState) -> Scope { + web::scope("/connector_onboarding") + .app_data(web::Data::new(state)) + .service(web::resource("/action_url").route(web::post().to(get_action_url))) + .service(web::resource("/sync").route(web::post().to(sync_onboarding_status))) + } +} diff --git a/crates/router/src/routes/connector_onboarding.rs b/crates/router/src/routes/connector_onboarding.rs new file mode 100644 index 000000000000..b7c39b3c1d2e --- /dev/null +++ b/crates/router/src/routes/connector_onboarding.rs @@ -0,0 +1,47 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::connector_onboarding as api_types; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, connector_onboarding as core}, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn get_action_url( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::GetActionUrl; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _: auth::UserFromToken, req| core::get_action_url(state, req), + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn sync_onboarding_status( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::SyncOnboardingStatus; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + core::sync_onboarding_status, + &auth::JWTAuth(Permission::MerchantAccountWrite), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 0c850922fff4..dcae11f58b76 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -28,6 +28,7 @@ pub enum ApiIdentifier { Gsm, User, UserRole, + ConnectorOnboarding, } impl From for ApiIdentifier { @@ -171,6 +172,8 @@ impl From for ApiIdentifier { Flow::ListRoles | Flow::GetRole | Flow::UpdateUserRole | Flow::GetAuthorizationInfo => { Self::UserRole } + + Flow::GetActionUrl | Flow::SyncOnboardingStatus => Self::ConnectorOnboarding, } } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index c74608ea20a1..0ec158199cea 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -1,6 +1,8 @@ pub mod admin; pub mod api_keys; pub mod configs; +#[cfg(feature = "olap")] +pub mod connector_onboarding; pub mod customers; pub mod disputes; pub mod enums; diff --git a/crates/router/src/types/api/connector_onboarding.rs b/crates/router/src/types/api/connector_onboarding.rs new file mode 100644 index 000000000000..5b1d581a20ef --- /dev/null +++ b/crates/router/src/types/api/connector_onboarding.rs @@ -0,0 +1 @@ +pub mod paypal; diff --git a/crates/router/src/types/api/connector_onboarding/paypal.rs b/crates/router/src/types/api/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..0cc026d4d7ad --- /dev/null +++ b/crates/router/src/types/api/connector_onboarding/paypal.rs @@ -0,0 +1,247 @@ +use api_models::connector_onboarding as api; +use error_stack::{IntoReport, ResultExt}; + +use crate::core::errors::{ApiErrorResponse, RouterResult}; + +#[derive(serde::Deserialize, Debug)] +pub struct HateoasLink { + pub href: String, + pub rel: String, + pub method: String, +} + +#[derive(serde::Deserialize, Debug)] +pub struct PartnerReferralResponse { + pub links: Vec, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralRequest { + pub tracking_id: String, + pub operations: Vec, + pub products: Vec, + pub capabilities: Vec, + pub partner_config_override: PartnerConfigOverride, + pub legal_consents: Vec, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalProducts { + Ppcp, + AdvancedVaulting, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalCapabilities { + PaypalWalletVaultingAdvanced, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralOperations { + pub operation: PayPalReferralOperationType, + pub api_integration_preference: PartnerReferralIntegrationPreference, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalReferralOperationType { + ApiIntegration, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralIntegrationPreference { + pub rest_api_integration: PartnerReferralRestApiIntegration, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralRestApiIntegration { + pub integration_method: IntegrationMethod, + pub integration_type: PayPalIntegrationType, + pub third_party_details: PartnerReferralThirdPartyDetails, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum IntegrationMethod { + Paypal, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalIntegrationType { + ThirdParty, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerReferralThirdPartyDetails { + pub features: Vec, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PayPalFeatures { + Payment, + Refund, + Vault, + AccessMerchantInformation, + BillingAgreement, + ReadSellerDispute, +} + +#[derive(serde::Serialize, Debug)] +pub struct PartnerConfigOverride { + pub partner_logo_url: String, + pub return_url: String, +} + +#[derive(serde::Serialize, Debug)] +pub struct LegalConsent { + #[serde(rename = "type")] + pub consent_type: LegalConsentType, + pub granted: bool, +} + +#[derive(serde::Serialize, Debug)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum LegalConsentType { + ShareDataConsent, +} + +impl PartnerReferralRequest { + pub fn new(tracking_id: String, return_url: String) -> Self { + Self { + tracking_id, + operations: vec![PartnerReferralOperations { + operation: PayPalReferralOperationType::ApiIntegration, + api_integration_preference: PartnerReferralIntegrationPreference { + rest_api_integration: PartnerReferralRestApiIntegration { + integration_method: IntegrationMethod::Paypal, + integration_type: PayPalIntegrationType::ThirdParty, + third_party_details: PartnerReferralThirdPartyDetails { + features: vec![ + PayPalFeatures::Payment, + PayPalFeatures::Refund, + PayPalFeatures::Vault, + PayPalFeatures::AccessMerchantInformation, + PayPalFeatures::BillingAgreement, + PayPalFeatures::ReadSellerDispute, + ], + }, + }, + }, + }], + products: vec![PayPalProducts::Ppcp, PayPalProducts::AdvancedVaulting], + capabilities: vec![PayPalCapabilities::PaypalWalletVaultingAdvanced], + partner_config_override: PartnerConfigOverride { + partner_logo_url: "https://hyperswitch.io/img/websiteIcon.svg".to_string(), + return_url, + }, + legal_consents: vec![LegalConsent { + consent_type: LegalConsentType::ShareDataConsent, + granted: true, + }], + } + } +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusResponse { + pub merchant_id: String, + pub links: Vec, +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusDetailsResponse { + pub merchant_id: String, + pub primary_email_confirmed: bool, + pub payments_receivable: bool, + pub products: Vec, +} + +#[derive(serde::Deserialize, Debug)] +pub struct SellerStatusProducts { + pub name: String, + pub vetting_status: Option, +} + +#[derive(serde::Deserialize, Debug, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum VettingStatus { + NeedMoreData, + Subscribed, + Denied, +} + +impl SellerStatusResponse { + pub fn extract_merchant_details_url(self, paypal_base_url: &str) -> RouterResult { + self.links + .get(0) + .and_then(|link| link.href.strip_prefix('/')) + .map(|link| format!("{}{}", paypal_base_url, link)) + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Merchant details not received in onboarding status") + } +} + +impl SellerStatusDetailsResponse { + pub fn check_payments_receivable(&self) -> Option { + if !self.payments_receivable { + return Some(api::PayPalOnboardingStatus::PaymentsNotReceivable); + } + None + } + + pub fn check_ppcp_custom_status(&self) -> Option { + match self.get_ppcp_custom_status() { + Some(VettingStatus::Denied) => Some(api::PayPalOnboardingStatus::PpcpCustomDenied), + Some(VettingStatus::Subscribed) => None, + _ => Some(api::PayPalOnboardingStatus::MorePermissionsNeeded), + } + } + + fn check_email_confirmation(&self) -> Option { + if !self.primary_email_confirmed { + return Some(api::PayPalOnboardingStatus::EmailNotVerified); + } + None + } + + pub async fn get_eligibility_status(&self) -> RouterResult { + Ok(self + .check_payments_receivable() + .or(self.check_email_confirmation()) + .or(self.check_ppcp_custom_status()) + .unwrap_or(api::PayPalOnboardingStatus::Success( + api::PayPalOnboardingDone { + payer_id: self.get_payer_id(), + }, + ))) + } + + fn get_ppcp_custom_status(&self) -> Option { + self.products + .iter() + .find(|product| product.name == "PPCP_CUSTOM") + .and_then(|ppcp_custom| ppcp_custom.vetting_status.clone()) + } + + fn get_payer_id(&self) -> String { + self.merchant_id.to_string() + } +} + +impl PartnerReferralResponse { + pub fn extract_action_url(self) -> RouterResult { + Ok(self + .links + .into_iter() + .find(|hateoas_link| hateoas_link.rel == "action_url") + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Failed to get action_url from paypal response")? + .href) + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index f1590342e17c..42116e1ecbf0 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "olap")] +pub mod connector_onboarding; pub mod currency; pub mod custom_serde; pub mod db_utils; diff --git a/crates/router/src/utils/connector_onboarding.rs b/crates/router/src/utils/connector_onboarding.rs new file mode 100644 index 000000000000..e8afcd68a468 --- /dev/null +++ b/crates/router/src/utils/connector_onboarding.rs @@ -0,0 +1,36 @@ +use crate::{ + core::errors::{api_error_response::NotImplementedMessage, ApiErrorResponse, RouterResult}, + routes::app::settings, + types::{self, api::enums}, +}; + +pub mod paypal; + +pub fn get_connector_auth( + connector: enums::Connector, + connector_data: &settings::ConnectorOnboarding, +) -> RouterResult { + match connector { + enums::Connector::Paypal => Ok(types::ConnectorAuthType::BodyKey { + api_key: connector_data.paypal.client_secret.clone(), + key1: connector_data.paypal.client_id.clone(), + }), + _ => Err(ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason(format!( + "Onboarding is not implemented for {}", + connector + )), + } + .into()), + } +} + +pub fn is_enabled( + connector: types::Connector, + conf: &settings::ConnectorOnboarding, +) -> Option { + match connector { + enums::Connector::Paypal => Some(conf.paypal.enabled), + _ => None, + } +} diff --git a/crates/router/src/utils/connector_onboarding/paypal.rs b/crates/router/src/utils/connector_onboarding/paypal.rs new file mode 100644 index 000000000000..c803775be071 --- /dev/null +++ b/crates/router/src/utils/connector_onboarding/paypal.rs @@ -0,0 +1,89 @@ +use common_utils::{ + ext_traits::Encode, + request::{Method, Request, RequestBuilder}, +}; +use error_stack::{IntoReport, ResultExt}; +use http::header; +use serde_json::json; + +use crate::{ + connector, + core::errors::{ApiErrorResponse, RouterResult}, + routes::AppState, + types, + types::api::{ + enums, + verify_connector::{self as verify_connector_types, VerifyConnector}, + }, + utils::verify_connector as verify_connector_utils, +}; + +pub async fn generate_access_token(state: AppState) -> RouterResult { + let connector = enums::Connector::Paypal; + let boxed_connector = types::api::ConnectorData::convert_connector( + &state.conf.connectors, + connector.to_string().as_str(), + )?; + let connector_auth = super::get_connector_auth(connector, &state.conf.connector_onboarding)?; + + connector::Paypal::get_access_token( + &state, + verify_connector_types::VerifyConnectorData { + connector: *boxed_connector, + connector_auth, + card_details: verify_connector_utils::get_test_card_details(connector)? + .ok_or(ApiErrorResponse::FlowNotSupported { + flow: "Connector onboarding".to_string(), + connector: connector.to_string(), + }) + .into_report()?, + }, + ) + .await? + .ok_or(ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Error occurred while retrieving access token") +} + +pub fn build_paypal_post_request( + url: String, + body: T, + access_token: String, +) -> RouterResult +where + T: serde::Serialize, +{ + let body = types::RequestBody::log_and_get_request_body( + &json!(body), + Encode::::encode_to_string_of_json, + ) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to build request body")?; + + Ok(RequestBuilder::new() + .method(Method::Post) + .url(&url) + .attach_default_headers() + .header( + header::AUTHORIZATION.to_string().as_str(), + format!("Bearer {}", access_token).as_str(), + ) + .header( + header::CONTENT_TYPE.to_string().as_str(), + "application/json", + ) + .body(Some(body)) + .build()) +} + +pub fn build_paypal_get_request(url: String, access_token: String) -> RouterResult { + Ok(RequestBuilder::new() + .method(Method::Get) + .url(&url) + .attach_default_headers() + .header( + header::AUTHORIZATION.to_string().as_str(), + format!("Bearer {}", access_token).as_str(), + ) + .build()) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index d35090551de7..4948bdd575b3 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -301,6 +301,10 @@ pub enum Flow { InviteUser, /// Incremental Authorization flow PaymentsIncrementalAuthorization, + /// Get action URL for connector onboarding + GetActionUrl, + /// Sync connector onboarding status + SyncOnboardingStatus, } /// diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index bec1074b99d0..2159d2d7994f 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -262,3 +262,9 @@ connection_timeout = 10 [kv_config] ttl = 300 # 5 * 60 seconds + +[connector_onboarding.paypal] +client_id = "" +client_secret = "" +partner_id = "" +enabled = true