refactor(routing): Api v2 for routing create and activate endpoints (#5423)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Amisha Prabhat
2024-08-01 15:00:28 +05:30
committed by GitHub
parent 1d4c87a9e3
commit 6140cfe04e
11 changed files with 568 additions and 139 deletions

View File

@ -17,8 +17,9 @@ frm = []
olap = [] olap = []
openapi = ["common_enums/openapi", "olap", "recon", "dummy_connector", "olap"] openapi = ["common_enums/openapi", "olap", "recon", "dummy_connector", "olap"]
recon = [] recon = []
v1 =[]
v2 = [] v2 = []
v1 = [] routing_v2 = []
merchant_connector_account_v2 = [] merchant_connector_account_v2 = []
customer_v2 = [] customer_v2 = []
merchant_account_v2 = [] merchant_account_v2 = []

View File

@ -3,7 +3,7 @@ use common_utils::events::{ApiEventMetric, ApiEventsType};
use crate::routing::{ use crate::routing::{
LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, ProfileDefaultRoutingConfig, LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, ProfileDefaultRoutingConfig,
RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind,
RoutingPayloadWrapper, RoutingRetrieveLinkQuery, RoutingRetrieveQuery, RoutingLinkWrapper, RoutingPayloadWrapper, RoutingRetrieveLinkQuery, RoutingRetrieveQuery,
}; };
impl ApiEventMetric for RoutingKind { impl ApiEventMetric for RoutingKind {
@ -64,3 +64,9 @@ impl ApiEventMetric for RoutingRetrieveLinkQuery {
Some(ApiEventsType::Routing) Some(ApiEventsType::Routing)
} }
} }
impl ApiEventMetric for RoutingLinkWrapper {
fn get_api_event_type(&self) -> Option<ApiEventsType> {
Some(ApiEventsType::Routing)
}
}

View File

