mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
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:
113
crates/api_models/src/conditional_configs.rs
Normal file
113
crates/api_models/src/conditional_configs.rs
Normal 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;
|
||||
@ -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;
|
||||
|
||||
77
crates/api_models/src/surcharge_decision_configs.rs
Normal file
77
crates/api_models/src/surcharge_decision_configs.rs
Normal 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;
|
||||
@ -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;
|
||||
|
||||
204
crates/router/src/core/conditional_config.rs
Normal file
204
crates/router/src/core/conditional_config.rs
Normal 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))
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
pub mod cards;
|
||||
pub mod surcharge_decision_configs;
|
||||
pub mod transformers;
|
||||
pub mod vault;
|
||||
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
@ -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>(
|
||||
|
||||
118
crates/router/src/core/payments/conditional_configs.rs
Normal file
118
crates/router/src/core/payments/conditional_configs.rs
Normal 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)
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>,
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
190
crates/router/src/core/surcharge_decision_config.rs
Normal file
190
crates/router/src/core/surcharge_decision_config.rs
Normal 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))
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user