diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 89a187faab..9194ac5167 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -59,7 +59,8 @@ pub struct MerchantAccountCreate { #[schema(default = false, example = true)] pub enable_payment_response_hash: Option, - /// Refers to the hash key used for payment response + /// Refers to the hash key used for calculating the signature for webhooks and redirect response + /// If the value is not provided, a default value is used pub payment_response_hash_key: Option, /// A boolean value to indicate if redirect to merchant with http post needs to be enabled @@ -927,3 +928,172 @@ pub enum PayoutRoutingAlgorithm { pub enum PayoutStraightThroughAlgorithm { Single(api_enums::PayoutConnectors), } + +#[derive(Clone, Debug, Deserialize, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct BusinessProfileCreate { + /// A short name to identify the business profile + #[schema(max_length = 64)] + pub profile_name: Option, + + /// The URL to redirect after the completion of the operation, This will be applied to all the + /// connector accounts under this profile + #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] + pub return_url: Option, + + /// A boolean value to indicate if payment response hash needs to be enabled + #[schema(default = true, example = true)] + pub enable_payment_response_hash: Option, + + /// Refers to the hash key used for calculating the signature for webhooks and redirect response + /// If the value is not provided, a default value is used + pub payment_response_hash_key: Option, + + /// A boolean value to indicate if redirect to merchant with http post needs to be enabled + #[schema(default = false, example = true)] + pub redirect_to_merchant_with_http_post: Option, + + /// Webhook related details + pub webhook_details: Option, + + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] + pub metadata: Option, + + /// The routing algorithm to be used for routing payments to desired connectors + #[schema(value_type = Option,example = json!({"type": "single", "data": "stripe"}))] + pub routing_algorithm: Option, + + ///Will be used to expire client secret after certain amount of time to be supplied in seconds + ///(900) for 15 mins + #[schema(example = 900)] + pub intent_fulfillment_time: Option, + + /// The frm routing algorithm to be used for routing payments to desired FRM's + #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] + pub frm_routing_algorithm: Option, + + /// The routing algorithm to be used for routing payouts to desired connectors + #[cfg(feature = "payouts")] + #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] + #[serde( + default, + deserialize_with = "payout_routing_algorithm::deserialize_option" + )] + pub payout_routing_algorithm: Option, +} + +#[derive(Clone, Debug, ToSchema, Serialize)] +pub struct BusinessProfileResponse { + /// The identifier for Merchant Account + #[schema(max_length = 64, example = "y3oqhf46pyzuxjbcn2giaqnb44")] + pub merchant_id: String, + + /// The unique identifier for Business Profile + #[schema(max_length = 64, example = "pro_abcdefghijklmnopqrstuvwxyz")] + pub profile_id: String, + + /// A short name to identify the business profile + #[schema(max_length = 64)] + pub profile_name: String, + + /// The URL to redirect after the completion of the operation, This will be applied to all the + /// connector accounts under this profile + #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] + pub return_url: Option, + + /// A boolean value to indicate if payment response hash needs to be enabled + #[schema(default = true, example = true)] + pub enable_payment_response_hash: bool, + + /// Refers to the hash key used for calculating the signature for webhooks and redirect response + /// If the value is not provided, a default value is used + pub payment_response_hash_key: Option, + + /// A boolean value to indicate if redirect to merchant with http post needs to be enabled + #[schema(default = false, example = true)] + pub redirect_to_merchant_with_http_post: bool, + + /// Webhook related details + pub webhook_details: Option, + + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] + pub metadata: Option, + + /// The routing algorithm to be used for routing payments to desired connectors + #[schema(value_type = Option,example = json!({"type": "single", "data": "stripe"}))] + pub routing_algorithm: Option, + + ///Will be used to expire client secret after certain amount of time to be supplied in seconds + ///(900) for 15 mins + #[schema(example = 900)] + pub intent_fulfillment_time: Option, + + /// The frm routing algorithm to be used for routing payments to desired FRM's + #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] + pub frm_routing_algorithm: Option, + + /// The routing algorithm to be used for routing payouts to desired connectors + #[cfg(feature = "payouts")] + #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] + #[serde( + default, + deserialize_with = "payout_routing_algorithm::deserialize_option" + )] + pub payout_routing_algorithm: Option, +} + +#[derive(Clone, Debug, Deserialize, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct BusinessProfileUpdate { + /// A short name to identify the business profile + #[schema(max_length = 64)] + pub profile_name: Option, + + /// The URL to redirect after the completion of the operation, This will be applied to all the + /// connector accounts under this profile + #[schema(value_type = Option, max_length = 255, example = "https://www.example.com/success")] + pub return_url: Option, + + /// A boolean value to indicate if payment response hash needs to be enabled + #[schema(default = true, example = true)] + pub enable_payment_response_hash: Option, + + /// Refers to the hash key used for calculating the signature for webhooks and redirect response + /// If the value is not provided, a default value is used + pub payment_response_hash_key: Option, + + /// A boolean value to indicate if redirect to merchant with http post needs to be enabled + #[schema(default = false, example = true)] + pub redirect_to_merchant_with_http_post: Option, + + /// Webhook related details + pub webhook_details: Option, + + /// You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long. Metadata is useful for storing additional, structured information on an object. + #[schema(value_type = Option, example = r#"{ "city": "NY", "unit": "245" }"#)] + pub metadata: Option, + + /// The routing algorithm to be used for routing payments to desired connectors + #[schema(value_type = Option,example = json!({"type": "single", "data": "stripe"}))] + pub routing_algorithm: Option, + + ///Will be used to expire client secret after certain amount of time to be supplied in seconds + ///(900) for 15 mins + #[schema(example = 900)] + pub intent_fulfillment_time: Option, + + /// The frm routing algorithm to be used for routing payments to desired FRM's + #[schema(value_type = Option,example = json!({"type": "single", "data": "signifyd"}))] + pub frm_routing_algorithm: Option, + + /// The routing algorithm to be used for routing payouts to desired connectors + #[cfg(feature = "payouts")] + #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] + #[serde( + default, + deserialize_with = "payout_routing_algorithm::deserialize_option" + )] + pub payout_routing_algorithm: Option, +} diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs new file mode 100644 index 0000000000..74c75a2bf5 --- /dev/null +++ b/crates/diesel_models/src/business_profile.rs @@ -0,0 +1,72 @@ +use common_utils::pii; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; + +use crate::schema::business_profile; + +#[derive( + Clone, + Debug, + serde::Deserialize, + serde::Serialize, + Identifiable, + Queryable, + router_derive::DebugAsDisplay, +)] +#[diesel(table_name = business_profile, primary_key(profile_id))] +pub struct BusinessProfile { + pub profile_id: String, + pub merchant_id: String, + pub profile_name: String, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, + pub return_url: Option, + pub enable_payment_response_hash: bool, + pub payment_response_hash_key: Option, + pub redirect_to_merchant_with_http_post: bool, + pub webhook_details: Option, + pub metadata: Option, + pub routing_algorithm: Option, + pub intent_fulfillment_time: Option, + pub frm_routing_algorithm: Option, + pub payout_routing_algorithm: Option, + pub is_recon_enabled: bool, +} + +#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] +#[diesel(table_name = business_profile, primary_key(profile_id))] +pub struct BusinessProfileNew { + pub profile_id: String, + pub merchant_id: String, + pub profile_name: String, + pub created_at: time::PrimitiveDateTime, + pub modified_at: time::PrimitiveDateTime, + pub return_url: Option, + pub enable_payment_response_hash: bool, + pub payment_response_hash_key: Option, + pub redirect_to_merchant_with_http_post: bool, + pub webhook_details: Option, + pub metadata: Option, + pub routing_algorithm: Option, + pub intent_fulfillment_time: Option, + pub frm_routing_algorithm: Option, + pub payout_routing_algorithm: Option, + pub is_recon_enabled: bool, +} + +#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] +#[diesel(table_name = business_profile)] +pub struct BusinessProfileUpdateInternal { + pub profile_name: Option, + pub modified_at: Option, + pub return_url: Option, + pub enable_payment_response_hash: Option, + pub payment_response_hash_key: Option, + pub redirect_to_merchant_with_http_post: Option, + pub webhook_details: Option, + pub metadata: Option, + pub routing_algorithm: Option, + pub intent_fulfillment_time: Option, + pub frm_routing_algorithm: Option, + pub payout_routing_algorithm: Option, + pub is_recon_enabled: Option, +} diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index 0d1e517046..ca979d30a2 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index ce24054080..bd280864cc 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -1,5 +1,6 @@ pub mod address; pub mod api_keys; +pub mod business_profile; mod capture; pub mod cards_info; pub mod configs; diff --git a/crates/diesel_models/src/query/business_profile.rs b/crates/diesel_models/src/query/business_profile.rs new file mode 100644 index 0000000000..0c9737ef61 --- /dev/null +++ b/crates/diesel_models/src/query/business_profile.rs @@ -0,0 +1,82 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods, Table}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + business_profile::{BusinessProfile, BusinessProfileNew, BusinessProfileUpdateInternal}, + errors, + schema::business_profile::dsl, + PgPooledConn, StorageResult, +}; + +impl BusinessProfileNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl BusinessProfile { + #[instrument(skip(conn))] + pub async fn update_by_profile_id( + self, + conn: &PgPooledConn, + business_profile: BusinessProfileUpdateInternal, + ) -> StorageResult { + match generics::generic_update_by_id::<::Table, _, _, _>( + conn, + self.profile_id.clone(), + business_profile, + ) + .await + { + Err(error) => match error.current_context() { + errors::DatabaseError::NoFieldsToUpdate => Ok(self), + _ => Err(error), + }, + result => result, + } + } + + #[instrument(skip(conn))] + pub async fn find_by_profile_id(conn: &PgPooledConn, profile_id: &str) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::profile_id.eq(profile_id.to_owned()), + ) + .await + } + + pub async fn list_business_profile_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &str, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + _, + >( + conn, + dsl::merchant_id.eq(merchant_id.to_string()), + None, + None, + None, + ) + .await + } + + pub async fn delete_by_profile_id_merchant_id( + conn: &PgPooledConn, + profile_id: &str, + merchant_id: &str, + ) -> StorageResult { + generics::generic_delete::<::Table, _>( + conn, + dsl::profile_id + .eq(profile_id.to_owned()) + .and(dsl::merchant_id.eq(merchant_id.to_string())), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index c6f9dfcc54..7acbcf0232 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -53,6 +53,34 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + business_profile (profile_id) { + #[max_length = 64] + profile_id -> Varchar, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + profile_name -> Varchar, + created_at -> Timestamp, + modified_at -> Timestamp, + return_url -> Nullable, + enable_payment_response_hash -> Bool, + #[max_length = 255] + payment_response_hash_key -> Nullable, + redirect_to_merchant_with_http_post -> Bool, + webhook_details -> Nullable, + metadata -> Nullable, + routing_algorithm -> Nullable, + intent_fulfillment_time -> Nullable, + frm_routing_algorithm -> Nullable, + payout_routing_algorithm -> Nullable, + is_recon_enabled -> Bool, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -756,6 +784,7 @@ diesel::table! { diesel::allow_tables_to_appear_in_same_query!( address, api_keys, + business_profile, captures, cards_info, configs, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index fae1e033cc..e22122138e 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -535,6 +535,10 @@ impl From for StripeErrorCode { object: "dispute".to_owned(), id: dispute_id, }, + errors::ApiErrorResponse::BusinessProfileNotFound { id } => Self::ResourceMissing { + object: "business_profile".to_owned(), + id, + }, errors::ApiErrorResponse::DisputeStatusValidationFailed { reason } => { Self::InternalServerError } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index b8a51dc51a..843c500615 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -25,6 +25,7 @@ use crate::{ types::{self as domain_types, AsyncLift}, }, storage, + transformers::ForeignTryFrom, }, utils::{self, OptionExt}, }; @@ -830,6 +831,218 @@ pub fn get_frm_config_as_secret( } } +pub async fn create_business_profile( + db: &dyn StorageInterface, + request: api::BusinessProfileCreate, + merchant_id: &str, +) -> RouterResponse { + let key_store = db + .get_merchant_key_store_by_merchant_id(merchant_id, &db.get_master_key().to_vec().into()) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + // Get the merchant account, if few fields are not passed, then they will be inherited from + // merchant account + let merchant_account = db + .find_merchant_account_by_merchant_id(merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + // Generate a unique profile id + let profile_id = common_utils::generate_id_with_default_len("pro"); + + let payment_response_hash_key = request + .payment_response_hash_key + .or(merchant_account.payment_response_hash_key) + .unwrap_or(generate_cryptographically_secure_random_string(64)); + + let webhook_details = request + .webhook_details + .as_ref() + .map(|webhook_details| { + utils::Encode::::encode_to_value(webhook_details).change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "webhook details", + }, + ) + }) + .transpose()?; + + if let Some(ref routing_algorithm) = request.routing_algorithm { + let _: api::RoutingAlgorithm = routing_algorithm + .clone() + .parse_value("RoutingAlgorithm") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "routing_algorithm", + }) + .attach_printable("Invalid routing algorithm given")?; + } + + let business_profile_new = storage::business_profile::BusinessProfileNew { + profile_id, + merchant_id: merchant_id.to_string(), + profile_name: request.profile_name.unwrap_or("default".to_string()), + created_at: date_time::now(), + modified_at: date_time::now(), + return_url: request + .return_url + .map(|return_url| return_url.to_string()) + .or(merchant_account.return_url), + enable_payment_response_hash: request + .enable_payment_response_hash + .unwrap_or(merchant_account.enable_payment_response_hash), + payment_response_hash_key: Some(payment_response_hash_key), + redirect_to_merchant_with_http_post: request + .redirect_to_merchant_with_http_post + .unwrap_or(merchant_account.redirect_to_merchant_with_http_post), + webhook_details: webhook_details.or(merchant_account.webhook_details), + metadata: request.metadata, + routing_algorithm: request + .routing_algorithm + .or(merchant_account.routing_algorithm), + intent_fulfillment_time: request + .intent_fulfillment_time + .map(i64::from) + .or(merchant_account.intent_fulfillment_time), + frm_routing_algorithm: request + .frm_routing_algorithm + .or(merchant_account.frm_routing_algorithm), + payout_routing_algorithm: request + .payout_routing_algorithm + .or(merchant_account.payout_routing_algorithm), + is_recon_enabled: merchant_account.is_recon_enabled, + }; + + let business_profile = db + .insert_business_profile(business_profile_new) + .await + .to_duplicate_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to insert Business profile because of duplication error")?; + Ok(service_api::ApplicationResponse::Json( + api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile) + .change_context(errors::ApiErrorResponse::InternalServerError)?, + )) +} + +pub async fn list_business_profile( + db: &dyn StorageInterface, + merchant_id: String, +) -> RouterResponse> { + let business_profiles = db + .list_business_profile_by_merchant_id(&merchant_id) + .await + .to_not_found_response(errors::ApiErrorResponse::InternalServerError)? + .into_iter() + .map(|business_profile| { + api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile) + }) + .collect::, _>>() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to parse business profile details")?; + + Ok(service_api::ApplicationResponse::Json(business_profiles)) +} + +pub async fn retrieve_business_profile( + db: &dyn StorageInterface, + profile_id: String, +) -> RouterResponse { + let business_profile = db + .find_business_profile_by_profile_id(&profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id, + })?; + + Ok(service_api::ApplicationResponse::Json( + api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile) + .change_context(errors::ApiErrorResponse::InternalServerError)?, + )) +} + +pub async fn delete_business_profile( + db: &dyn StorageInterface, + profile_id: String, + merchant_id: &str, +) -> RouterResponse { + let delete_result = db + .delete_business_profile_by_profile_id_merchant_id(&profile_id, merchant_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id, + })?; + + Ok(service_api::ApplicationResponse::Json(delete_result)) +} + +pub async fn update_business_profile( + db: &dyn StorageInterface, + profile_id: &str, + merchant_id: &str, + request: api::BusinessProfileUpdate, +) -> RouterResponse { + let business_profile = db + .find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_owned(), + })?; + + if business_profile.merchant_id != merchant_id { + Err(errors::ApiErrorResponse::AccessForbidden)? + } + + let webhook_details = request + .webhook_details + .as_ref() + .map(|webhook_details| { + utils::Encode::::encode_to_value(webhook_details).change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "webhook details", + }, + ) + }) + .transpose()?; + + if let Some(ref routing_algorithm) = request.routing_algorithm { + let _: api::RoutingAlgorithm = routing_algorithm + .clone() + .parse_value("RoutingAlgorithm") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "routing_algorithm", + }) + .attach_printable("Invalid routing algorithm given")?; + } + + let business_profile_update = storage::business_profile::BusinessProfileUpdateInternal { + profile_name: request.profile_name, + modified_at: Some(date_time::now()), + return_url: request.return_url.map(|return_url| return_url.to_string()), + enable_payment_response_hash: request.enable_payment_response_hash, + payment_response_hash_key: request.payment_response_hash_key, + redirect_to_merchant_with_http_post: request.redirect_to_merchant_with_http_post, + webhook_details, + metadata: request.metadata, + routing_algorithm: request.routing_algorithm, + intent_fulfillment_time: request.intent_fulfillment_time.map(i64::from), + frm_routing_algorithm: request.frm_routing_algorithm, + payout_routing_algorithm: request.payout_routing_algorithm, + is_recon_enabled: None, + }; + + let updated_business_profile = db + .update_business_profile_by_profile_id(business_profile, business_profile_update) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_owned(), + })?; + + Ok(service_api::ApplicationResponse::Json( + api_models::admin::BusinessProfileResponse::foreign_try_from(updated_business_profile) + .change_context(errors::ApiErrorResponse::InternalServerError)?, + )) +} + pub(crate) fn validate_auth_type( connector_name: api_models::enums::Connector, val: &types::ConnectorAuthType, diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index e659d5b215..e0da78cf0d 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -153,6 +153,8 @@ pub enum ApiErrorResponse { MerchantAccountNotFound, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Merchant connector account with id '{id}' does not exist in our records")] MerchantConnectorAccountNotFound { id: String }, + #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Business profile with the given id '{id}' does not exist in our records")] + BusinessProfileNotFound { id: String }, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Resource ID does not exist in our records")] ResourceIdNotFound, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_02", message = "Mandate does not exist in our records")] diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 2d6774a3d0..d91879ac47 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -207,6 +207,9 @@ impl ErrorSwitch for ApiErrorRespon } Self::DisputeNotFound { .. } => { AER::NotFound(ApiError::new("HE", 2, "Dispute does not exist in our records", None)) + }, + Self::BusinessProfileNotFound { id } => { + AER::NotFound(ApiError::new("HE", 2, format!("Business profile with the given id {id} does not exist"), None)) } Self::FileNotFound => { AER::NotFound(ApiError::new("HE", 2, "File does not exist in our records", None)) diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 1f034cb25c..69b62f84ab 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 business_profile; pub mod cache; pub mod capture; pub mod cards_info; @@ -75,6 +76,7 @@ pub trait StorageInterface: + merchant_key_store::MerchantKeyStoreInterface + MasterKeyInterface + RedisConnInterface + + business_profile::BusinessProfileInterface + 'static { } diff --git a/crates/router/src/db/business_profile.rs b/crates/router/src/db/business_profile.rs new file mode 100644 index 0000000000..72b1d16edc --- /dev/null +++ b/crates/router/src/db/business_profile.rs @@ -0,0 +1,151 @@ +use error_stack::IntoReport; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::MockDb, + types::storage::{self, business_profile}, +}; + +#[async_trait::async_trait] +pub trait BusinessProfileInterface { + async fn insert_business_profile( + &self, + business_profile: business_profile::BusinessProfileNew, + ) -> CustomResult; + + async fn find_business_profile_by_profile_id( + &self, + profile_id: &str, + ) -> CustomResult; + + async fn update_business_profile_by_profile_id( + &self, + current_state: business_profile::BusinessProfile, + business_profile_update: business_profile::BusinessProfileUpdateInternal, + ) -> CustomResult; + + async fn delete_business_profile_by_profile_id_merchant_id( + &self, + profile_id: &str, + merchant_id: &str, + ) -> CustomResult; + + async fn list_business_profile_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl BusinessProfileInterface for Store { + async fn insert_business_profile( + &self, + business_profile: business_profile::BusinessProfileNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + business_profile + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_business_profile_by_profile_id( + &self, + profile_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::business_profile::BusinessProfile::find_by_profile_id(&conn, profile_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn update_business_profile_by_profile_id( + &self, + current_state: business_profile::BusinessProfile, + business_profile_update: business_profile::BusinessProfileUpdateInternal, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::business_profile::BusinessProfile::update_by_profile_id( + current_state, + &conn, + business_profile_update, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_business_profile_by_profile_id_merchant_id( + &self, + profile_id: &str, + merchant_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::business_profile::BusinessProfile::delete_by_profile_id_merchant_id( + &conn, + profile_id, + merchant_id, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_business_profile_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::business_profile::BusinessProfile::list_business_profile_by_merchant_id( + &conn, + merchant_id, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BusinessProfileInterface for MockDb { + async fn insert_business_profile( + &self, + _business_profile: business_profile::BusinessProfileNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_business_profile_by_profile_id( + &self, + _profile_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn update_business_profile_by_profile_id( + &self, + _current_state: business_profile::BusinessProfile, + _business_profile_update: business_profile::BusinessProfileUpdateInternal, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_business_profile_by_profile_id_merchant_id( + &self, + _profile_id: &str, + _merchant_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn list_business_profile_by_merchant_id( + &self, + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 2c8c0a4f9f..1dcef5f7c9 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -106,13 +106,19 @@ pub fn mk_app( #[cfg(any(feature = "olap", feature = "oltp"))] { + #[cfg(feature = "olap")] + { + // This is a more specific route as compared to `MerchantConnectorAccount` + // so it is registered before `MerchantConnectorAccount`. + server_app = server_app.service(routes::BusinessProfile::server(state.clone())) + } server_app = server_app .service(routes::Payments::server(state.clone())) .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) .service(routes::Refunds::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) - .service(routes::Mandates::server(state.clone())); + .service(routes::Mandates::server(state.clone())) } #[cfg(feature = "oltp")] @@ -129,7 +135,7 @@ pub fn mk_app( .service(routes::MerchantAccount::server(state.clone())) .service(routes::ApiKeys::server(state.clone())) .service(routes::Files::server(state.clone())) - .service(routes::Disputes::server(state.clone())); + .service(routes::Disputes::server(state.clone())) } #[cfg(feature = "payouts")] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index aa9da13aa5..9c21377d8f 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -25,9 +25,9 @@ pub use self::app::DummyConnector; #[cfg(feature = "payouts")] pub use self::app::Payouts; pub use self::app::{ - ApiKeys, AppState, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, Files, Health, - Mandates, MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, Refunds, - Webhooks, + ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, + Files, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentMethods, Payments, + Refunds, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index dd8ca8d323..225b9bad82 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -386,6 +386,109 @@ pub async fn merchant_account_toggle_kv( .await } +#[instrument(skip_all, fields(flow = ?Flow::BusinessProfileCreate))] +pub async fn business_profile_create( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> HttpResponse { + let flow = Flow::BusinessProfileCreate; + let payload = json_payload.into_inner(); + let merchant_id = path.into_inner(); + + api::server_wrap( + flow, + state.get_ref(), + &req, + payload, + |state, _, req| create_business_profile(&*state.store, req, &merchant_id), + &auth::AdminApiAuth, + ) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::BusinessProfileRetrieve))] +pub async fn business_profile_retrieve( + state: web::Data, + req: HttpRequest, + path: web::Path<(String, String)>, +) -> HttpResponse { + let flow = Flow::BusinessProfileRetrieve; + let (_, profile_id) = path.into_inner(); + + api::server_wrap( + flow, + state.get_ref(), + &req, + profile_id, + |state, _, profile_id| retrieve_business_profile(&*state.store, profile_id), + &auth::AdminApiAuth, + ) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::BusinessProfileUpdate))] +pub async fn business_profile_update( + state: web::Data, + req: HttpRequest, + path: web::Path<(String, String)>, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::BusinessProfileUpdate; + let (merchant_id, profile_id) = path.into_inner(); + + api::server_wrap( + flow, + state.get_ref(), + &req, + json_payload.into_inner(), + |state, _, req| update_business_profile(&*state.store, &profile_id, &merchant_id, req), + &auth::AdminApiAuth, + ) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::BusinessProfileDelete))] +pub async fn business_profile_delete( + state: web::Data, + req: HttpRequest, + path: web::Path<(String, String)>, +) -> HttpResponse { + let flow = Flow::BusinessProfileDelete; + let (merchant_id, profile_id) = path.into_inner(); + + api::server_wrap( + flow, + state.get_ref(), + &req, + profile_id, + |state, _, profile_id| delete_business_profile(&*state.store, profile_id, &merchant_id), + &auth::AdminApiAuth, + ) + .await +} + +#[instrument(skip_all, fields(flow = ?Flow::BusinessProfileList))] +pub async fn business_profiles_list( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::BusinessProfileList; + let merchant_id = path.into_inner(); + + api::server_wrap( + flow, + state.get_ref(), + &req, + merchant_id, + |state, _, merchant_id| list_business_profile(&*state.store, merchant_id), + &auth::AdminApiAuth, + ) + .await +} + /// Merchant Account - KV Status /// /// Toggle KV mode for the Merchant Account diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index cce227dc46..763f159e88 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -528,3 +528,24 @@ impl Cache { .service(web::resource("/invalidate/{key}").route(web::post().to(invalidate))) } } + +pub struct BusinessProfile; + +#[cfg(feature = "olap")] +impl BusinessProfile { + pub fn server(state: AppState) -> Scope { + web::scope("/account/{account_id}/business_profile") + .app_data(web::Data::new(state)) + .service( + web::resource("") + .route(web::post().to(business_profile_create)) + .route(web::get().to(business_profiles_list)), + ) + .service( + web::resource("/{profile_id}") + .route(web::get().to(business_profile_retrieve)) + .route(web::post().to(business_profile_update)) + .route(web::delete().to(business_profile_delete)), + ) + } +} diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index c21e0790ff..eb809155af 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -1,5 +1,6 @@ pub use api_models::admin::{ - payout_routing_algorithm, MerchantAccountCreate, MerchantAccountDeleteResponse, + payout_routing_algorithm, BusinessProfileCreate, BusinessProfileResponse, + BusinessProfileUpdate, MerchantAccountCreate, MerchantAccountDeleteResponse, MerchantAccountResponse, MerchantAccountUpdate, MerchantConnectorCreate, MerchantConnectorDeleteResponse, MerchantConnectorDetails, MerchantConnectorDetailsWrap, MerchantConnectorId, MerchantConnectorResponse, MerchantDetails, MerchantId, @@ -7,8 +8,12 @@ pub use api_models::admin::{ RoutingAlgorithm, StraightThroughAlgorithm, ToggleKVRequest, ToggleKVResponse, WebhookDetails, }; use common_utils::ext_traits::ValueExt; +use masking::Secret; -use crate::{core::errors, types::domain}; +use crate::{ + core::errors, + types::{domain, storage, transformers::ForeignTryFrom}, +}; impl TryFrom for MerchantAccountResponse { type Error = error_stack::Report; @@ -41,3 +46,27 @@ impl TryFrom for MerchantAccountResponse { }) } } + +impl ForeignTryFrom for BusinessProfileResponse { + type Error = error_stack::Report; + + fn foreign_try_from( + item: storage::business_profile::BusinessProfile, + ) -> Result { + Ok(Self { + merchant_id: item.merchant_id, + profile_id: item.profile_id, + profile_name: item.profile_name, + return_url: item.return_url, + enable_payment_response_hash: item.enable_payment_response_hash, + payment_response_hash_key: item.payment_response_hash_key, + redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, + webhook_details: item.webhook_details.map(Secret::new), + metadata: item.metadata, + routing_algorithm: item.routing_algorithm, + intent_fulfillment_time: item.intent_fulfillment_time, + frm_routing_algorithm: item.frm_routing_algorithm, + payout_routing_algorithm: item.payout_routing_algorithm, + }) + } +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 46e3eac5fb..d9ecdaaa19 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 business_profile; pub mod capture; pub mod cards_info; pub mod configs; diff --git a/crates/router/src/types/storage/business_profile.rs b/crates/router/src/types/storage/business_profile.rs new file mode 100644 index 0000000000..2ab7597bcd --- /dev/null +++ b/crates/router/src/types/storage/business_profile.rs @@ -0,0 +1,3 @@ +pub use diesel_models::business_profile::{ + BusinessProfile, BusinessProfileNew, BusinessProfileUpdateInternal, +}; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 1ef07c135b..bfedf63dd0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -191,6 +191,16 @@ pub enum Flow { RetrieveDisputeEvidence, /// Invalidate cache flow CacheInvalidate, + /// Create a business profile + BusinessProfileCreate, + /// Update a business profile + BusinessProfileUpdate, + /// Retrieve a business profile + BusinessProfileRetrieve, + /// Delete a business profile + BusinessProfileDelete, + /// List all the business profiles for a merchant + BusinessProfileList, } /// diff --git a/migrations/2023-08-08-144148_add_business_profile_table/down.sql b/migrations/2023-08-08-144148_add_business_profile_table/down.sql new file mode 100644 index 0000000000..9d31be83c5 --- /dev/null +++ b/migrations/2023-08-08-144148_add_business_profile_table/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS business_profile; diff --git a/migrations/2023-08-08-144148_add_business_profile_table/up.sql b/migrations/2023-08-08-144148_add_business_profile_table/up.sql new file mode 100644 index 0000000000..b4edd8bd12 --- /dev/null +++ b/migrations/2023-08-08-144148_add_business_profile_table/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here +CREATE TABLE IF NOT EXISTS business_profile ( + profile_id VARCHAR(64) PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + profile_name VARCHAR(64) NOT NULL, + created_at TIMESTAMP NOT NULL, + modified_at TIMESTAMP NOT NULL, + return_url TEXT, + enable_payment_response_hash BOOLEAN NOT NULL DEFAULT TRUE, + payment_response_hash_key VARCHAR(255) DEFAULT NULL, + redirect_to_merchant_with_http_post BOOLEAN NOT NULL DEFAULT FALSE, + webhook_details JSON, + metadata JSON, + routing_algorithm JSON, + intent_fulfillment_time BIGINT, + frm_routing_algorithm JSONB, + payout_routing_algorithm JSONB, + is_recon_enabled BOOLEAN NOT NULL DEFAULT FALSE +); diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 209f21e6b4..85c6e8565e 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5885,7 +5885,7 @@ }, "payment_response_hash_key": { "type": "string", - "description": "Refers to the hash key used for payment response", + "description": "Refers to the hash key used for calculating the signature for webhooks and redirect response\nIf the value is not provided, a default value is used", "nullable": true }, "redirect_to_merchant_with_http_post": {