@ -1,6 +1,6 @@
use std::fmt::Debug; use std::fmt::Debug;
use common_utils::errors::ParsingError; use common_utils::{errors::ParsingError, ext_traits::ValueExt, pii};
pub use euclid::{ pub use euclid::{
dssa::types::EuclidAnalysable, dssa::types::EuclidAnalysable,
frontend::{ frontend::{
@ -427,6 +427,14 @@ impl RoutingAlgorithmRef {
self.surcharge_config_algo_id = Some(ids); self.surcharge_config_algo_id = Some(ids);
self.timestamp = common_utils::date_time::now_unix_timestamp(); self.timestamp = common_utils::date_time::now_unix_timestamp();
} }
pub fn parse_routing_algorithm(
value: Option<pii::SecretSerdeValue>,
) -> Result<Option<Self>, error_stack::Report<ParsingError>> {
value
.map(|val| val.parse_value::<Self>("RoutingAlgorithmRef"))
.transpose()
}
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
@ -459,6 +467,12 @@ pub enum RoutingKind {
} }
#[repr(transparent)] #[repr(transparent)]
#[derive(serde::Serialize, serde::Deserialize, Debug)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
#[serde(transparent)] #[serde(transparent)]
pub struct RoutingAlgorithmId(pub String); pub struct RoutingAlgorithmId(pub String);
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RoutingLinkWrapper {
pub profile_id: String,
pub algorithm_id: RoutingAlgorithmId,
}

View File

@ -10,7 +10,7 @@ license.workspace = true
[features] [features]
default = ["kv_store", "v1"] default = ["kv_store", "v1"]
kv_store = [] kv_store = []
v1 = [] v1 =[]
v2 = [] v2 = []
customer_v2 = [] customer_v2 = []
merchant_account_v2 = [] merchant_account_v2 = []
@ -27,6 +27,7 @@ strum = { version = "0.26.2", features = ["derive"] }
thiserror = "1.0.58" thiserror = "1.0.58"
time = { version = "0.3.35", features = ["serde", "serde-well-known", "std"] } time = { version = "0.3.35", features = ["serde", "serde-well-known", "std"] }
# First party crates # First party crates
common_enums = { version = "0.1.0", path = "../common_enums" } common_enums = { version = "0.1.0", path = "../common_enums" }
common_utils = { version = "0.1.0", path = "../common_utils" } common_utils = { version = "0.1.0", path = "../common_utils" }

View File

@ -1,8 +1,10 @@
pub mod api; pub mod api;
pub mod behaviour;
pub mod customer; pub mod customer;
pub mod errors; pub mod errors;
pub mod mandates; pub mod mandates;
pub mod merchant_account; pub mod merchant_account;
pub mod merchant_key_store;
pub mod payment_address; pub mod payment_address;
pub mod payment_method_data; pub mod payment_method_data;
pub mod payments; pub mod payments;
@ -13,9 +15,6 @@ pub mod router_data_v2;
pub mod router_flow_types; pub mod router_flow_types;
pub mod router_request_types; pub mod router_request_types;
pub mod router_response_types; pub mod router_response_types;
pub mod behaviour;
pub mod merchant_key_store;
pub mod type_encryption; pub mod type_encryption;
pub mod types; pub mod types;

View File

@ -36,6 +36,7 @@ v1 = ["api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "stor
customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2"] customer_v2 = ["api_models/customer_v2", "diesel_models/customer_v2", "hyperswitch_domain_models/customer_v2"]
merchant_account_v2 = ["api_models/merchant_account_v2", "diesel_models/merchant_account_v2", "hyperswitch_domain_models/merchant_account_v2"] merchant_account_v2 = ["api_models/merchant_account_v2", "diesel_models/merchant_account_v2", "hyperswitch_domain_models/merchant_account_v2"]
payment_v2 = ["api_models/payment_v2", "diesel_models/payment_v2", "hyperswitch_domain_models/payment_v2"] payment_v2 = ["api_models/payment_v2", "diesel_models/payment_v2", "hyperswitch_domain_models/payment_v2"]
routing_v2 = ["api_models/routing_v2"]
merchant_connector_account_v2 = ["api_models/merchant_connector_account_v2", "kgraph_utils/merchant_connector_account_v2"] merchant_connector_account_v2 = ["api_models/merchant_connector_account_v2", "kgraph_utils/merchant_connector_account_v2"]
# Partial Auth # Partial Auth

View File

@ -7,15 +7,23 @@ use api_models::{
self as routing_types, RoutingAlgorithmId, RoutingRetrieveLinkQuery, RoutingRetrieveQuery, self as routing_types, RoutingAlgorithmId, RoutingRetrieveLinkQuery, RoutingRetrieveQuery,
}, },
}; };
#[cfg(all(feature = "v2", feature = "routing_v2"))]
use diesel_models::routing_algorithm::RoutingAlgorithm;
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "routing_v2")))]
use diesel_models::routing_algorithm::RoutingAlgorithm; use diesel_models::routing_algorithm::RoutingAlgorithm;
use error_stack::ResultExt; use error_stack::ResultExt;
#[cfg(all(feature = "v2", feature = "routing_v2"))]
use masking::Secret;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use super::payments; use super::payments;
#[cfg(feature = "payouts")] #[cfg(feature = "payouts")]
use super::payouts; use super::payouts;
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "routing_v2")))]
use crate::consts;
#[cfg(all(feature = "v2", feature = "routing_v2"))]
use crate::{consts, core::errors::RouterResult, db::StorageInterface};
use crate::{ use crate::{
consts,
core::{ core::{
errors::{self, RouterResponse, StorageErrorExt}, errors::{self, RouterResponse, StorageErrorExt},
metrics, utils as core_utils, metrics, utils as core_utils,
@ -28,7 +36,6 @@ use crate::{
}, },
utils::{self, OptionExt, ValueExt}, utils::{self, OptionExt, ValueExt},
}; };
pub enum TransactionData<'a, F> pub enum TransactionData<'a, F>
where where
F: Clone, F: Clone,
@ -38,6 +45,77 @@ where
Payout(&'a payouts::PayoutData), Payout(&'a payouts::PayoutData),
} }
#[cfg(all(feature = "v2", feature = "routing_v2"))]
struct RoutingAlgorithmUpdate(RoutingAlgorithm);
#[cfg(all(feature = "v2", feature = "routing_v2"))]
impl RoutingAlgorithmUpdate {
pub fn create_new_routing_algorithm(
algorithm: routing_types::RoutingAlgorithm,
merchant_id: &common_utils::id_type::MerchantId,
name: String,
description: String,
profile_id: String,
transaction_type: &enums::TransactionType,
) -> Self {
let algorithm_id = common_utils::generate_id(
consts::ROUTING_CONFIG_ID_LENGTH,
&format!("routing_{}", merchant_id.get_string_repr()),
);
let timestamp = common_utils::date_time::now();
let algo = RoutingAlgorithm {
algorithm_id,
profile_id,
merchant_id: merchant_id.clone(),
name,
description: Some(description),
kind: algorithm.get_kind().foreign_into(),
algorithm_data: serde_json::json!(algorithm),
created_at: timestamp,
modified_at: timestamp,
algorithm_for: transaction_type.to_owned(),
};
Self(algo)
}
pub async fn fetch_routing_algo(
merchant_id: &common_utils::id_type::MerchantId,
algorithm_id: &str,
db: &dyn StorageInterface,
) -> RouterResult<Self> {
let routing_algo = db
.find_routing_algorithm_by_algorithm_id_merchant_id(algorithm_id, merchant_id)
.await
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)?;
Ok(Self(routing_algo))
}
pub fn update_routing_ref_with_algorithm_id(
&self,
transaction_type: &enums::TransactionType,
routing_ref: &mut routing_types::RoutingAlgorithmRef,
) -> RouterResult<()> {
utils::when(self.0.algorithm_for != *transaction_type, || {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: format!(
"Cannot use {}'s routing algorithm for {} operation",
self.0.algorithm_for, transaction_type
),
})
})?;
utils::when(
routing_ref.algorithm_id == Some(self.0.algorithm_id.clone()),
|| {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "Algorithm is already active".to_string(),
})
},
)?;
routing_ref.update_algorithm_id(self.0.algorithm_id.clone());
Ok(())
}
}
pub async fn retrieve_merchant_routing_dictionary( pub async fn retrieve_merchant_routing_dictionary(
state: SessionState, state: SessionState,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
@ -67,6 +145,94 @@ pub async fn retrieve_merchant_routing_dictionary(
)) ))
} }
#[cfg(all(feature = "v2", feature = "routing_v2"))]
pub async fn create_routing_config(
state: SessionState,
merchant_account: domain::MerchantAccount,
key_store: domain::MerchantKeyStore,
request: routing_types::RoutingConfigRequest,
transaction_type: &enums::TransactionType,
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(&metrics::CONTEXT, 1, &[]);
let db = &*state.store;
let name = request
.name
.get_required_value("name")
.change_context(errors::ApiErrorResponse::MissingRequiredField { field_name: "name" })
.attach_printable("Name of config not given")?;
let description = request
.description
.get_required_value("description")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "description",
})
.attach_printable("Description of config not given")?;
let algorithm = request
.algorithm
.get_required_value("algorithm")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "algorithm",
})
.attach_printable("Algorithm of config not given")?;
let business_profile = core_utils::validate_and_get_business_profile(
db,
request.profile_id.as_ref(),
merchant_account.get_id(),
)
.await?
.get_required_value("BusinessProfile")?;
let all_mcas = helpers::MerchantConnectorAccounts::get_all_mcas(
merchant_account.get_id(),
&key_store,
&state,
)
.await?;
let name_mca_id_set = helpers::ConnectNameAndMCAIdForProfile(
all_mcas.filter_by_profile(&business_profile.profile_id, |mca| {
(&mca.connector_name, &mca.merchant_connector_id)
}),
);
let name_set = helpers::ConnectNameForProfile(
all_mcas.filter_by_profile(&business_profile.profile_id, |mca| &mca.connector_name),
);
let algorithm_helper = helpers::RoutingAlgorithmHelpers {
name_mca_id_set,
name_set,
routing_algorithm: &algorithm,
};
algorithm_helper.validate_connectors_in_routing_config()?;
let algo = RoutingAlgorithmUpdate::create_new_routing_algorithm(
algorithm,
merchant_account.get_id(),
name,
description,
business_profile.profile_id,
transaction_type,
);
let record = state
.store
.as_ref()
.insert_routing_algorithm(algo.0)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
let new_record = record.foreign_into();
metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(new_record))
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "routing_v2")))]
pub async fn create_routing_config( pub async fn create_routing_config(
state: SessionState, state: SessionState,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
@ -76,7 +242,6 @@ pub async fn create_routing_config(
) -> RouterResponse<routing_types::RoutingDictionaryRecord> { ) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(&metrics::CONTEXT, 1, &[]); metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref(); let db = state.store.as_ref();
let name = request let name = request
.name .name
.get_required_value("name") .get_required_value("name")
@ -148,6 +313,56 @@ pub async fn create_routing_config(
Ok(service_api::ApplicationResponse::Json(new_record)) Ok(service_api::ApplicationResponse::Json(new_record))
} }
#[cfg(all(feature = "v2", feature = "routing_v2"))]
pub async fn link_routing_config(
state: SessionState,
merchant_account: domain::MerchantAccount,
profile_id: String,
algorithm_id: String,
transaction_type: &enums::TransactionType,
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
metrics::ROUTING_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
let routing_algorithm =
RoutingAlgorithmUpdate::fetch_routing_algo(merchant_account.get_id(), &algorithm_id, db)
.await?;
utils::when(routing_algorithm.0.profile_id != profile_id, || {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "Profile Id is invalid for the routing config".to_string(),
})
})?;
let business_profile = core_utils::validate_and_get_business_profile(
db,
Some(&profile_id),
merchant_account.get_id(),
)
.await?
.get_required_value("BusinessProfile")?;
let mut routing_ref = routing_types::RoutingAlgorithmRef::parse_routing_algorithm(
business_profile.routing_algorithm.clone().map(Secret::new),
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize routing algorithm ref from merchant account")?
.unwrap_or_default();
routing_algorithm.update_routing_ref_with_algorithm_id(transaction_type, &mut routing_ref)?;
// TODO move to business profile
helpers::update_business_profile_active_algorithm_ref(
db,
business_profile,
routing_ref,
transaction_type,
)
.await?;
metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(
routing_algorithm.0.foreign_into(),
))
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "routing_v2")))]
pub async fn link_routing_config( pub async fn link_routing_config(
state: SessionState, state: SessionState,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
@ -249,6 +464,7 @@ pub async fn retrieve_routing_config(
metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(response)) Ok(service_api::ApplicationResponse::Json(response))
} }
pub async fn unlink_routing_config( pub async fn unlink_routing_config(
state: SessionState, state: SessionState,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
@ -326,6 +542,7 @@ pub async fn unlink_routing_config(
} }
} }
//feature update
pub async fn update_default_routing_config( pub async fn update_default_routing_config(
state: SessionState, state: SessionState,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,

View File

@ -12,6 +12,8 @@ use error_stack::ResultExt;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use storage_impl::redis::cache; use storage_impl::redis::cache;
#[cfg(all(feature = "v2", feature = "routing_v2"))]
use crate::types::domain::MerchantConnectorAccount;
use crate::{ use crate::{
core::errors::{self, RouterResult}, core::errors::{self, RouterResult},
db::StorageInterface, db::StorageInterface,
@ -20,54 +22,6 @@ use crate::{
utils::StringExt, utils::StringExt,
}; };
/// provides the complete merchant routing dictionary that is basically a list of all the routing
/// configs a merchant configured with an active_id field that specifies the current active routing
/// config
// pub async fn get_merchant_routing_dictionary(
// db: &dyn StorageInterface,
// merchant_id: &str,
// ) -> RouterResult<routing_types::RoutingDictionary> {
// let key = get_routing_dictionary_key(merchant_id);
// let maybe_dict = db.find_config_by_key(&key).await;
// match maybe_dict {
// Ok(config) => config
// .config
// .parse_struct("RoutingDictionary")
// .change_context(errors::ApiErrorResponse::InternalServerError)
// .attach_printable("Merchant routing dictionary has invalid structure"),
// Err(e) if e.current_context().is_db_not_found() => {
// let new_dictionary = routing_types::RoutingDictionary {
// merchant_id: merchant_id.to_owned(),
// active_id: None,
// records: Vec::new(),
// };
// let serialized = new_dictionary
// .encode_to_string_of_json()
// .change_context(errors::ApiErrorResponse::InternalServerError)
// .attach_printable("Error serializing newly created merchant dictionary")?;
// let new_config = configs::ConfigNew {
// key,
// config: serialized,
// };
// db.insert_config(new_config)
// .await
// .change_context(errors::ApiErrorResponse::InternalServerError)
// .attach_printable("Error inserting new routing dictionary for merchant")?;
// Ok(new_dictionary)
// }
// Err(e) => Err(e)
// .change_context(errors::ApiErrorResponse::InternalServerError)
// .attach_printable("Error fetching routing dictionary for merchant"),
// }
// }
/// Provides us with all the configured configs of the Merchant in the ascending time configured /// Provides us with all the configured configs of the Merchant in the ascending time configured
/// manner and chooses the first of them /// manner and chooses the first of them
pub async fn get_merchant_default_config( pub async fn get_merchant_default_config(
@ -163,28 +117,6 @@ pub async fn update_merchant_routing_dictionary(
Ok(()) Ok(())
} }
pub async fn update_routing_algorithm(
db: &dyn StorageInterface,
algorithm_id: String,
algorithm: routing_types::RoutingAlgorithm,
) -> RouterResult<()> {
let algorithm_str = algorithm
.encode_to_string_of_json()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to serialize routing algorithm to string")?;
let config_update = configs::ConfigUpdate::Update {
config: Some(algorithm_str),
};
db.update_config_by_key(&algorithm_id, config_update)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error updating the routing algorithm in DB")?;
Ok(())
}
/// This will help make one of all configured algorithms to be in active state for a particular /// This will help make one of all configured algorithms to be in active state for a particular
/// merchant /// merchant
pub async fn update_merchant_active_algorithm_ref( pub async fn update_merchant_active_algorithm_ref(
@ -238,7 +170,7 @@ pub async fn update_merchant_active_algorithm_ref(
Ok(()) Ok(())
} }
// TODO: Move it to business_profile
pub async fn update_business_profile_active_algorithm_ref( pub async fn update_business_profile_active_algorithm_ref(
db: &dyn StorageInterface, db: &dyn StorageInterface,
current_business_profile: BusinessProfile, current_business_profile: BusinessProfile,
@ -307,6 +239,157 @@ pub async fn update_business_profile_active_algorithm_ref(
Ok(()) Ok(())
} }
#[cfg(all(feature = "v2", feature = "routing_v2"))]
#[derive(Clone, Debug)]
pub struct RoutingAlgorithmHelpers<'h> {
pub name_mca_id_set: ConnectNameAndMCAIdForProfile<'h>,
pub name_set: ConnectNameForProfile<'h>,
pub routing_algorithm: &'h routing_types::RoutingAlgorithm,
}
#[derive(Clone, Debug)]
pub struct ConnectNameAndMCAIdForProfile<'a>(pub FxHashSet<(&'a String, &'a String)>);
#[derive(Clone, Debug)]
pub struct ConnectNameForProfile<'a>(pub FxHashSet<&'a String>);
#[cfg(all(feature = "v2", feature = "routing_v2"))]
#[derive(Clone, Debug)]
pub struct MerchantConnectorAccounts(pub Vec<MerchantConnectorAccount>);
#[cfg(all(feature = "v2", feature = "routing_v2"))]
impl MerchantConnectorAccounts {
pub async fn get_all_mcas(
merchant_id: &common_utils::id_type::MerchantId,
key_store: &domain::MerchantKeyStore,
state: &SessionState,
) -> RouterResult<Self> {
let db = &*state.store;
let key_manager_state = &state.into();
Ok(Self(
db.find_merchant_connector_account_by_merchant_id_and_disabled_list(
key_manager_state,
merchant_id,
true,
key_store,
)
.await
.change_context(
errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
id: merchant_id.get_string_repr().to_owned(),
},
)?,
))
}
fn filter_and_map<'a, T>(
&'a self,
filter: impl Fn(&'a MerchantConnectorAccount) -> bool,
func: impl Fn(&'a MerchantConnectorAccount) -> T,
) -> FxHashSet<T>
where
T: std::hash::Hash + Eq,
{
self.0
.iter()
.filter(|mca| filter(mca))
.map(func)
.collect::<FxHashSet<_>>()
}
pub fn filter_by_profile<'a, T>(
&'a self,
profile_id: &'a str,
func: impl Fn(&'a MerchantConnectorAccount) -> T,
) -> FxHashSet<T>
where
T: std::hash::Hash + Eq,
{
self.filter_and_map(|mca| mca.profile_id.as_deref() == Some(profile_id), func)
}
}
#[cfg(all(feature = "v2", feature = "routing_v2"))]
impl<'h> RoutingAlgorithmHelpers<'h> {
fn connector_choice(
&self,
choice: &routing_types::RoutableConnectorChoice,
) -> RouterResult<()> {
if let Some(ref mca_id) = choice.merchant_connector_id {
error_stack::ensure!(
self.name_mca_id_set.0.contains(&(&choice.connector.to_string(), mca_id)),
errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"connector with name '{}' and merchant connector account id '{}' not found for the given profile",
choice.connector,
mca_id,
)
}
);
} else {
error_stack::ensure!(
self.name_set.0.contains(&choice.connector.to_string()),
errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"connector with name '{}' not found for the given profile",
choice.connector,
)
}
);
};
Ok(())
}
pub fn validate_connectors_in_routing_config(&self) -> RouterResult<()> {
match self.routing_algorithm {
routing_types::RoutingAlgorithm::Single(choice) => {
self.connector_choice(choice)?;
}
routing_types::RoutingAlgorithm::Priority(list) => {
for choice in list {
self.connector_choice(choice)?;
}
}
routing_types::RoutingAlgorithm::VolumeSplit(splits) => {
for split in splits {
self.connector_choice(&split.connector)?;
}
}
routing_types::RoutingAlgorithm::Advanced(program) => {
let check_connector_selection =
|selection: &routing_types::ConnectorSelection| -> RouterResult<()> {
match selection {
routing_types::ConnectorSelection::VolumeSplit(splits) => {
for split in splits {
self.connector_choice(&split.connector)?;
}
}
routing_types::ConnectorSelection::Priority(list) => {
for choice in list {
self.connector_choice(choice)?;
}
}
}
Ok(())
};
check_connector_selection(&program.default_selection)?;
for rule in &program.rules {
check_connector_selection(&rule.connector_selection)?;
}
}
}
Ok(())
}
}
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "routing_v2")))]
pub async fn validate_connectors_in_routing_config( pub async fn validate_connectors_in_routing_config(
state: &SessionState, state: &SessionState,
key_store: &domain::MerchantKeyStore, key_store: &domain::MerchantKeyStore,
@ -326,7 +409,6 @@ pub async fn validate_connectors_in_routing_config(
.change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound {
id: merchant_id.get_string_repr().to_owned(), id: merchant_id.get_string_repr().to_owned(),
})?; })?;
let name_mca_id_set = all_mcas let name_mca_id_set = all_mcas
.iter() .iter()
.filter(|mca| mca.profile_id.as_deref() == Some(profile_id)) .filter(|mca| mca.profile_id.as_deref() == Some(profile_id))
@ -339,7 +421,7 @@ pub async fn validate_connectors_in_routing_config(
.map(|mca| &mca.connector_name) .map(|mca| &mca.connector_name)
.collect::<FxHashSet<_>>(); .collect::<FxHashSet<_>>();
let check_connector_choice = |choice: &routing_types::RoutableConnectorChoice| { let connector_choice = |choice: &routing_types::RoutableConnectorChoice| {
if let Some(ref mca_id) = choice.merchant_connector_id { if let Some(ref mca_id) = choice.merchant_connector_id {
error_stack::ensure!( error_stack::ensure!(
name_mca_id_set.contains(&(&choice.connector.to_string(), mca_id)), name_mca_id_set.contains(&(&choice.connector.to_string(), mca_id)),
@ -368,18 +450,18 @@ pub async fn validate_connectors_in_routing_config(
match routing_algorithm { match routing_algorithm {
routing_types::RoutingAlgorithm::Single(choice) => { routing_types::RoutingAlgorithm::Single(choice) => {
check_connector_choice(choice)?; connector_choice(choice)?;
} }
routing_types::RoutingAlgorithm::Priority(list) => { routing_types::RoutingAlgorithm::Priority(list) => {
for choice in list { for choice in list {
check_connector_choice(choice)?; connector_choice(choice)?;
} }
} }
routing_types::RoutingAlgorithm::VolumeSplit(splits) => { routing_types::RoutingAlgorithm::VolumeSplit(splits) => {
for split in splits { for split in splits {
check_connector_choice(&split.connector)?; connector_choice(&split.connector)?;
} }
} }
@ -389,13 +471,13 @@ pub async fn validate_connectors_in_routing_config(
match selection { match selection {
routing_types::ConnectorSelection::VolumeSplit(splits) => { routing_types::ConnectorSelection::VolumeSplit(splits) => {
for split in splits { for split in splits {
check_connector_choice(&split.connector)?; connector_choice(&split.connector)?;
} }
} }
routing_types::ConnectorSelection::Priority(list) => { routing_types::ConnectorSelection::Priority(list) => {
for choice in list { for choice in list {
check_connector_choice(choice)?; connector_choice(choice)?;
} }
} }
} }

View File

@ -1,7 +1,11 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use actix_web::{web, Scope}; use actix_web::{web, Scope};
#[cfg(feature = "olap")] #[cfg(all(
feature = "olap",
any(feature = "v1", feature = "v2"),
not(feature = "routing_v2")
))]
use api_models::routing::RoutingRetrieveQuery; use api_models::routing::RoutingRetrieveQuery;
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
use common_enums::TransactionType; use common_enums::TransactionType;
@ -41,7 +45,7 @@ use super::pm_auth;
#[cfg(feature = "oltp")] #[cfg(feature = "oltp")]
use super::poll::retrieve_poll_status; use super::poll::retrieve_poll_status;
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
use super::routing as cloud_routing; use super::routing;
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains};
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
@ -66,7 +70,6 @@ use crate::routes::fraud_check as frm_routes;
use crate::routes::recon as recon_routes; use crate::routes::recon as recon_routes;
pub use crate::{ pub use crate::{
configs::settings, configs::settings,
core::routing,
db::{CommonStorageInterface, GlobalStorageInterface, StorageImpl, StorageInterface}, db::{CommonStorageInterface, GlobalStorageInterface, StorageImpl, StorageInterface},
events::EventsHandler, events::EventsHandler,
routes::cards_info::card_iin_info, routes::cards_info::card_iin_info,
@ -612,7 +615,27 @@ impl Forex {
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
pub struct Routing; pub struct Routing;
#[cfg(feature = "olap")] #[cfg(all(feature = "olap", feature = "v2", feature = "routing_v2"))]
impl Routing {
pub fn server(state: AppState) -> Scope {
web::scope("/v2/routing_algorithm")
.app_data(web::Data::new(state.clone()))
.service(
web::resource("").route(web::post().to(|state, req, payload| {
routing::routing_create_config(state, req, payload, &TransactionType::Payment)
})),
)
.service(
web::resource("/{algorithm_id}")
.route(web::get().to(routing::routing_retrieve_config)),
)
}
}
#[cfg(all(
feature = "olap",
any(feature = "v1", feature = "v2"),
not(feature = "routing_v2")
))]
impl Routing { impl Routing {
pub fn server(state: AppState) -> Scope { pub fn server(state: AppState) -> Scope {
#[allow(unused_mut)] #[allow(unused_mut)]
@ -620,7 +643,7 @@ impl Routing {
.app_data(web::Data::new(state.clone())) .app_data(web::Data::new(state.clone()))
.service( .service(
web::resource("/active").route(web::get().to(|state, req, query_params| { web::resource("/active").route(web::get().to(|state, req, query_params| {
cloud_routing::routing_retrieve_linked_config( routing::routing_retrieve_linked_config(
state, state,
req, req,
query_params, query_params,
@ -632,7 +655,7 @@ impl Routing {
web::resource("") web::resource("")
.route( .route(
web::get().to(|state, req, path: web::Query<RoutingRetrieveQuery>| { web::get().to(|state, req, path: web::Query<RoutingRetrieveQuery>| {
cloud_routing::list_routing_configs( routing::list_routing_configs(
state, state,
req, req,
path, path,
@ -641,7 +664,7 @@ impl Routing {
}), }),
) )
.route(web::post().to(|state, req, payload| { .route(web::post().to(|state, req, payload| {
cloud_routing::routing_create_config( routing::routing_create_config(
state, state,
req, req,
payload, payload,
@ -652,14 +675,14 @@ impl Routing {
.service( .service(
web::resource("/default") web::resource("/default")
.route(web::get().to(|state, req| { .route(web::get().to(|state, req| {
cloud_routing::routing_retrieve_default_config( routing::routing_retrieve_default_config(
state, state,
req, req,
&TransactionType::Payment, &TransactionType::Payment,
) )
})) }))
.route(web::post().to(|state, req, payload| { .route(web::post().to(|state, req, payload| {
cloud_routing::routing_update_default_config( routing::routing_update_default_config(
state, state,
req, req,
payload, payload,
@ -669,32 +692,25 @@ impl Routing {
) )
.service( .service(
web::resource("/deactivate").route(web::post().to(|state, req, payload| { web::resource("/deactivate").route(web::post().to(|state, req, payload| {
cloud_routing::routing_unlink_config( routing::routing_unlink_config(state, req, payload, &TransactionType::Payment)
state,
req,
payload,
&TransactionType::Payment,
)
})), })),
) )
.service( .service(
web::resource("/decision") web::resource("/decision")
.route(web::put().to(cloud_routing::upsert_decision_manager_config)) .route(web::put().to(routing::upsert_decision_manager_config))
.route(web::get().to(cloud_routing::retrieve_decision_manager_config)) .route(web::get().to(routing::retrieve_decision_manager_config))
.route(web::delete().to(cloud_routing::delete_decision_manager_config)), .route(web::delete().to(routing::delete_decision_manager_config)),
) )
.service( .service(
web::resource("/decision/surcharge") web::resource("/decision/surcharge")
.route(web::put().to(cloud_routing::upsert_surcharge_decision_manager_config)) .route(web::put().to(routing::upsert_surcharge_decision_manager_config))
.route(web::get().to(cloud_routing::retrieve_surcharge_decision_manager_config)) .route(web::get().to(routing::retrieve_surcharge_decision_manager_config))
.route( .route(web::delete().to(routing::delete_surcharge_decision_manager_config)),
web::delete().to(cloud_routing::delete_surcharge_decision_manager_config),
),
) )
.service( .service(
web::resource("/default/profile/{profile_id}").route(web::post().to( web::resource("/default/profile/{profile_id}").route(web::post().to(
|state, req, path, payload| { |state, req, path, payload| {
cloud_routing::routing_update_default_config_for_profile( routing::routing_update_default_config_for_profile(
state, state,
req, req,
path, path,
@ -706,7 +722,7 @@ impl Routing {
) )
.service( .service(
web::resource("/default/profile").route(web::get().to(|state, req| { web::resource("/default/profile").route(web::get().to(|state, req| {
cloud_routing::routing_retrieve_default_config_for_profiles( routing::routing_retrieve_default_config_for_profiles(
state, state,
req, req,
&TransactionType::Payment, &TransactionType::Payment,
@ -721,7 +737,7 @@ impl Routing {
web::resource("/payouts") web::resource("/payouts")
.route(web::get().to( .route(web::get().to(
|state, req, path: web::Query<RoutingRetrieveQuery>| { |state, req, path: web::Query<RoutingRetrieveQuery>| {
cloud_routing::list_routing_configs( routing::list_routing_configs(
state, state,
req, req,
path, path,
@ -730,7 +746,7 @@ impl Routing {
}, },
)) ))
.route(web::post().to(|state, req, payload| { .route(web::post().to(|state, req, payload| {
cloud_routing::routing_create_config( routing::routing_create_config(
state, state,
req, req,
payload, payload,
@ -740,7 +756,7 @@ impl Routing {
) )
.service(web::resource("/payouts/active").route(web::get().to( .service(web::resource("/payouts/active").route(web::get().to(
|state, req, query_params| { |state, req, query_params| {
cloud_routing::routing_retrieve_linked_config( routing::routing_retrieve_linked_config(
state, state,
req, req,
query_params, query_params,
@ -751,14 +767,14 @@ impl Routing {
.service( .service(
web::resource("/payouts/default") web::resource("/payouts/default")
.route(web::get().to(|state, req| { .route(web::get().to(|state, req| {
cloud_routing::routing_retrieve_default_config( routing::routing_retrieve_default_config(
state, state,
req, req,
&TransactionType::Payout, &TransactionType::Payout,
) )
})) }))
.route(web::post().to(|state, req, payload| { .route(web::post().to(|state, req, payload| {
cloud_routing::routing_update_default_config( routing::routing_update_default_config(
state, state,
req, req,
payload, payload,
@ -769,18 +785,13 @@ impl Routing {
.service( .service(
web::resource("/payouts/{algorithm_id}/activate").route(web::post().to( web::resource("/payouts/{algorithm_id}/activate").route(web::post().to(
|state, req, path| { |state, req, path| {
cloud_routing::routing_link_config( routing::routing_link_config(state, req, path, &TransactionType::Payout)
state,
req,
path,
&TransactionType::Payout,
)
}, },
)), )),
) )
.service(web::resource("/payouts/deactivate").route(web::post().to( .service(web::resource("/payouts/deactivate").route(web::post().to(
|state, req, payload| { |state, req, payload| {
cloud_routing::routing_unlink_config( routing::routing_unlink_config(
state, state,
req, req,
payload, payload,
@ -791,7 +802,7 @@ impl Routing {
.service( .service(
web::resource("/payouts/default/profile/{profile_id}").route(web::post().to( web::resource("/payouts/default/profile/{profile_id}").route(web::post().to(
|state, req, path, payload| { |state, req, path, payload| {
cloud_routing::routing_update_default_config_for_profile( routing::routing_update_default_config_for_profile(
state, state,
req, req,
path, path,
@ -803,7 +814,7 @@ impl Routing {
) )
.service( .service(
web::resource("/payouts/default/profile").route(web::get().to(|state, req| { web::resource("/payouts/default/profile").route(web::get().to(|state, req| {
cloud_routing::routing_retrieve_default_config_for_profiles( routing::routing_retrieve_default_config_for_profiles(
state, state,
req, req,
&TransactionType::Payout, &TransactionType::Payout,
@ -815,17 +826,12 @@ impl Routing {
route = route route = route
.service( .service(
web::resource("/{algorithm_id}") web::resource("/{algorithm_id}")
.route(web::get().to(cloud_routing::routing_retrieve_config)), .route(web::get().to(routing::routing_retrieve_config)),
) )
.service( .service(
web::resource("/{algorithm_id}/activate").route(web::post().to( web::resource("/{algorithm_id}/activate").route(web::post().to(
|state, req, path| { |state, req, path| {
cloud_routing::routing_link_config( routing::routing_link_config(state, req, path, &TransactionType::Payment)
state,
req,
path,
&TransactionType::Payment,
)
}, },
)), )),
); );
@ -1410,8 +1416,64 @@ impl PayoutLink {
} }
pub struct BusinessProfile; pub struct BusinessProfile;
#[cfg(all(feature = "olap", feature = "v2", feature = "routing_v2"))]
#[cfg(feature = "olap")] impl BusinessProfile {
pub fn server(state: AppState) -> Scope {
web::scope("/v2/profiles")
.app_data(web::Data::new(state))
.service(
web::scope("/{profile_id}")
.service(
web::resource("/fallback_routing")
.route(web::get().to(|state, req| {
routing::routing_retrieve_default_config(
state,
req,
&TransactionType::Payment,
)
}))
.route(web::post().to(|state, req, payload| {
routing::routing_update_default_config(
state,
req,
payload,
&TransactionType::Payment,
)
})),
)
.service(
web::resource("/activate_routing_algorithm").route(web::patch().to(
|state, req, path, payload| {
routing::routing_link_config(
state,
req,
path,
payload,
&TransactionType::Payment,
)
},
)),
)
.service(
web::resource("/deactivate_routing_algorithm").route(web::post().to(
|state, req, path| {
routing::routing_unlink_config(
state,
req,
path,
&TransactionType::Payment,
)
},
)),
),
)
}
}
#[cfg(all(
feature = "olap",
any(feature = "v1", feature = "v2"),
not(feature = "routing_v2")
))]
impl BusinessProfile { impl BusinessProfile {
pub fn server(state: AppState) -> Scope { pub fn server(state: AppState) -> Scope {
web::scope("/account/{account_id}/business_profile") web::scope("/account/{account_id}/business_profile")

View File

@ -54,7 +54,11 @@ pub async fn routing_create_config(
.await .await
} }
#[cfg(feature = "olap")] #[cfg(all(
feature = "olap",
any(feature = "v1", feature = "v2"),
not(feature = "routing_v2")
))]
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn routing_link_config( pub async fn routing_link_config(
state: web::Data<AppState>, state: web::Data<AppState>,
@ -89,6 +93,48 @@ pub async fn routing_link_config(
.await .await
} }
#[cfg(all(feature = "olap", feature = "v2", feature = "routing_v2"))]
#[instrument(skip_all)]
pub async fn routing_link_config(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<String>,
json_payload: web::Json<routing_types::RoutingAlgorithmId>,
transaction_type: &enums::TransactionType,
) -> impl Responder {
let flow = Flow::RoutingLinkConfig;
let wrapper = routing_types::RoutingLinkWrapper {
profile_id: path.into_inner(),
algorithm_id: json_payload.into_inner(),
};
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
wrapper,
|state, auth: auth::AuthenticationData, wrapper, _| {
routing::link_routing_config(
state,
auth.merchant_account,
wrapper.profile_id,
wrapper.algorithm_id.0,
transaction_type,
)
},
#[cfg(not(feature = "release"))]
auth::auth_type(
&auth::ApiKeyAuth,
&auth::JWTAuth(Permission::RoutingWrite),
req.headers(),
),
#[cfg(feature = "release")]
&auth::JWTAuth(Permission::RoutingWrite),
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn routing_retrieve_config( pub async fn routing_retrieve_config(

View File

@ -55,7 +55,7 @@ check_v2 *FLAGS:
jq -r ' jq -r '
[ ( .workspace_members | sort ) as $package_ids # Store workspace crate package IDs in `package_ids` array [ ( .workspace_members | sort ) as $package_ids # Store workspace crate package IDs in `package_ids` array
| .packages[] | select( IN(.id; $package_ids[]) ) | .features | keys[] ] | unique # Select all unique features from all workspace crates | .packages[] | select( IN(.id; $package_ids[]) ) | .features | keys[] ] | unique # Select all unique features from all workspace crates
| del( .[] | select( any( . ; . == ("v1", "merchant_account_v2", "payment_v2") ) ) ) # Exclude some features from features list | del( .[] | select( any( . ; . == ("v1", "merchant_account_v2", "payment_v2","routing_v2") ) ) ) # Exclude some features from features list
| join(",") # Construct a comma-separated string of features for passing to `cargo` | join(",") # Construct a comma-separated string of features for passing to `cargo`
')" ')"