Files
Kashif 75c633fc7c feat(payouts): extend routing capabilities to payout operation (#3531)
Co-authored-by: Kashif <mohammed.kashif@juspay.in>
2024-02-26 07:30:10 +00:00

944 lines
36 KiB
Rust

pub mod helpers;
pub mod transformers;
#[cfg(feature = "business_profile_routing")]
use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery};
use api_models::{
enums,
routing::{self as routing_types, RoutingAlgorithmId},
};
#[cfg(not(feature = "business_profile_routing"))]
use common_utils::ext_traits::{Encode, StringExt};
#[cfg(not(feature = "business_profile_routing"))]
use diesel_models::configs;
#[cfg(feature = "business_profile_routing")]
use diesel_models::routing_algorithm::RoutingAlgorithm;
use error_stack::{IntoReport, ResultExt};
use rustc_hash::FxHashSet;
use super::payments;
#[cfg(feature = "payouts")]
use super::payouts;
#[cfg(feature = "business_profile_routing")]
use crate::types::transformers::{ForeignInto, ForeignTryInto};
use crate::{
consts,
core::{
errors::{RouterResponse, StorageErrorExt},
metrics, utils as core_utils,
},
routes::AppState,
types::domain,
utils::{self, OptionExt, ValueExt},
};
#[cfg(not(feature = "business_profile_routing"))]
use crate::{core::errors, services::api as service_api, types::storage};
#[cfg(feature = "business_profile_routing")]
use crate::{errors, services::api as service_api};
#[derive(Clone)]
pub enum TransactionData<'a, F>
where
F: Clone,
{
Payment(&'a payments::PaymentData<F>),
#[cfg(feature = "payouts")]
Payout(&'a payouts::PayoutData),
}
pub async fn retrieve_merchant_routing_dictionary(
state: AppState,
merchant_account: domain::MerchantAccount,
#[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveQuery,
#[cfg(feature = "business_profile_routing")] transaction_type: &enums::TransactionType,
) -> RouterResponse<routing_types::RoutingKind> {
metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE.add(&metrics::CONTEXT, 1, &[]);
#[cfg(feature = "business_profile_routing")]
{
let routing_metadata = state
.store
.list_routing_algorithm_metadata_by_merchant_id_transaction_type(
&merchant_account.merchant_id,
transaction_type,
i64::from(query_params.limit.unwrap_or_default()),
i64::from(query_params.offset.unwrap_or_default()),
)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
let result = routing_metadata
.into_iter()
.map(ForeignInto::foreign_into)
.collect::<Vec<_>>();
metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add(
&metrics::CONTEXT,
1,
&[],
);
Ok(service_api::ApplicationResponse::Json(
routing_types::RoutingKind::RoutingAlgorithm(result),
))
}
#[cfg(not(feature = "business_profile_routing"))]
metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
#[cfg(not(feature = "business_profile_routing"))]
Ok(service_api::ApplicationResponse::Json(
routing_types::RoutingKind::Config(
helpers::get_merchant_routing_dictionary(
state.store.as_ref(),
&merchant_account.merchant_id,
)
.await?,
),
))
}
pub async fn create_routing_config(
state: AppState,
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.as_ref();
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 algorithm_id = common_utils::generate_id(
consts::ROUTING_CONFIG_ID_LENGTH,
&format!("routing_{}", &merchant_account.merchant_id),
);
#[cfg(feature = "business_profile_routing")]
{
let profile_id = request
.profile_id
.get_required_value("profile_id")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "profile_id",
})
.attach_printable("Profile_id not provided")?;
core_utils::validate_and_get_business_profile(
db,
Some(&profile_id),
&merchant_account.merchant_id,
)
.await?;
helpers::validate_connectors_in_routing_config(
db,
&key_store,
&merchant_account.merchant_id,
&profile_id,
&algorithm,
)
.await?;
let timestamp = common_utils::date_time::now();
let algo = RoutingAlgorithm {
algorithm_id: algorithm_id.clone(),
profile_id,
merchant_id: merchant_account.merchant_id,
name: name.clone(),
description: Some(description.clone()),
kind: algorithm.get_kind().foreign_into(),
algorithm_data: serde_json::json!(algorithm),
created_at: timestamp,
modified_at: timestamp,
algorithm_for: transaction_type.to_owned(),
};
let record = db
.insert_routing_algorithm(algo)
.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(not(feature = "business_profile_routing"))]
{
let algorithm_str = algorithm
.encode_to_string_of_json()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to serialize routing algorithm to string")?;
let mut algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("RoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize routing algorithm ref from merchant account")?
.unwrap_or_default();
let mut merchant_dictionary =
helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?;
utils::when(
merchant_dictionary.records.len() >= consts::MAX_ROUTING_CONFIGS_PER_MERCHANT,
|| {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: format!("Reached the maximum number of routing configs ({}), please delete some to create new ones", consts::MAX_ROUTING_CONFIGS_PER_MERCHANT),
})
.into_report()
},
)?;
let timestamp = common_utils::date_time::now_unix_timestamp();
let records_are_empty = merchant_dictionary.records.is_empty();
let new_record = routing_types::RoutingDictionaryRecord {
id: algorithm_id.clone(),
name: name.clone(),
kind: algorithm.get_kind(),
description: description.clone(),
created_at: timestamp,
modified_at: timestamp,
algorithm_for: Some(*transaction_type),
};
merchant_dictionary.records.push(new_record.clone());
let new_algorithm_config = configs::ConfigNew {
key: algorithm_id.clone(),
config: algorithm_str,
};
db.insert_config(new_algorithm_config)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to save new routing algorithm config to DB")?;
if records_are_empty {
merchant_dictionary.active_id = Some(algorithm_id.clone());
algorithm_ref.update_algorithm_id(algorithm_id);
helpers::update_merchant_active_algorithm_ref(db, &key_store, algorithm_ref).await?;
}
helpers::update_merchant_routing_dictionary(
db,
&merchant_account.merchant_id,
merchant_dictionary,
)
.await?;
metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(new_record))
}
}
pub async fn link_routing_config(
state: AppState,
merchant_account: domain::MerchantAccount,
#[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore,
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();
#[cfg(feature = "business_profile_routing")]
{
let routing_algorithm = db
.find_routing_algorithm_by_algorithm_id_merchant_id(
&algorithm_id,
&merchant_account.merchant_id,
)
.await
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)?;
let business_profile = core_utils::validate_and_get_business_profile(
db,
Some(&routing_algorithm.profile_id),
&merchant_account.merchant_id,
)
.await?
.get_required_value("BusinessProfile")
.change_context(errors::ApiErrorResponse::BusinessProfileNotFound {
id: routing_algorithm.profile_id.clone(),
})?;
let mut routing_ref: routing_types::RoutingAlgorithmRef = match transaction_type {
enums::TransactionType::Payment => business_profile.routing_algorithm.clone(),
#[cfg(feature = "payouts")]
enums::TransactionType::Payout => business_profile.payout_routing_algorithm.clone(),
}
.map(|val| val.parse_value("RoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize routing algorithm ref from merchant account")?
.unwrap_or_default();
utils::when(routing_algorithm.algorithm_for != *transaction_type, || {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: format!(
"Cannot use {}'s routing algorithm for {} operation",
routing_algorithm.algorithm_for, transaction_type
),
})
})?;
utils::when(
routing_ref.algorithm_id == Some(algorithm_id.clone()),
|| {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "Algorithm is already active".to_string(),
})
.into_report()
},
)?;
routing_ref.update_algorithm_id(algorithm_id);
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.foreign_into(),
))
}
#[cfg(not(feature = "business_profile_routing"))]
{
let mut routing_ref: routing_types::RoutingAlgorithmRef = match transaction_type {
enums::TransactionType::Payment => merchant_account.routing_algorithm.clone(),
#[cfg(feature = "payouts")]
enums::TransactionType::Payout => merchant_account.payout_routing_algorithm.clone(),
}
.map(|val| val.parse_value("RoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize routing algorithm ref from merchant account")?
.unwrap_or_default();
utils::when(
routing_ref.algorithm_id == Some(algorithm_id.clone()),
|| {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "Algorithm is already active".to_string(),
})
.into_report()
},
)?;
let mut merchant_dictionary =
helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?;
let modified_at = common_utils::date_time::now_unix_timestamp();
let record = merchant_dictionary
.records
.iter_mut()
.find(|rec| rec.id == algorithm_id)
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)
.into_report()
.attach_printable("Record with given ID not found for routing config activation")?;
record.modified_at = modified_at;
merchant_dictionary.active_id = Some(record.id.clone());
let response = record.clone();
routing_ref.update_algorithm_id(algorithm_id);
helpers::update_merchant_routing_dictionary(
db,
&merchant_account.merchant_id,
merchant_dictionary,
)
.await?;
helpers::update_merchant_active_algorithm_ref(db, &key_store, routing_ref).await?;
metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(response))
}
}
pub async fn retrieve_routing_config(
state: AppState,
merchant_account: domain::MerchantAccount,
algorithm_id: RoutingAlgorithmId,
) -> RouterResponse<routing_types::MerchantRoutingAlgorithm> {
metrics::ROUTING_RETRIEVE_CONFIG.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
#[cfg(feature = "business_profile_routing")]
{
let routing_algorithm = db
.find_routing_algorithm_by_algorithm_id_merchant_id(
&algorithm_id.0,
&merchant_account.merchant_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
core_utils::validate_and_get_business_profile(
db,
Some(&routing_algorithm.profile_id),
&merchant_account.merchant_id,
)
.await?
.get_required_value("BusinessProfile")
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)?;
let response = routing_algorithm
.foreign_try_into()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to parse routing algorithm")?;
metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(response))
}
#[cfg(not(feature = "business_profile_routing"))]
{
let merchant_dictionary =
helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?;
let record = merchant_dictionary
.records
.into_iter()
.find(|rec| rec.id == algorithm_id.0)
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)
.into_report()
.attach_printable("Algorithm with the given ID not found in the merchant dictionary")?;
let algorithm_config = db
.find_config_by_key(&algorithm_id.0)
.await
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)
.attach_printable("Routing config not found in DB")?;
let algorithm: routing_types::RoutingAlgorithm = algorithm_config
.config
.parse_struct("RoutingAlgorithm")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error deserializing routing algorithm config")?;
let response = routing_types::MerchantRoutingAlgorithm {
id: record.id,
name: record.name,
description: record.description,
algorithm,
created_at: record.created_at,
modified_at: record.modified_at,
algorithm_for: record
.algorithm_for
.unwrap_or(enums::TransactionType::Payment),
};
metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(response))
}
}
pub async fn unlink_routing_config(
state: AppState,
merchant_account: domain::MerchantAccount,
#[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore,
#[cfg(feature = "business_profile_routing")] request: routing_types::RoutingConfigRequest,
transaction_type: &enums::TransactionType,
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
metrics::ROUTING_UNLINK_CONFIG.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
#[cfg(feature = "business_profile_routing")]
{
let profile_id = request
.profile_id
.get_required_value("profile_id")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "profile_id",
})
.attach_printable("Profile_id not provided")?;
let business_profile = core_utils::validate_and_get_business_profile(
db,
Some(&profile_id),
&merchant_account.merchant_id,
)
.await?;
match business_profile {
Some(business_profile) => {
let routing_algo_ref: routing_types::RoutingAlgorithmRef = match transaction_type {
enums::TransactionType::Payment => business_profile.routing_algorithm.clone(),
#[cfg(feature = "payouts")]
enums::TransactionType::Payout => {
business_profile.payout_routing_algorithm.clone()
}
}
.map(|val| val.parse_value("RoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to deserialize routing algorithm ref from merchant account",
)?
.unwrap_or_default();
let timestamp = common_utils::date_time::now_unix_timestamp();
match routing_algo_ref.algorithm_id {
Some(algorithm_id) => {
let routing_algorithm: routing_types::RoutingAlgorithmRef =
routing_types::RoutingAlgorithmRef {
algorithm_id: None,
timestamp,
config_algo_id: routing_algo_ref.config_algo_id.clone(),
surcharge_config_algo_id: routing_algo_ref.surcharge_config_algo_id,
};
let record = db
.find_routing_algorithm_by_profile_id_algorithm_id(
&profile_id,
&algorithm_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
let response = record.foreign_into();
helpers::update_business_profile_active_algorithm_ref(
db,
business_profile,
routing_algorithm,
transaction_type,
)
.await?;
metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(
&metrics::CONTEXT,
1,
&[],
);
Ok(service_api::ApplicationResponse::Json(response))
}
None => Err(errors::ApiErrorResponse::PreconditionFailed {
message: "Algorithm is already inactive".to_string(),
})
.into_report()?,
}
}
None => Err(errors::ApiErrorResponse::InvalidRequestData {
message: "The business_profile is not present".to_string(),
}
.into()),
}
}
#[cfg(not(feature = "business_profile_routing"))]
{
let mut merchant_dictionary =
helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?;
let routing_algo_ref: routing_types::RoutingAlgorithmRef = match transaction_type {
enums::TransactionType::Payment => merchant_account.routing_algorithm.clone(),
#[cfg(feature = "payouts")]
enums::TransactionType::Payout => merchant_account.payout_routing_algorithm.clone(),
}
.map(|val| val.parse_value("RoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to deserialize routing algorithm ref from merchant account")?
.unwrap_or_default();
let timestamp = common_utils::date_time::now_unix_timestamp();
utils::when(routing_algo_ref.algorithm_id.is_none(), || {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "Algorithm is already inactive".to_string(),
})
.into_report()
})?;
let routing_algorithm: routing_types::RoutingAlgorithmRef =
routing_types::RoutingAlgorithmRef {
algorithm_id: None,
timestamp,
config_algo_id: routing_algo_ref.config_algo_id.clone(),
surcharge_config_algo_id: routing_algo_ref.surcharge_config_algo_id,
};
let active_algorithm_id = merchant_dictionary
.active_id
.or(routing_algo_ref.algorithm_id.clone())
.ok_or(errors::ApiErrorResponse::PreconditionFailed {
// When the merchant_dictionary doesn't have any active algorithm and merchant_account doesn't have any routing_algorithm configured
message: "Algorithm is already inactive".to_string(),
})
.into_report()?;
let record = merchant_dictionary
.records
.iter_mut()
.find(|rec| rec.id == active_algorithm_id)
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)
.into_report()
.attach_printable("Record with the given ID not found for de-activation")?;
let response = record.clone();
merchant_dictionary.active_id = None;
helpers::update_merchant_routing_dictionary(
db,
&merchant_account.merchant_id,
merchant_dictionary,
)
.await?;
let ref_value = routing_algorithm
.encode_to_value()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed converting routing algorithm ref to json value")?;
let merchant_account_update = storage::MerchantAccountUpdate::Update {
merchant_name: None,
merchant_details: None,
return_url: None,
webhook_details: None,
sub_merchants_enabled: None,
parent_merchant_id: None,
enable_payment_response_hash: None,
payment_response_hash_key: None,
redirect_to_merchant_with_http_post: None,
publishable_key: None,
locker_id: None,
metadata: None,
routing_algorithm: Some(ref_value),
primary_business_details: None,
intent_fulfillment_time: None,
frm_routing_algorithm: None,
payout_routing_algorithm: None,
default_profile: None,
payment_link_config: None,
};
db.update_specific_fields_in_merchant(
&key_store.merchant_id,
merchant_account_update,
&key_store,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update routing algorithm ref in merchant account")?;
metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(response))
}
}
pub async fn update_default_routing_config(
state: AppState,
merchant_account: domain::MerchantAccount,
updated_config: Vec<routing_types::RoutableConnectorChoice>,
transaction_type: &enums::TransactionType,
) -> RouterResponse<Vec<routing_types::RoutableConnectorChoice>> {
metrics::ROUTING_UPDATE_CONFIG.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
let default_config =
helpers::get_merchant_default_config(db, &merchant_account.merchant_id, transaction_type)
.await?;
utils::when(default_config.len() != updated_config.len(), || {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "current config and updated config have different lengths".to_string(),
})
.into_report()
})?;
let existing_set: FxHashSet<String> =
FxHashSet::from_iter(default_config.iter().map(|c| c.to_string()));
let updated_set: FxHashSet<String> =
FxHashSet::from_iter(updated_config.iter().map(|c| c.to_string()));
let symmetric_diff: Vec<String> = existing_set
.symmetric_difference(&updated_set)
.cloned()
.collect();
utils::when(!symmetric_diff.is_empty(), || {
Err(errors::ApiErrorResponse::InvalidRequestData {
message: format!(
"connector mismatch between old and new configs ({})",
symmetric_diff.join(", ")
),
})
.into_report()
})?;
helpers::update_merchant_default_config(
db,
&merchant_account.merchant_id,
updated_config.clone(),
transaction_type,
)
.await?;
metrics::ROUTING_UPDATE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(updated_config))
}
pub async fn retrieve_default_routing_config(
state: AppState,
merchant_account: domain::MerchantAccount,
transaction_type: &enums::TransactionType,
) -> RouterResponse<Vec<routing_types::RoutableConnectorChoice>> {
metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
helpers::get_merchant_default_config(db, &merchant_account.merchant_id, transaction_type)
.await
.map(|conn_choice| {
metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE.add(
&metrics::CONTEXT,
1,
&[],
);
service_api::ApplicationResponse::Json(conn_choice)
})
}
pub async fn retrieve_linked_routing_config(
state: AppState,
merchant_account: domain::MerchantAccount,
#[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveLinkQuery,
) -> RouterResponse<routing_types::LinkedRoutingConfigRetrieveResponse> {
metrics::ROUTING_RETRIEVE_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
#[cfg(feature = "business_profile_routing")]
{
let business_profiles = if let Some(profile_id) = query_params.profile_id {
core_utils::validate_and_get_business_profile(
db,
Some(&profile_id),
&merchant_account.merchant_id,
)
.await?
.map(|profile| vec![profile])
.get_required_value("BusinessProfile")
.change_context(errors::ApiErrorResponse::BusinessProfileNotFound { id: profile_id })?
} else {
db.list_business_profile_by_merchant_id(&merchant_account.merchant_id)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?
};
let mut active_algorithms = Vec::new();
for business_profile in business_profiles {
let routing_ref: routing_types::RoutingAlgorithmRef = business_profile
.routing_algorithm
.clone()
.map(|val| val.parse_value("RoutingAlgorithmRef"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(
"unable to deserialize routing algorithm ref from merchant account",
)?
.unwrap_or_default();
if let Some(algorithm_id) = routing_ref.algorithm_id {
let record = db
.find_routing_algorithm_metadata_by_algorithm_id_profile_id(
&algorithm_id,
&business_profile.profile_id,
)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
active_algorithms.push(record.foreign_into());
}
}
metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(
routing_types::LinkedRoutingConfigRetrieveResponse::ProfileBased(active_algorithms),
))
}
#[cfg(not(feature = "business_profile_routing"))]
{
let merchant_dictionary =
helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?;
let algorithm = if let Some(algorithm_id) = merchant_dictionary.active_id {
let record = merchant_dictionary
.records
.into_iter()
.find(|rec| rec.id == algorithm_id)
.ok_or(errors::ApiErrorResponse::ResourceIdNotFound)
.into_report()
.attach_printable("record for active algorithm not found in merchant dictionary")?;
let config = db
.find_config_by_key(&algorithm_id)
.await
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error finding routing config in db")?;
let the_algorithm: routing_types::RoutingAlgorithm = config
.config
.parse_struct("RoutingAlgorithm")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("unable to parse routing algorithm")?;
Some(routing_types::MerchantRoutingAlgorithm {
id: record.id,
name: record.name,
description: record.description,
algorithm: the_algorithm,
created_at: record.created_at,
modified_at: record.modified_at,
algorithm_for: record
.algorithm_for
.unwrap_or(enums::TransactionType::Payment),
})
} else {
None
};
let response = routing_types::LinkedRoutingConfigRetrieveResponse::MerchantAccountBased(
routing_types::RoutingRetrieveResponse { algorithm },
);
metrics::ROUTING_RETRIEVE_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(response))
}
}
pub async fn retrieve_default_routing_config_for_profiles(
state: AppState,
merchant_account: domain::MerchantAccount,
transaction_type: &enums::TransactionType,
) -> RouterResponse<Vec<routing_types::ProfileDefaultRoutingConfig>> {
metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
let all_profiles = db
.list_business_profile_by_merchant_id(&merchant_account.merchant_id)
.await
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)
.attach_printable("error retrieving all business profiles for merchant")?;
let retrieve_config_futures = all_profiles
.iter()
.map(|prof| helpers::get_merchant_default_config(db, &prof.profile_id, transaction_type))
.collect::<Vec<_>>();
let configs = futures::future::join_all(retrieve_config_futures)
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()?;
let default_configs = configs
.into_iter()
.zip(all_profiles.iter().map(|prof| prof.profile_id.clone()))
.map(
|(config, profile_id)| routing_types::ProfileDefaultRoutingConfig {
profile_id,
connectors: config,
},
)
.collect::<Vec<_>>();
metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(default_configs))
}
pub async fn update_default_routing_config_for_profile(
state: AppState,
merchant_account: domain::MerchantAccount,
updated_config: Vec<routing_types::RoutableConnectorChoice>,
profile_id: String,
transaction_type: &enums::TransactionType,
) -> RouterResponse<routing_types::ProfileDefaultRoutingConfig> {
metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]);
let db = state.store.as_ref();
let business_profile = core_utils::validate_and_get_business_profile(
db,
Some(&profile_id),
&merchant_account.merchant_id,
)
.await?
.get_required_value("BusinessProfile")
.change_context(errors::ApiErrorResponse::BusinessProfileNotFound { id: profile_id })?;
let default_config =
helpers::get_merchant_default_config(db, &business_profile.profile_id, transaction_type)
.await?;
utils::when(default_config.len() != updated_config.len(), || {
Err(errors::ApiErrorResponse::PreconditionFailed {
message: "current config and updated config have different lengths".to_string(),
})
.into_report()
})?;
let existing_set = FxHashSet::from_iter(default_config.iter().map(|c| {
(
c.connector.to_string(),
#[cfg(feature = "connector_choice_mca_id")]
c.merchant_connector_id.as_ref(),
#[cfg(not(feature = "connector_choice_mca_id"))]
c.sub_label.as_ref(),
)
}));
let updated_set = FxHashSet::from_iter(updated_config.iter().map(|c| {
(
c.connector.to_string(),
#[cfg(feature = "connector_choice_mca_id")]
c.merchant_connector_id.as_ref(),
#[cfg(not(feature = "connector_choice_mca_id"))]
c.sub_label.as_ref(),
)
}));
let symmetric_diff = existing_set
.symmetric_difference(&updated_set)
.cloned()
.collect::<Vec<_>>();
utils::when(!symmetric_diff.is_empty(), || {
let error_str = symmetric_diff
.into_iter()
.map(|(connector, ident)| format!("'{connector}:{ident:?}'"))
.collect::<Vec<_>>()
.join(", ");
Err(errors::ApiErrorResponse::InvalidRequestData {
message: format!("connector mismatch between old and new configs ({error_str})"),
})
.into_report()
})?;
helpers::update_merchant_default_config(
db,
&business_profile.profile_id,
updated_config.clone(),
transaction_type,
)
.await?;
metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]);
Ok(service_api::ApplicationResponse::Json(
routing_types::ProfileDefaultRoutingConfig {
profile_id: business_profile.profile_id,
connectors: updated_config,
},
))
}