feat: add support for 3ds and surcharge decision through routing rules (#2869)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Hrithikesh
2023-11-21 20:25:50 +05:30
committed by GitHub
parent 3f3b797dc6
commit f8618e0770
23 changed files with 1717 additions and 58 deletions

View File

@ -0,0 +1,113 @@
use common_utils::events;
use euclid::{
dssa::types::EuclidAnalysable,
enums,
frontend::{
ast::Program,
dir::{DirKeyKind, DirValue, EuclidDirFilter},
},
types::Metadata,
};
use serde::{Deserialize, Serialize};
#[derive(
Clone,
Debug,
Hash,
PartialEq,
Eq,
strum::Display,
strum::EnumVariantNames,
strum::EnumIter,
strum::EnumString,
Serialize,
Deserialize,
)]
#[serde(rename_all = "snake_case")]
#[strum(serialize_all = "snake_case")]
pub enum AuthenticationType {
ThreeDs,
NoThreeDs,
}
impl AuthenticationType {
pub fn to_dir_value(&self) -> DirValue {
match self {
Self::ThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::ThreeDs),
Self::NoThreeDs => DirValue::AuthenticationType(enums::AuthenticationType::NoThreeDs),
}
}
}
impl EuclidAnalysable for AuthenticationType {
fn get_dir_value_for_analysis(&self, rule_name: String) -> Vec<(DirValue, Metadata)> {
let auth = self.to_string();
vec![(
self.to_dir_value(),
std::collections::HashMap::from_iter([(
"AUTHENTICATION_TYPE".to_string(),
serde_json::json!({
"rule_name":rule_name,
"Authentication_type": auth,
}),
)]),
)]
}
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ConditionalConfigs {
pub override_3ds: Option<AuthenticationType>,
}
impl EuclidDirFilter for ConditionalConfigs {
const ALLOWED: &'static [DirKeyKind] = &[
DirKeyKind::PaymentMethod,
DirKeyKind::CardType,
DirKeyKind::CardNetwork,
DirKeyKind::MetaData,
DirKeyKind::PaymentAmount,
DirKeyKind::PaymentCurrency,
DirKeyKind::CaptureMethod,
DirKeyKind::BillingCountry,
DirKeyKind::BusinessCountry,
];
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DecisionManagerRecord {
pub name: String,
pub program: Program<ConditionalConfigs>,
pub created_at: i64,
pub modified_at: i64,
}
impl events::ApiEventMetric for DecisionManagerRecord {
fn get_api_event_type(&self) -> Option<events::ApiEventsType> {
Some(events::ApiEventsType::Routing)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConditionalConfigReq {
pub name: Option<String>,
pub algorithm: Option<Program<ConditionalConfigs>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct DecisionManagerRequest {
pub name: Option<String>,
pub program: Option<Program<ConditionalConfigs>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum DecisionManager {
DecisionManagerv0(ConditionalConfigReq),
DecisionManagerv1(DecisionManagerRequest),
}
impl events::ApiEventMetric for DecisionManager {
fn get_api_event_type(&self) -> Option<events::ApiEventsType> {
Some(events::ApiEventsType::Routing)
}
}
pub type DecisionManagerResponse = DecisionManagerRecord;

View File

@ -4,6 +4,7 @@ pub mod analytics;
pub mod api_keys;
pub mod bank_accounts;
pub mod cards_info;
pub mod conditional_configs;
pub mod customers;
pub mod disputes;
pub mod enums;
@ -22,6 +23,7 @@ pub mod payments;
pub mod payouts;
pub mod refunds;
pub mod routing;
pub mod surcharge_decision_configs;
pub mod user;
pub mod verifications;
pub mod webhooks;

View File

@ -0,0 +1,77 @@
use common_utils::{consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, events, types::Percentage};
use euclid::frontend::{
ast::Program,
dir::{DirKeyKind, EuclidDirFilter},
};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SurchargeDetails {
pub surcharge: Surcharge,
pub tax_on_surcharge: Option<Percentage<SURCHARGE_PERCENTAGE_PRECISION_LENGTH>>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type", content = "value")]
pub enum Surcharge {
Fixed(i64),
Rate(Percentage<SURCHARGE_PERCENTAGE_PRECISION_LENGTH>),
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct SurchargeDecisionConfigs {
pub surcharge_details: Option<SurchargeDetails>,
}
impl EuclidDirFilter for SurchargeDecisionConfigs {
const ALLOWED: &'static [DirKeyKind] = &[
DirKeyKind::PaymentMethod,
DirKeyKind::MetaData,
DirKeyKind::PaymentAmount,
DirKeyKind::PaymentCurrency,
DirKeyKind::BillingCountry,
DirKeyKind::CardType,
DirKeyKind::CardNetwork,
DirKeyKind::PayLaterType,
DirKeyKind::WalletType,
DirKeyKind::BankTransferType,
DirKeyKind::BankRedirectType,
DirKeyKind::BankDebitType,
DirKeyKind::CryptoType,
];
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SurchargeDecisionManagerRecord {
pub name: String,
pub merchant_surcharge_configs: MerchantSurchargeConfigs,
pub algorithm: Program<SurchargeDecisionConfigs>,
pub created_at: i64,
pub modified_at: i64,
}
impl events::ApiEventMetric for SurchargeDecisionManagerRecord {
fn get_api_event_type(&self) -> Option<events::ApiEventsType> {
Some(events::ApiEventsType::Routing)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct SurchargeDecisionConfigReq {
pub name: Option<String>,
pub merchant_surcharge_configs: MerchantSurchargeConfigs,
pub algorithm: Option<Program<SurchargeDecisionConfigs>>,
}
impl events::ApiEventMetric for SurchargeDecisionConfigReq {
fn get_api_event_type(&self) -> Option<events::ApiEventsType> {
Some(events::ApiEventsType::Routing)
}
}
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
pub struct MerchantSurchargeConfigs {
pub show_surcharge_breakup_screen: Option<bool>,
}
pub type SurchargeDecisionManagerResponse = SurchargeDecisionManagerRecord;

View File

@ -3,6 +3,7 @@ pub mod api_keys;
pub mod api_locking;
pub mod cache;
pub mod cards_info;
pub mod conditional_config;
pub mod configs;
pub mod customers;
pub mod disputes;
@ -19,6 +20,7 @@ pub mod payments;
pub mod payouts;
pub mod refunds;
pub mod routing;
pub mod surcharge_decision_config;
#[cfg(feature = "olap")]
pub mod user;
pub mod utils;

View File

@ -0,0 +1,204 @@
use api_models::{
conditional_configs::{DecisionManager, DecisionManagerRecord, DecisionManagerResponse},
routing::{self},
};
use common_utils::ext_traits::{StringExt, ValueExt};
use diesel_models::configs;
use error_stack::{IntoReport, ResultExt};
use euclid::frontend::ast;
use super::routing::helpers::{
get_payment_config_routing_id, update_merchant_active_algorithm_ref,
};
use crate::{
core::errors::{self, RouterResponse},
routes::AppState,
services::api as service_api,
types::domain,
utils::{self, OptionExt},
};
pub async fn upsert_conditional_config(
state: AppState,
key_store: domain::MerchantKeyStore,
merchant_account: domain::MerchantAccount,
request: DecisionManager,
) -> RouterResponse<DecisionManagerRecord> {
let db = state.store.as_ref();
let (name, prog) = match request {
DecisionManager::DecisionManagerv0(ccr) => {
let name = ccr.name;
let prog = ccr
.algorithm
.get_required_value("algorithm")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "algorithm",
})
.attach_printable("Algorithm for config not given")?;
(name, prog)
}
DecisionManager::DecisionManagerv1(dmr) => {
let name = dmr.name;
let prog = dmr
.program
.get_required_value("program")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "program",
})
.attach_printable("Program for config not given")?;
(name, prog)
}
};
let timestamp = common_utils::date_time::now_unix_timestamp();
let mut algo_id: routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the routing algorithm")?
.unwrap_or_default();
let key = get_payment_config_routing_id(merchant_account.merchant_id.as_str());
let read_config_key = db.find_config_by_key(&key).await;
ast::lowering::lower_program(prog.clone())
.into_report()
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid Request Data".to_string(),
})
.attach_printable("The Request has an Invalid Comparison")?;
match read_config_key {
Ok(config) => {
let previous_record: DecisionManagerRecord = config
.config
.parse_struct("DecisionManagerRecord")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("The Payment Config Key Not Found")?;
let new_algo = DecisionManagerRecord {
name: previous_record.name,
program: prog,
modified_at: timestamp,
created_at: previous_record.created_at,
};
let serialize_updated_str =
utils::Encode::<DecisionManagerRecord>::encode_to_string_of_json(&new_algo)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to serialize config to string")?;
let updated_config = configs::ConfigUpdate::Update {
config: Some(serialize_updated_str),
};
db.update_config_by_key(&key, updated_config)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error serializing the config")?;
algo_id.update_conditional_config_id(key);
update_merchant_active_algorithm_ref(db, &key_store, algo_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update routing algorithm ref")?;
Ok(service_api::ApplicationResponse::Json(new_algo))
}
Err(e) if e.current_context().is_db_not_found() => {
let new_rec = DecisionManagerRecord {
name: name
.get_required_value("name")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "name",
})
.attach_printable("name of the config not found")?,
program: prog,
modified_at: timestamp,
created_at: timestamp,
};
let serialized_str =
utils::Encode::<DecisionManagerRecord>::encode_to_string_of_json(&new_rec)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error serializing the config")?;
let new_config = configs::ConfigNew {
key: key.clone(),
config: serialized_str,
};
db.insert_config(new_config)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error fetching the config")?;
algo_id.update_conditional_config_id(key);
update_merchant_active_algorithm_ref(db, &key_store, algo_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update routing algorithm ref")?;
Ok(service_api::ApplicationResponse::Json(new_rec))
}
Err(e) => Err(e)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error fetching payment config"),
}
}
pub async fn delete_conditional_config(
state: AppState,
key_store: domain::MerchantKeyStore,
merchant_account: domain::MerchantAccount,
) -> RouterResponse<()> {
let db = state.store.as_ref();
let key = get_payment_config_routing_id(&merchant_account.merchant_id);
let mut algo_id: routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|value| value.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the conditional_config algorithm")?
.unwrap_or_default();
algo_id.config_algo_id = None;
update_merchant_active_algorithm_ref(db, &key_store, algo_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update deleted algorithm ref")?;
db.delete_config_by_key(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to delete routing config from DB")?;
Ok(service_api::ApplicationResponse::StatusOk)
}
pub async fn retrieve_conditional_config(
state: AppState,
merchant_account: domain::MerchantAccount,
) -> RouterResponse<DecisionManagerResponse> {
let db = state.store.as_ref();
let algorithm_id = get_payment_config_routing_id(merchant_account.merchant_id.as_str());
let algo_config = db
.find_config_by_key(&algorithm_id)
.await
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)
.attach_printable("The conditional config was not found in the DB")?;
let record: DecisionManagerRecord = algo_config
.config
.parse_struct("ConditionalConfigRecord")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("The Conditional Config Record was not found")?;
let response = DecisionManagerRecord {
name: record.name,
program: record.program,
created_at: record.created_at,
modified_at: record.modified_at,
};
Ok(service_api::ApplicationResponse::Json(response))
}

View File

@ -375,3 +375,23 @@ pub enum RoutingError {
#[error("Unable to parse metadata")]
MetadataParsingError,
}
#[derive(Debug, Clone, thiserror::Error)]
pub enum ConditionalConfigError {
#[error("failed to fetch the fallback config for the merchant")]
FallbackConfigFetchFailed,
#[error("The lock on the DSL cache is most probably poisoned")]
DslCachePoisoned,
#[error("Merchant routing algorithm not found in cache")]
CacheMiss,
#[error("Expected DSL to be saved in DB but did not find")]
DslMissingInDb,
#[error("Unable to parse DSL from JSON")]
DslParsingError,
#[error("Failed to initialize DSL backend")]
DslBackendInitError,
#[error("Error executing the DSL")]
DslExecutionError,
#[error("Error constructing the Input")]
InputConstructionError,
}

View File

@ -1,4 +1,5 @@
pub mod cards;
pub mod surcharge_decision_configs;
pub mod transformers;
pub mod vault;

View File

@ -12,6 +12,7 @@ use api_models::{
ResponsePaymentMethodTypes, ResponsePaymentMethodsEnabled,
},
payments::BankCodeResponse,
surcharge_decision_configs as api_surcharge_decision_configs,
};
use common_utils::{
consts,
@ -23,6 +24,7 @@ use error_stack::{report, IntoReport, ResultExt};
use masking::Secret;
use router_env::{instrument, tracing};
use super::surcharge_decision_configs::perform_surcharge_decision_management_for_payment_method_list;
use crate::{
configs::settings,
core::{
@ -35,6 +37,7 @@ use crate::{
helpers,
routing::{self, SessionFlowRoutingInput},
},
utils::persist_individual_surcharge_details_in_redis,
},
db, logger,
pii::prelude::*,
@ -1527,6 +1530,21 @@ pub async fn list_payment_methods(
});
}
let merchant_surcharge_configs =
if let Some((attempt, payment_intent)) = payment_attempt.as_ref().zip(payment_intent) {
Box::pin(call_surcharge_decision_management(
state,
&merchant_account,
attempt,
payment_intent,
billing_address,
&mut payment_method_responses,
))
.await?
} else {
api_surcharge_decision_configs::MerchantSurchargeConfigs::default()
};
Ok(services::ApplicationResponse::Json(
api::PaymentMethodListResponse {
redirect_url: merchant_account.return_url,
@ -1558,11 +1576,69 @@ pub async fn list_payment_methods(
}
},
),
show_surcharge_breakup_screen: false,
show_surcharge_breakup_screen: merchant_surcharge_configs
.show_surcharge_breakup_screen
.unwrap_or_default(),
},
))
}
pub async fn call_surcharge_decision_management(
state: routes::AppState,
merchant_account: &domain::MerchantAccount,
payment_attempt: &storage::PaymentAttempt,
payment_intent: storage::PaymentIntent,
billing_address: Option<domain::Address>,
response_payment_method_types: &mut [ResponsePaymentMethodsEnabled],
) -> errors::RouterResult<api_surcharge_decision_configs::MerchantSurchargeConfigs> {
if payment_attempt.surcharge_amount.is_some() {
Ok(api_surcharge_decision_configs::MerchantSurchargeConfigs::default())
} else {
let algorithm_ref: routing_types::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the routing algorithm")?
.unwrap_or_default();
let (surcharge_results, merchant_sucharge_configs) =
perform_surcharge_decision_management_for_payment_method_list(
&state,
algorithm_ref,
payment_attempt,
&payment_intent,
billing_address.as_ref().map(Into::into),
response_payment_method_types,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error performing surcharge decision operation")?;
if !surcharge_results.is_empty_result() {
persist_individual_surcharge_details_in_redis(
&state,
merchant_account,
&surcharge_results,
)
.await?;
let _ = state
.store
.update_payment_intent(
payment_intent,
storage::PaymentIntentUpdate::SurchargeApplicableUpdate {
surcharge_applicable: true,
updated_by: merchant_account.storage_scheme.to_string(),
},
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)
.attach_printable("Failed to update surcharge_applicable in Payment Intent");
}
Ok(merchant_sucharge_configs)
}
}
#[allow(clippy::too_many_arguments)]
pub async fn filter_payment_methods(
payment_methods: Vec<serde_json::Value>,

View File

@ -0,0 +1,301 @@
use api_models::{
payment_methods::{self, SurchargeDetailsResponse, SurchargeMetadata},
payments::Address,
routing,
surcharge_decision_configs::{
self, SurchargeDecisionConfigs, SurchargeDecisionManagerRecord, SurchargeDetails,
},
};
use common_utils::{ext_traits::StringExt, static_cache::StaticCache};
use error_stack::{self, IntoReport, ResultExt};
use euclid::{
backend,
backend::{inputs as dsl_inputs, EuclidBackend},
};
use router_env::{instrument, tracing};
use crate::{core::payments::PaymentData, db::StorageInterface, types::storage as oss_storage};
static CONF_CACHE: StaticCache<VirInterpreterBackendCacheWrapper> = StaticCache::new();
use crate::{
core::{
errors::ConditionalConfigError as ConfigError,
payments::{
conditional_configs::ConditionalConfigResult, routing::make_dsl_input_for_surcharge,
},
},
AppState,
};
struct VirInterpreterBackendCacheWrapper {
cached_alogorith: backend::VirInterpreterBackend<SurchargeDecisionConfigs>,
merchant_surcharge_configs: surcharge_decision_configs::MerchantSurchargeConfigs,
}
impl TryFrom<SurchargeDecisionManagerRecord> for VirInterpreterBackendCacheWrapper {
type Error = error_stack::Report<ConfigError>;
fn try_from(value: SurchargeDecisionManagerRecord) -> Result<Self, Self::Error> {
let cached_alogorith = backend::VirInterpreterBackend::with_program(value.algorithm)
.into_report()
.change_context(ConfigError::DslBackendInitError)
.attach_printable("Error initializing DSL interpreter backend")?;
let merchant_surcharge_configs = value.merchant_surcharge_configs;
Ok(Self {
cached_alogorith,
merchant_surcharge_configs,
})
}
}
pub async fn perform_surcharge_decision_management_for_payment_method_list(
state: &AppState,
algorithm_ref: routing::RoutingAlgorithmRef,
payment_attempt: &oss_storage::PaymentAttempt,
payment_intent: &oss_storage::PaymentIntent,
billing_address: Option<Address>,
response_payment_method_types: &mut [api_models::payment_methods::ResponsePaymentMethodsEnabled],
) -> ConditionalConfigResult<(
SurchargeMetadata,
surcharge_decision_configs::MerchantSurchargeConfigs,
)> {
let mut surcharge_metadata = SurchargeMetadata::new(payment_attempt.attempt_id.clone());
let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id {
id
} else {
return Ok((
surcharge_metadata,
surcharge_decision_configs::MerchantSurchargeConfigs::default(),
));
};
let key = ensure_algorithm_cached(
&*state.store,
&payment_attempt.merchant_id,
algorithm_ref.timestamp,
algorithm_id.as_str(),
)
.await?;
let cached_algo = CONF_CACHE
.retrieve(&key)
.into_report()
.change_context(ConfigError::CacheMiss)
.attach_printable("Unable to retrieve cached routing algorithm even after refresh")?;
let mut backend_input =
make_dsl_input_for_surcharge(payment_attempt, payment_intent, billing_address)
.change_context(ConfigError::InputConstructionError)?;
let interpreter = &cached_algo.cached_alogorith;
let merchant_surcharge_configs = cached_algo.merchant_surcharge_configs.clone();
for payment_methods_enabled in response_payment_method_types.iter_mut() {
for payment_method_type_response in
&mut payment_methods_enabled.payment_method_types.iter_mut()
{
let payment_method_type = payment_method_type_response.payment_method_type;
backend_input.payment_method.payment_method_type = Some(payment_method_type);
backend_input.payment_method.payment_method =
Some(payment_methods_enabled.payment_method);
if let Some(card_network_list) = &mut payment_method_type_response.card_networks {
for card_network_type in card_network_list.iter_mut() {
backend_input.payment_method.card_network =
Some(card_network_type.card_network.clone());
let surcharge_output =
execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?;
card_network_type.surcharge_details = surcharge_output
.surcharge_details
.map(|surcharge_details| {
get_surcharge_details_response(surcharge_details, payment_attempt).map(
|surcharge_details_response| {
surcharge_metadata.insert_surcharge_details(
&payment_methods_enabled.payment_method,
&payment_method_type_response.payment_method_type,
Some(&card_network_type.card_network),
surcharge_details_response.clone(),
);
surcharge_details_response
},
)
})
.transpose()?;
}
} else {
let surcharge_output =
execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?;
payment_method_type_response.surcharge_details = surcharge_output
.surcharge_details
.map(|surcharge_details| {
get_surcharge_details_response(surcharge_details, payment_attempt).map(
|surcharge_details_response| {
surcharge_metadata.insert_surcharge_details(
&payment_methods_enabled.payment_method,
&payment_method_type_response.payment_method_type,
None,
surcharge_details_response.clone(),
);
surcharge_details_response
},
)
})
.transpose()?;
}
}
}
Ok((surcharge_metadata, merchant_surcharge_configs))
}
pub async fn perform_surcharge_decision_management_for_session_flow<O>(
state: &AppState,
algorithm_ref: routing::RoutingAlgorithmRef,
payment_data: &mut PaymentData<O>,
payment_method_type_list: &Vec<common_enums::PaymentMethodType>,
) -> ConditionalConfigResult<SurchargeMetadata>
where
O: Send + Clone,
{
let mut surcharge_metadata =
SurchargeMetadata::new(payment_data.payment_attempt.attempt_id.clone());
let algorithm_id = if let Some(id) = algorithm_ref.surcharge_config_algo_id {
id
} else {
return Ok(surcharge_metadata);
};
let key = ensure_algorithm_cached(
&*state.store,
&payment_data.payment_attempt.merchant_id,
algorithm_ref.timestamp,
algorithm_id.as_str(),
)
.await?;
let cached_algo = CONF_CACHE
.retrieve(&key)
.into_report()
.change_context(ConfigError::CacheMiss)
.attach_printable("Unable to retrieve cached routing algorithm even after refresh")?;
let mut backend_input = make_dsl_input_for_surcharge(
&payment_data.payment_attempt,
&payment_data.payment_intent,
payment_data.address.billing.clone(),
)
.change_context(ConfigError::InputConstructionError)?;
let interpreter = &cached_algo.cached_alogorith;
for payment_method_type in payment_method_type_list {
backend_input.payment_method.payment_method_type = Some(*payment_method_type);
// in case of session flow, payment_method will always be wallet
backend_input.payment_method.payment_method = Some(payment_method_type.to_owned().into());
let surcharge_output =
execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?;
if let Some(surcharge_details) = surcharge_output.surcharge_details {
let surcharge_details_response =
get_surcharge_details_response(surcharge_details, &payment_data.payment_attempt)?;
surcharge_metadata.insert_surcharge_details(
&payment_method_type.to_owned().into(),
payment_method_type,
None,
surcharge_details_response,
);
}
}
Ok(surcharge_metadata)
}
fn get_surcharge_details_response(
surcharge_details: SurchargeDetails,
payment_attempt: &oss_storage::PaymentAttempt,
) -> ConditionalConfigResult<SurchargeDetailsResponse> {
let surcharge_amount = match surcharge_details.surcharge.clone() {
surcharge_decision_configs::Surcharge::Fixed(value) => value,
surcharge_decision_configs::Surcharge::Rate(percentage) => percentage
.apply_and_ceil_result(payment_attempt.amount)
.change_context(ConfigError::DslExecutionError)
.attach_printable("Failed to Calculate surcharge amount by applying percentage")?,
};
let tax_on_surcharge_amount = surcharge_details
.tax_on_surcharge
.clone()
.map(|tax_on_surcharge| {
tax_on_surcharge
.apply_and_ceil_result(surcharge_amount)
.change_context(ConfigError::DslExecutionError)
.attach_printable("Failed to Calculate tax amount")
})
.transpose()?
.unwrap_or(0);
Ok(SurchargeDetailsResponse {
surcharge: match surcharge_details.surcharge {
surcharge_decision_configs::Surcharge::Fixed(surcharge_amount) => {
payment_methods::Surcharge::Fixed(surcharge_amount)
}
surcharge_decision_configs::Surcharge::Rate(percentage) => {
payment_methods::Surcharge::Rate(percentage)
}
},
tax_on_surcharge: surcharge_details.tax_on_surcharge,
surcharge_amount,
tax_on_surcharge_amount,
final_amount: payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount,
})
}
#[instrument(skip_all)]
pub async fn ensure_algorithm_cached(
store: &dyn StorageInterface,
merchant_id: &str,
timestamp: i64,
algorithm_id: &str,
) -> ConditionalConfigResult<String> {
let key = format!("surcharge_dsl_{merchant_id}");
let present = CONF_CACHE
.present(&key)
.into_report()
.change_context(ConfigError::DslCachePoisoned)
.attach_printable("Error checking presence of DSL")?;
let expired = CONF_CACHE
.expired(&key, timestamp)
.into_report()
.change_context(ConfigError::DslCachePoisoned)
.attach_printable("Error checking presence of DSL")?;
if !present || expired {
refresh_surcharge_algorithm_cache(store, key.clone(), algorithm_id, timestamp).await?
}
Ok(key)
}
#[instrument(skip_all)]
pub async fn refresh_surcharge_algorithm_cache(
store: &dyn StorageInterface,
key: String,
algorithm_id: &str,
timestamp: i64,
) -> ConditionalConfigResult<()> {
let config = store
.find_config_by_key(algorithm_id)
.await
.change_context(ConfigError::DslMissingInDb)
.attach_printable("Error parsing DSL from config")?;
let record: SurchargeDecisionManagerRecord = config
.config
.parse_struct("Program")
.change_context(ConfigError::DslParsingError)
.attach_printable("Error parsing routing algorithm from configs")?;
let value_to_cache = VirInterpreterBackendCacheWrapper::try_from(record)?;
CONF_CACHE
.save(key, value_to_cache, timestamp)
.into_report()
.change_context(ConfigError::DslCachePoisoned)
.attach_printable("Error saving DSL to cache")?;
Ok(())
}
pub fn execute_dsl_and_get_conditional_config(
backend_input: dsl_inputs::BackendInput,
interpreter: &backend::VirInterpreterBackend<SurchargeDecisionConfigs>,
) -> ConditionalConfigResult<SurchargeDecisionConfigs> {
let routing_output = interpreter
.execute(backend_input)
.map(|out| out.connector_selection)
.into_report()
.change_context(ConfigError::DslExecutionError)?;
Ok(routing_output)
}

View File

@ -1,4 +1,5 @@
pub mod access_token;
pub mod conditional_configs;
pub mod customers;
pub mod flows;
pub mod helpers;
@ -13,9 +14,9 @@ pub mod types;
use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoIter};
use api_models::{
enums,
self, enums,
payment_methods::{Surcharge, SurchargeDetailsResponse},
payments::HeaderPayload,
payments::{self, HeaderPayload},
};
use common_utils::{ext_traits::AsyncExt, pii};
use data_models::mandates::MandateData;
@ -24,6 +25,7 @@ use error_stack::{IntoReport, ResultExt};
use futures::future::join_all;
use helpers::ApplePayData;
use masking::Secret;
use redis_interface::errors::RedisError;
use router_env::{instrument, tracing};
#[cfg(feature = "olap")]
use router_types::transformers::ForeignFrom;
@ -35,11 +37,15 @@ pub use self::operations::{
PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate,
};
use self::{
conditional_configs::perform_decision_management,
flows::{ConstructFlowSpecificData, Feature},
helpers::get_key_params_for_surcharge_details,
operations::{payment_complete_authorize, BoxedOperation, Operation},
routing::{self as self_routing, SessionFlowRoutingInput},
};
use super::errors::StorageErrorExt;
use super::{
errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils,
};
use crate::{
configs::settings::PaymentMethodTypeTokenFilter,
core::{
@ -55,8 +61,8 @@ use crate::{
self as router_types,
api::{self, ConnectorCallType},
domain,
storage::{self, enums as storage_enums},
transformers::ForeignTryInto,
storage::{self, enums as storage_enums, payment_attempt::PaymentAttemptExt},
transformers::{ForeignInto, ForeignTryInto},
},
utils::{
add_apple_pay_flow_metrics, add_connector_http_status_code_metrics, Encode, OptionExt,
@ -141,6 +147,8 @@ where
.to_not_found_response(errors::ApiErrorResponse::CustomerNotFound)
.attach_printable("Failed while fetching/creating customer")?;
call_decision_manager(state, &merchant_account, &mut payment_data).await?;
let connector = get_connector_choice(
&operation,
state,
@ -167,6 +175,10 @@ where
let mut connector_http_status_code = None;
let mut external_latency = None;
if let Some(connector_details) = connector {
operation
.to_domain()?
.populate_payment_data(state, &mut payment_data, &req, &merchant_account)
.await?;
payment_data = match connector_details {
api::ConnectorCallType::PreDetermined(connector) => {
let schedule_time = if should_add_task_to_process_tracker {
@ -294,8 +306,14 @@ where
}
api::ConnectorCallType::SessionMultiple(connectors) => {
let session_surcharge_data =
get_session_surcharge_data(&payment_data.payment_attempt);
let session_surcharge_details =
call_surcharge_decision_management_for_session_flow(
state,
&merchant_account,
&mut payment_data,
&connectors,
)
.await?;
call_multiple_connectors_service(
state,
&merchant_account,
@ -304,7 +322,7 @@ where
&operation,
payment_data,
&customer,
session_surcharge_data,
session_surcharge_details,
)
.await?
}
@ -348,6 +366,123 @@ where
))
}
#[instrument(skip_all)]
pub async fn call_decision_manager<O>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
payment_data: &mut PaymentData<O>,
) -> RouterResult<()>
where
O: Send + Clone,
{
let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the routing algorithm")?
.unwrap_or_default();
let output = perform_decision_management(
state,
algorithm_ref,
merchant_account.merchant_id.as_str(),
payment_data,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the conditional config")?;
payment_data.payment_attempt.authentication_type = payment_data
.payment_attempt
.authentication_type
.or(output.override_3ds.map(ForeignInto::foreign_into))
.or(Some(storage_enums::AuthenticationType::NoThreeDs));
Ok(())
}
#[instrument(skip_all)]
async fn populate_surcharge_details<F>(
state: &AppState,
payment_data: &mut PaymentData<F>,
request: &payments::PaymentsRequest,
) -> RouterResult<()>
where
F: Send + Clone,
{
if payment_data
.payment_intent
.surcharge_applicable
.unwrap_or(false)
{
let payment_method_data = request
.payment_method_data
.clone()
.get_required_value("payment_method_data")?;
let (payment_method, payment_method_type, card_network) =
get_key_params_for_surcharge_details(payment_method_data)?;
let calculated_surcharge_details = match utils::get_individual_surcharge_detail_from_redis(
state,
&payment_method,
&payment_method_type,
card_network,
&payment_data.payment_attempt.attempt_id,
)
.await
{
Ok(surcharge_details) => Some(surcharge_details),
Err(err) if err.current_context() == &RedisError::NotFound => None,
Err(err) => Err(err).change_context(errors::ApiErrorResponse::InternalServerError)?,
};
let request_surcharge_details = request.surcharge_details;
match (request_surcharge_details, calculated_surcharge_details) {
(Some(request_surcharge_details), Some(calculated_surcharge_details)) => {
if calculated_surcharge_details
.is_request_surcharge_matching(request_surcharge_details)
{
payment_data.surcharge_details = Some(calculated_surcharge_details);
} else {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(),
}
.into());
}
}
(None, Some(_calculated_surcharge_details)) => {
return Err(errors::ApiErrorResponse::MissingRequiredField {
field_name: "surcharge_details",
}
.into());
}
(Some(request_surcharge_details), None) => {
if request_surcharge_details.is_surcharge_zero() {
return Ok(());
} else {
return Err(errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid value provided: 'surcharge_details'. surcharge details provided do not match with surcharge details sent in payment_methods list response".to_string(),
}
.into());
}
}
(None, None) => return Ok(()),
};
} else {
let surcharge_details =
payment_data
.payment_attempt
.get_surcharge_details()
.map(|surcharge_details| {
surcharge_details
.get_surcharge_details_object(payment_data.payment_attempt.amount)
});
payment_data.surcharge_details = surcharge_details;
}
Ok(())
}
#[inline]
pub fn get_connector_data(
connectors: &mut IntoIter<api::ConnectorData>,
@ -359,20 +494,66 @@ pub fn get_connector_data(
.attach_printable("Connector not found in connectors iterator")
}
pub fn get_session_surcharge_data(
payment_attempt: &data_models::payments::payment_attempt::PaymentAttempt,
) -> Option<api::SessionSurchargeDetails> {
payment_attempt.surcharge_amount.map(|surcharge_amount| {
let tax_on_surcharge_amount = payment_attempt.tax_amount.unwrap_or(0);
let final_amount = payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount;
api::SessionSurchargeDetails::PreDetermined(SurchargeDetailsResponse {
surcharge: Surcharge::Fixed(surcharge_amount),
tax_on_surcharge: None,
surcharge_amount,
tax_on_surcharge_amount,
final_amount,
#[instrument(skip_all)]
pub async fn call_surcharge_decision_management_for_session_flow<O>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
payment_data: &mut PaymentData<O>,
session_connector_data: &[api::SessionConnectorData],
) -> RouterResult<Option<api::SessionSurchargeDetails>>
where
O: Send + Clone + Sync,
{
if let Some(surcharge_amount) = payment_data.payment_attempt.surcharge_amount {
let tax_on_surcharge_amount = payment_data.payment_attempt.tax_amount.unwrap_or(0);
let final_amount =
payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount;
Ok(Some(api::SessionSurchargeDetails::PreDetermined(
SurchargeDetailsResponse {
surcharge: Surcharge::Fixed(surcharge_amount),
tax_on_surcharge: None,
surcharge_amount,
tax_on_surcharge_amount,
final_amount,
},
)))
} else {
let payment_method_type_list = session_connector_data
.iter()
.map(|session_connector_data| session_connector_data.payment_method_type)
.collect();
let algorithm_ref: api::routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the routing algorithm")?
.unwrap_or_default();
let surcharge_results =
surcharge_decision_configs::perform_surcharge_decision_management_for_session_flow(
state,
algorithm_ref,
payment_data,
&payment_method_type_list,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("error performing surcharge decision operation")?;
core_utils::persist_individual_surcharge_details_in_redis(
state,
merchant_account,
&surcharge_results,
)
.await?;
Ok(if surcharge_results.is_empty_result() {
None
} else {
Some(api::SessionSurchargeDetails::Calculated(surcharge_results))
})
})
}
}
#[allow(clippy::too_many_arguments)]
pub async fn payments_core<F, Res, Req, Op, FData, Ctx>(

View File

@ -0,0 +1,118 @@
mod transformers;
use api_models::{
conditional_configs::{ConditionalConfigs, DecisionManagerRecord},
routing,
};
use common_utils::{ext_traits::StringExt, static_cache::StaticCache};
use error_stack::{IntoReport, ResultExt};
use euclid::backend::{self, inputs as dsl_inputs, EuclidBackend};
use router_env::{instrument, tracing};
use super::routing::make_dsl_input;
use crate::{
core::{errors, errors::ConditionalConfigError as ConfigError, payments},
routes,
};
static CONF_CACHE: StaticCache<backend::VirInterpreterBackend<ConditionalConfigs>> =
StaticCache::new();
pub type ConditionalConfigResult<O> = errors::CustomResult<O, ConfigError>;
#[instrument(skip_all)]
pub async fn perform_decision_management<F: Clone>(
state: &routes::AppState,
algorithm_ref: routing::RoutingAlgorithmRef,
merchant_id: &str,
payment_data: &mut payments::PaymentData<F>,
) -> ConditionalConfigResult<ConditionalConfigs> {
let algorithm_id = if let Some(id) = algorithm_ref.config_algo_id {
id
} else {
return Ok(ConditionalConfigs::default());
};
let key = ensure_algorithm_cached(
state,
merchant_id,
algorithm_ref.timestamp,
algorithm_id.as_str(),
)
.await?;
let cached_algo = CONF_CACHE
.retrieve(&key)
.into_report()
.change_context(ConfigError::CacheMiss)
.attach_printable("Unable to retrieve cached routing algorithm even after refresh")?;
let backend_input =
make_dsl_input(payment_data).change_context(ConfigError::InputConstructionError)?;
let interpreter = cached_algo.as_ref();
execute_dsl_and_get_conditional_config(backend_input, interpreter).await
}
#[instrument(skip_all)]
pub async fn ensure_algorithm_cached(
state: &routes::AppState,
merchant_id: &str,
timestamp: i64,
algorithm_id: &str,
) -> ConditionalConfigResult<String> {
let key = format!("dsl_{merchant_id}");
let present = CONF_CACHE
.present(&key)
.into_report()
.change_context(ConfigError::DslCachePoisoned)
.attach_printable("Error checking presece of DSL")?;
let expired = CONF_CACHE
.expired(&key, timestamp)
.into_report()
.change_context(ConfigError::DslCachePoisoned)
.attach_printable("Error checking presence of DSL")?;
if !present || expired {
refresh_routing_cache(state, key.clone(), algorithm_id, timestamp).await?;
};
Ok(key)
}
#[instrument(skip_all)]
pub async fn refresh_routing_cache(
state: &routes::AppState,
key: String,
algorithm_id: &str,
timestamp: i64,
) -> ConditionalConfigResult<()> {
let config = state
.store
.find_config_by_key(algorithm_id)
.await
.change_context(ConfigError::DslMissingInDb)
.attach_printable("Error parsing DSL from config")?;
let rec: DecisionManagerRecord = config
.config
.parse_struct("Program")
.change_context(ConfigError::DslParsingError)
.attach_printable("Error parsing routing algorithm from configs")?;
let interpreter: backend::VirInterpreterBackend<ConditionalConfigs> =
backend::VirInterpreterBackend::with_program(rec.program)
.into_report()
.change_context(ConfigError::DslBackendInitError)
.attach_printable("Error initializing DSL interpreter backend")?;
CONF_CACHE
.save(key, interpreter, timestamp)
.into_report()
.change_context(ConfigError::DslCachePoisoned)
.attach_printable("Error saving DSL to cache")?;
Ok(())
}
pub async fn execute_dsl_and_get_conditional_config(
backend_input: dsl_inputs::BackendInput,
interpreter: &backend::VirInterpreterBackend<ConditionalConfigs>,
) -> ConditionalConfigResult<ConditionalConfigs> {
let routing_output = interpreter
.execute(backend_input)
.map(|out| out.connector_selection)
.into_report()
.change_context(ConfigError::DslExecutionError)?;
Ok(routing_output)
}

View File

@ -0,0 +1,22 @@
use api_models::{self, conditional_configs};
use diesel_models::enums as storage_enums;
use euclid::enums as dsl_enums;
use crate::types::transformers::ForeignFrom;
impl ForeignFrom<dsl_enums::AuthenticationType> for conditional_configs::AuthenticationType {
fn foreign_from(from: dsl_enums::AuthenticationType) -> Self {
match from {
dsl_enums::AuthenticationType::ThreeDs => Self::ThreeDs,
dsl_enums::AuthenticationType::NoThreeDs => Self::NoThreeDs,
}
}
}
impl ForeignFrom<conditional_configs::AuthenticationType> for storage_enums::AuthenticationType {
fn foreign_from(from: conditional_configs::AuthenticationType) -> Self {
match from {
conditional_configs::AuthenticationType::ThreeDs => Self::ThreeDs,
conditional_configs::AuthenticationType::NoThreeDs => Self::NoThreeDs,
}
}
}

View File

@ -1,5 +1,6 @@
use std::borrow::Cow;
use api_models::payments::GetPaymentMethodType;
use base64::Engine;
use common_utils::{
ext_traits::{AsyncExt, ByteSliceExt, ValueExt},
@ -3516,6 +3517,106 @@ impl ApplePayData {
}
}
pub fn get_key_params_for_surcharge_details(
payment_method_data: api_models::payments::PaymentMethodData,
) -> RouterResult<(
common_enums::PaymentMethod,
common_enums::PaymentMethodType,
Option<common_enums::CardNetwork>,
)> {
match payment_method_data {
api_models::payments::PaymentMethodData::Card(card) => {
let card_type = card
.card_type
.get_required_value("payment_method_data.card.card_type")?;
let card_network = card
.card_network
.get_required_value("payment_method_data.card.card_network")?;
match card_type.to_lowercase().as_str() {
"credit" => Ok((
common_enums::PaymentMethod::Card,
common_enums::PaymentMethodType::Credit,
Some(card_network),
)),
"debit" => Ok((
common_enums::PaymentMethod::Card,
common_enums::PaymentMethodType::Debit,
Some(card_network),
)),
_ => {
logger::debug!("Invalid Card type found in payment confirm call, hence surcharge not applicable");
Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "payment_method_data.card.card_type",
}
.into())
}
}
}
api_models::payments::PaymentMethodData::CardRedirect(card_redirect_data) => Ok((
common_enums::PaymentMethod::CardRedirect,
card_redirect_data.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::Wallet(wallet) => Ok((
common_enums::PaymentMethod::Wallet,
wallet.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::PayLater(pay_later) => Ok((
common_enums::PaymentMethod::PayLater,
pay_later.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::BankRedirect(bank_redirect) => Ok((
common_enums::PaymentMethod::BankRedirect,
bank_redirect.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::BankDebit(bank_debit) => Ok((
common_enums::PaymentMethod::BankDebit,
bank_debit.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::BankTransfer(bank_transfer) => Ok((
common_enums::PaymentMethod::BankTransfer,
bank_transfer.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::Crypto(crypto) => Ok((
common_enums::PaymentMethod::Crypto,
crypto.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::MandatePayment => {
Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "payment_method_data",
}
.into())
}
api_models::payments::PaymentMethodData::Reward => {
Err(errors::ApiErrorResponse::InvalidDataValue {
field_name: "payment_method_data",
}
.into())
}
api_models::payments::PaymentMethodData::Upi(_) => Ok((
common_enums::PaymentMethod::Upi,
common_enums::PaymentMethodType::UpiCollect,
None,
)),
api_models::payments::PaymentMethodData::Voucher(voucher) => Ok((
common_enums::PaymentMethod::Voucher,
voucher.get_payment_method_type(),
None,
)),
api_models::payments::PaymentMethodData::GiftCard(gift_card) => Ok((
common_enums::PaymentMethod::GiftCard,
gift_card.get_payment_method_type(),
None,
)),
}
}
pub fn validate_payment_link_request(
payment_link_object: &api_models::payments::PaymentLinkObject,
confirm: Option<bool>,

View File

@ -152,6 +152,16 @@ pub trait Domain<F: Clone, R, Ctx: PaymentMethodRetrieve>: Send + Sync {
payment_intent: &storage::PaymentIntent,
mechant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse>;
async fn populate_payment_data<'a>(
&'a self,
_state: &AppState,
_payment_data: &mut PaymentData<F>,
_request: &R,
_merchant_account: &domain::MerchantAccount,
) -> CustomResult<(), errors::ApiErrorResponse> {
Ok(())
}
}
#[async_trait]

View File

@ -1,9 +1,6 @@
use std::marker::PhantomData;
use api_models::{
enums::FrmSuggestion,
payment_methods::{self, SurchargeDetailsResponse},
};
use api_models::enums::FrmSuggestion;
use async_trait::async_trait;
use common_utils::ext_traits::{AsyncExt, Encode};
use error_stack::{report, IntoReport, ResultExt};
@ -18,7 +15,10 @@ use crate::{
core::{
errors::{self, CustomResult, RouterResult, StorageErrorExt},
payment_methods::PaymentMethodRetrieve,
payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData},
payments::{
self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress,
PaymentData,
},
utils::{self as core_utils, get_individual_surcharge_detail_from_redis},
},
db::StorageInterface,
@ -430,11 +430,6 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
)
.await?;
let surcharge_details = Self::get_surcharge_details_from_payment_request_or_payment_attempt(
request,
&payment_attempt,
);
let payment_data = PaymentData {
flow: PhantomData,
payment_intent,
@ -465,7 +460,7 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
ephemeral_key: None,
multiple_capture_data: None,
redirect_response: None,
surcharge_details,
surcharge_details: None,
frm_message: None,
payment_link_data: None,
};
@ -574,6 +569,17 @@ impl<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
// creating the payment or if none is passed then use the routing algorithm
helpers::get_connector_default(state, request.routing.clone()).await
}
#[instrument(skip_all)]
async fn populate_payment_data<'a>(
&'a self,
state: &AppState,
payment_data: &mut PaymentData<F>,
request: &api::PaymentsRequest,
_merchant_account: &domain::MerchantAccount,
) -> CustomResult<(), errors::ApiErrorResponse> {
populate_surcharge_details(state, payment_data, request).await
}
}
#[async_trait]
@ -921,26 +927,4 @@ impl PaymentConfirm {
_ => Ok(()),
}
}
fn get_surcharge_details_from_payment_request_or_payment_attempt(
payment_request: &api::PaymentsRequest,
payment_attempt: &storage::PaymentAttempt,
) -> Option<SurchargeDetailsResponse> {
payment_request
.surcharge_details
.map(|surcharge_details| {
surcharge_details.get_surcharge_details_object(payment_attempt.amount)
}) // if not passed in confirm request, look inside payment_attempt
.or(payment_attempt
.surcharge_amount
.map(|surcharge_amount| SurchargeDetailsResponse {
surcharge: payment_methods::Surcharge::Fixed(surcharge_amount),
tax_on_surcharge: None,
surcharge_amount,
tax_on_surcharge_amount: payment_attempt.tax_amount.unwrap_or(0),
final_amount: payment_attempt.amount
+ surcharge_amount
+ payment_attempt.tax_amount.unwrap_or(0),
}))
}
}

View File

@ -9,6 +9,7 @@ use std::{
use api_models::{
admin as admin_api,
enums::{self as api_enums, CountryAlpha2},
payments::Address,
routing::ConnectorSelection,
};
use common_utils::static_cache::StaticCache;
@ -996,3 +997,60 @@ async fn perform_session_routing_for_pm_type(
Ok(final_choice)
}
pub fn make_dsl_input_for_surcharge(
payment_attempt: &oss_storage::PaymentAttempt,
payment_intent: &oss_storage::PaymentIntent,
billing_address: Option<Address>,
) -> RoutingResult<dsl_inputs::BackendInput> {
let mandate_data = dsl_inputs::MandateData {
mandate_acceptance_type: None,
mandate_type: None,
payment_type: None,
};
let payment_input = dsl_inputs::PaymentInput {
amount: payment_attempt.amount,
// currency is always populated in payment_attempt during payment create
currency: payment_attempt
.currency
.get_required_value("currency")
.change_context(errors::RoutingError::DslMissingRequiredField {
field_name: "currency".to_string(),
})?,
authentication_type: payment_attempt.authentication_type,
card_bin: None,
capture_method: payment_attempt.capture_method,
business_country: payment_intent
.business_country
.map(api_enums::Country::from_alpha2),
billing_country: billing_address
.and_then(|bic| bic.address)
.and_then(|add| add.country)
.map(api_enums::Country::from_alpha2),
business_label: payment_intent.business_label.clone(),
setup_future_usage: payment_intent.setup_future_usage,
};
let metadata = payment_intent
.metadata
.clone()
.map(|val| val.parse_value("routing_parameters"))
.transpose()
.change_context(errors::RoutingError::MetadataParsingError)
.attach_printable("Unable to parse routing_parameters from metadata of payment_intent")
.unwrap_or_else(|err| {
logger::error!(error=?err);
None
});
let payment_method_input = dsl_inputs::PaymentMethodInput {
payment_method: None,
payment_method_type: None,
card_network: None,
};
let backend_input = dsl_inputs::BackendInput {
metadata,
payment: payment_input,
payment_method: payment_method_input,
mandate: mandate_data,
};
Ok(backend_input)
}

View File

@ -0,0 +1,190 @@
use api_models::{
routing::{self},
surcharge_decision_configs::{
SurchargeDecisionConfigReq, SurchargeDecisionManagerRecord,
SurchargeDecisionManagerResponse,
},
};
use common_utils::ext_traits::{StringExt, ValueExt};
use diesel_models::configs;
use error_stack::{IntoReport, ResultExt};
use euclid::frontend::ast;
use super::routing::helpers::{
get_payment_method_surcharge_routing_id, update_merchant_active_algorithm_ref,
};
use crate::{
core::errors::{self, RouterResponse},
routes::AppState,
services::api as service_api,
types::domain,
utils::{self, OptionExt},
};
pub async fn upsert_surcharge_decision_config(
state: AppState,
key_store: domain::MerchantKeyStore,
merchant_account: domain::MerchantAccount,
request: SurchargeDecisionConfigReq,
) -> RouterResponse<SurchargeDecisionManagerRecord> {
let db = state.store.as_ref();
let name = request.name;
let program = request
.algorithm
.get_required_value("algorithm")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "algorithm",
})
.attach_printable("Program for config not given")?;
let merchant_surcharge_configs = request.merchant_surcharge_configs;
let timestamp = common_utils::date_time::now_unix_timestamp();
let mut algo_id: routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|val| val.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the routing algorithm")?
.unwrap_or_default();
let key = get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str());
let read_config_key = db.find_config_by_key(&key).await;
ast::lowering::lower_program(program.clone())
.into_report()
.change_context(errors::ApiErrorResponse::InvalidRequestData {
message: "Invalid Request Data".to_string(),
})
.attach_printable("The Request has an Invalid Comparison")?;
match read_config_key {
Ok(config) => {
let previous_record: SurchargeDecisionManagerRecord = config
.config
.parse_struct("SurchargeDecisionManagerRecord")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("The Payment Config Key Not Found")?;
let new_algo = SurchargeDecisionManagerRecord {
name: name.unwrap_or(previous_record.name),
algorithm: program,
modified_at: timestamp,
created_at: previous_record.created_at,
merchant_surcharge_configs,
};
let serialize_updated_str =
utils::Encode::<SurchargeDecisionManagerRecord>::encode_to_string_of_json(
&new_algo,
)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to serialize config to string")?;
let updated_config = configs::ConfigUpdate::Update {
config: Some(serialize_updated_str),
};
db.update_config_by_key(&key, updated_config)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error serializing the config")?;
algo_id.update_surcharge_config_id(key);
update_merchant_active_algorithm_ref(db, &key_store, algo_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update routing algorithm ref")?;
Ok(service_api::ApplicationResponse::Json(new_algo))
}
Err(e) if e.current_context().is_db_not_found() => {
let new_rec = SurchargeDecisionManagerRecord {
name: name
.get_required_value("name")
.change_context(errors::ApiErrorResponse::MissingRequiredField {
field_name: "name",
})
.attach_printable("name of the config not found")?,
algorithm: program,
merchant_surcharge_configs,
modified_at: timestamp,
created_at: timestamp,
};
let serialized_str =
utils::Encode::<SurchargeDecisionManagerRecord>::encode_to_string_of_json(&new_rec)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error serializing the config")?;
let new_config = configs::ConfigNew {
key: key.clone(),
config: serialized_str,
};
db.insert_config(new_config)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error fetching the config")?;
algo_id.update_surcharge_config_id(key);
update_merchant_active_algorithm_ref(db, &key_store, algo_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update routing algorithm ref")?;
Ok(service_api::ApplicationResponse::Json(new_rec))
}
Err(e) => Err(e)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Error fetching payment config"),
}
}
pub async fn delete_surcharge_decision_config(
state: AppState,
key_store: domain::MerchantKeyStore,
merchant_account: domain::MerchantAccount,
) -> RouterResponse<()> {
let db = state.store.as_ref();
let key = get_payment_method_surcharge_routing_id(&merchant_account.merchant_id);
let mut algo_id: routing::RoutingAlgorithmRef = merchant_account
.routing_algorithm
.clone()
.map(|value| value.parse_value("routing algorithm"))
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not decode the surcharge conditional_config algorithm")?
.unwrap_or_default();
algo_id.surcharge_config_algo_id = None;
update_merchant_active_algorithm_ref(db, &key_store, algo_id)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to update deleted algorithm ref")?;
db.delete_config_by_key(&key)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to delete routing config from DB")?;
Ok(service_api::ApplicationResponse::StatusOk)
}
pub async fn retrieve_surcharge_decision_config(
state: AppState,
merchant_account: domain::MerchantAccount,
) -> RouterResponse<SurchargeDecisionManagerResponse> {
let db = state.store.as_ref();
let algorithm_id =
get_payment_method_surcharge_routing_id(merchant_account.merchant_id.as_str());
let algo_config = db
.find_config_by_key(&algorithm_id)
.await
.change_context(errors::ApiErrorResponse::ResourceIdNotFound)
.attach_printable("The surcharge conditional config was not found in the DB")?;
let record: SurchargeDecisionManagerRecord = algo_config
.config
.parse_struct("SurchargeDecisionConfigsRecord")
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("The Surcharge Decision Config Record was not found")?;
Ok(service_api::ApplicationResponse::Json(record))
}

View File

@ -1070,6 +1070,7 @@ pub fn get_flow_name<F>() -> RouterResult<String> {
.to_string())
}
#[instrument(skip_all)]
pub async fn persist_individual_surcharge_details_in_redis(
state: &AppState,
merchant_account: &domain::MerchantAccount,
@ -1109,6 +1110,7 @@ pub async fn persist_individual_surcharge_details_in_redis(
Ok(())
}
#[instrument(skip_all)]
pub async fn get_individual_surcharge_detail_from_redis(
state: &AppState,
payment_method: &euclid_enums::PaymentMethod,

View File

@ -325,6 +325,20 @@ impl Routing {
web::resource("/deactivate")
.route(web::post().to(cloud_routing::routing_unlink_config)),
)
.service(
web::resource("/decision")
.route(web::put().to(cloud_routing::upsert_decision_manager_config))
.route(web::get().to(cloud_routing::retrieve_decision_manager_config))
.route(web::delete().to(cloud_routing::delete_decision_manager_config)),
)
.service(
web::resource("/decision/surcharge")
.route(web::put().to(cloud_routing::upsert_surcharge_decision_manager_config))
.route(web::get().to(cloud_routing::retrieve_surcharge_decision_manager_config))
.route(
web::delete().to(cloud_routing::delete_surcharge_decision_manager_config),
),
)
.service(
web::resource("/{algorithm_id}")
.route(web::get().to(cloud_routing::routing_retrieve_config)),

View File

@ -46,7 +46,10 @@ impl From<Flow> for ApiIdentifier {
| Flow::RoutingRetrieveDictionary
| Flow::RoutingUpdateConfig
| Flow::RoutingUpdateDefaultConfig
| Flow::RoutingDeleteConfig => Self::Routing,
| Flow::RoutingDeleteConfig
| Flow::DecisionManagerDeleteConfig
| Flow::DecisionManagerRetrieveConfig
| Flow::DecisionManagerUpsertConfig => Self::Routing,
Flow::MerchantConnectorsCreate
| Flow::MerchantConnectorsRetrieve

View File

@ -12,7 +12,7 @@ use router_env::{
};
use crate::{
core::{api_locking, routing},
core::{api_locking, conditional_config, routing, surcharge_decision_config},
routes::AppState,
services::{api as oss_api, authentication as auth},
};
@ -248,6 +248,172 @@ pub async fn routing_retrieve_default_config(
.await
}
#[cfg(feature = "olap")]
#[instrument(skip_all)]
pub async fn upsert_surcharge_decision_manager_config(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<api_models::surcharge_decision_configs::SurchargeDecisionConfigReq>,
) -> impl Responder {
let flow = Flow::DecisionManagerUpsertConfig;
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
|state, auth: auth::AuthenticationData, update_decision| {
surcharge_decision_config::upsert_surcharge_decision_config(
state,
auth.key_store,
auth.merchant_account,
update_decision,
)
},
#[cfg(not(feature = "release"))]
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
#[cfg(feature = "release")]
&auth::JWTAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "olap")]
#[instrument(skip_all)]
pub async fn delete_surcharge_decision_manager_config(
state: web::Data<AppState>,
req: HttpRequest,
) -> impl Responder {
let flow = Flow::DecisionManagerDeleteConfig;
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
(),
|state, auth: auth::AuthenticationData, ()| {
surcharge_decision_config::delete_surcharge_decision_config(
state,
auth.key_store,
auth.merchant_account,
)
},
#[cfg(not(feature = "release"))]
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
#[cfg(feature = "release")]
&auth::JWTAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "olap")]
#[instrument(skip_all)]
pub async fn retrieve_surcharge_decision_manager_config(
state: web::Data<AppState>,
req: HttpRequest,
) -> impl Responder {
let flow = Flow::DecisionManagerRetrieveConfig;
oss_api::server_wrap(
flow,
state,
&req,
(),
|state, auth: auth::AuthenticationData, _| {
surcharge_decision_config::retrieve_surcharge_decision_config(
state,
auth.merchant_account,
)
},
#[cfg(not(feature = "release"))]
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
#[cfg(feature = "release")]
&auth::JWTAuth,
api_locking::LockAction::NotApplicable,
)
.await
}
#[cfg(feature = "olap")]
#[instrument(skip_all)]
pub async fn upsert_decision_manager_config(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<api_models::conditional_configs::DecisionManager>,
) -> impl Responder {
let flow = Flow::DecisionManagerUpsertConfig;
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
json_payload.into_inner(),
|state, auth: auth::AuthenticationData, update_decision| {
conditional_config::upsert_conditional_config(
state,
auth.key_store,
auth.merchant_account,
update_decision,
)
},
#[cfg(not(feature = "release"))]
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
#[cfg(feature = "release")]
&auth::JWTAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "olap")]
#[instrument(skip_all)]
pub async fn delete_decision_manager_config(
state: web::Data<AppState>,
req: HttpRequest,
) -> impl Responder {
let flow = Flow::DecisionManagerDeleteConfig;
Box::pin(oss_api::server_wrap(
flow,
state,
&req,
(),
|state, auth: auth::AuthenticationData, ()| {
conditional_config::delete_conditional_config(
state,
auth.key_store,
auth.merchant_account,
)
},
#[cfg(not(feature = "release"))]
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
#[cfg(feature = "release")]
&auth::JWTAuth,
api_locking::LockAction::NotApplicable,
))
.await
}
#[cfg(feature = "olap")]
#[instrument(skip_all)]
pub async fn retrieve_decision_manager_config(
state: web::Data<AppState>,
req: HttpRequest,
) -> impl Responder {
let flow = Flow::DecisionManagerRetrieveConfig;
oss_api::server_wrap(
flow,
state,
&req,
(),
|state, auth: auth::AuthenticationData, _| {
conditional_config::retrieve_conditional_config(state, auth.merchant_account)
},
#[cfg(not(feature = "release"))]
auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()),
#[cfg(feature = "release")]
&auth::JWTAuth,
api_locking::LockAction::NotApplicable,
)
.await
}
#[cfg(feature = "olap")]
#[instrument(skip_all)]
pub async fn routing_retrieve_linked_config(

View File

@ -17,6 +17,7 @@ pub trait PaymentAttemptExt {
fn get_next_capture_id(&self) -> String;
fn get_total_amount(&self) -> i64;
fn get_surcharge_details(&self) -> Option<api_models::payments::RequestSurchargeDetails>;
}
impl PaymentAttemptExt for PaymentAttempt {
@ -58,7 +59,14 @@ impl PaymentAttemptExt for PaymentAttempt {
let next_sequence_number = self.multiple_capture_count.unwrap_or_default() + 1;
format!("{}_{}", self.attempt_id.clone(), next_sequence_number)
}
fn get_surcharge_details(&self) -> Option<api_models::payments::RequestSurchargeDetails> {
self.surcharge_amount.map(|surcharge_amount| {
api_models::payments::RequestSurchargeDetails {
surcharge_amount,
tax_amount: self.tax_amount,
}
})
}
fn get_total_amount(&self) -> i64 {
self.amount + self.surcharge_amount.unwrap_or(0) + self.tax_amount.unwrap_or(0)
}

View File

@ -247,6 +247,12 @@ pub enum Flow {
GsmRuleDelete,
/// User connect account
UserConnectAccount,
/// Upsert Decision Manager Config
DecisionManagerUpsertConfig,
/// Delete Decision Manager Config
DecisionManagerDeleteConfig,
/// Retrieve Decision Manager Config
DecisionManagerRetrieveConfig,
}
///