diff --git a/Cargo.lock b/Cargo.lock index e8719b29f51d..4231c62d9499 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1708,6 +1708,7 @@ dependencies = [ "thiserror", "time", "tokio 1.32.0", + "utoipa", ] [[package]] diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index b3c6b049d5d9..84830498b344 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -8,7 +8,7 @@ use common_utils::{ types::{Percentage, Surcharge}, }; use serde::de; -use utoipa::ToSchema; +use utoipa::{schema, ToSchema}; #[cfg(feature = "payouts")] use crate::payouts; @@ -264,19 +264,6 @@ pub struct CardNetworkTypes { pub card_network: api_enums::CardNetwork, /// surcharge details for this card network - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// The list of eligible connectors for a given card network @@ -313,31 +300,19 @@ pub struct ResponsePaymentMethodTypes { pub required_fields: Option>, /// surcharge details for this payment method type if exists - #[schema(example = r#" - { - "surcharge": { - "type": "rate", - "value": { - "percentage": 2.5 - } - }, - "tax_on_surcharge": { - "percentage": 1.5 - } - } - "#)] pub surcharge_details: Option, /// auth service connector label for this payment method type, if exists pub pm_auth_connector: Option, } -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] + +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub struct SurchargeDetailsResponse { /// surcharge value - pub surcharge: Surcharge, + pub surcharge: SurchargeResponse, /// tax on surcharge value - pub tax_on_surcharge: Option>, + pub tax_on_surcharge: Option, /// surcharge amount for this payment pub display_surcharge_amount: f64, /// tax on surcharge amount for this payment @@ -348,6 +323,36 @@ pub struct SurchargeDetailsResponse { pub display_final_amount: f64, } +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum SurchargeResponse { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(SurchargePercentage), +} + +impl From for SurchargeResponse { + fn from(value: Surcharge) -> Self { + match value { + Surcharge::Fixed(amount) => Self::Fixed(amount), + Surcharge::Rate(percentage) => Self::Rate(percentage.into()), + } + } +} + +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct SurchargePercentage { + percentage: f32, +} + +impl From> for SurchargePercentage { + fn from(value: Percentage) -> Self { + Self { + percentage: value.get_percentage(), + } + } +} /// Required fields info used while listing the payment_method_data #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq, ToSchema, Hash)] pub struct RequiredFieldInfo { @@ -716,6 +721,9 @@ pub struct CustomerPaymentMethod { #[schema(example = json!({"mask": "0000"}))] pub bank: Option, + /// Surcharge details for this saved card + pub surcharge_details: Option, + /// Whether this payment method requires CVV to be collected #[schema(example = true)] pub requires_cvv: bool, diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 3619c93d772c..3a41b111b39d 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -38,6 +38,7 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } +utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates common_enums = { version = "0.1.0", path = "../common_enums" } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 545733e298ab..bbcfe45a1d0c 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -25,7 +25,10 @@ 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 super::surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, +}; use crate::{ configs::settings, core::{ @@ -38,7 +41,6 @@ use crate::{ helpers, routing::{self, SessionFlowRoutingInput}, }, - utils::persist_individual_surcharge_details_in_redis, }, db, logger, pii::prelude::*, @@ -1687,12 +1689,9 @@ pub async fn call_surcharge_decision_management( .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?; + surcharge_results + .persist_individual_surcharge_details_in_redis(&state, merchant_account) + .await?; let _ = state .store .update_payment_intent( @@ -1711,6 +1710,56 @@ pub async fn call_surcharge_decision_management( } } +pub async fn call_surcharge_decision_management_for_saved_card( + state: &routes::AppState, + merchant_account: &domain::MerchantAccount, + payment_attempt: &storage::PaymentAttempt, + payment_intent: storage::PaymentIntent, + customer_payment_method_response: &mut api::CustomerPaymentMethodsListResponse, +) -> errors::RouterResult<()> { + if payment_attempt.surcharge_amount.is_some() { + Ok(()) + } 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 = perform_surcharge_decision_management_for_saved_cards( + state, + algorithm_ref, + payment_attempt, + &payment_intent, + &mut customer_payment_method_response.customer_payment_methods, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error performing surcharge decision operation")?; + if !surcharge_results.is_empty_result() { + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .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(()) + } +} + #[allow(clippy::too_many_arguments)] pub async fn filter_payment_methods( payment_methods: Vec, @@ -2195,12 +2244,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .await } else { let cloned_secret = req.and_then(|r| r.client_secret.as_ref().cloned()); - let payment_intent = helpers::verify_payment_intent_time_and_client_secret( - db, - &merchant_account, - cloned_secret, - ) - .await?; + let payment_intent: Option = + helpers::verify_payment_intent_time_and_client_secret( + db, + &merchant_account, + cloned_secret, + ) + .await?; let customer_id = payment_intent .as_ref() .and_then(|intent| intent.customer_id.to_owned()) @@ -2326,6 +2376,7 @@ pub async fn list_customer_payment_method( created: Some(pm.created_at), bank_transfer: pmd, bank: bank_details, + surcharge_details: None, requires_cvv, }; customer_pms.push(pma.to_owned()); @@ -2377,9 +2428,36 @@ pub async fn list_customer_payment_method( } } - let response = api::CustomerPaymentMethodsListResponse { + let mut response = api::CustomerPaymentMethodsListResponse { customer_payment_methods: customer_pms, }; + let payment_attempt = payment_intent + .as_ref() + .async_map(|payment_intent| async { + state + .store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + &merchant_account.merchant_id, + &payment_intent.active_attempt.get_id(), + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + }) + .await + .transpose()?; + + if let Some((payment_attempt, payment_intent)) = payment_attempt.zip(payment_intent) { + call_surcharge_decision_management_for_saved_card( + state, + &merchant_account, + &payment_attempt, + payment_intent, + &mut response, + ) + .await?; + } Ok(services::ApplicationResponse::Json(response)) } diff --git a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs index 38ae71754b87..e130795e945a 100644 --- a/crates/router/src/core/payment_methods/surcharge_decision_configs.rs +++ b/crates/router/src/core/payment_methods/surcharge_decision_configs.rs @@ -112,9 +112,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - Some(&card_network_type.card_network), + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + Some(card_network_type.card_network.clone()), + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -138,9 +140,11 @@ pub async fn perform_surcharge_decision_management_for_payment_method_list( payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_methods_enabled.payment_method, - &payment_method_type_response.payment_method_type, - None, + types::SurchargeKey::PaymentMethodData( + payment_methods_enabled.payment_method, + payment_method_type_response.payment_method_type, + None, + ), surcharge_details.clone(), ); SurchargeDetailsResponse::foreign_try_from(( @@ -201,15 +205,82 @@ where 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_from_surcharge_output( + let surcharge_details = get_surcharge_details_from_surcharge_output( surcharge_details, &payment_data.payment_attempt, )?; surcharge_metadata.insert_surcharge_details( - &payment_method_type.to_owned().into(), - payment_method_type, - None, - surcharge_details_response, + types::SurchargeKey::PaymentMethodData( + payment_method_type.to_owned().into(), + *payment_method_type, + None, + ), + surcharge_details, + ); + } + } + Ok(surcharge_metadata) +} +pub async fn perform_surcharge_decision_management_for_saved_cards( + state: &AppState, + algorithm_ref: routing::RoutingAlgorithmRef, + payment_attempt: &oss_storage::PaymentAttempt, + payment_intent: &oss_storage::PaymentIntent, + customer_payment_method_list: &mut [api_models::payment_methods::CustomerPaymentMethod], +) -> ConditionalConfigResult { + let mut surcharge_metadata = types::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); + }; + + 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, None) + .change_context(ConfigError::InputConstructionError)?; + let interpreter = &cached_algo.cached_alogorith; + + for customer_payment_method in customer_payment_method_list.iter_mut() { + backend_input.payment_method.payment_method = Some(customer_payment_method.payment_method); + backend_input.payment_method.payment_method_type = + customer_payment_method.payment_method_type; + backend_input.payment_method.card_network = customer_payment_method + .card + .as_ref() + .and_then(|card| card.scheme.as_ref()) + .map(|scheme| { + scheme + .clone() + .parse_enum("CardNetwork") + .change_context(ConfigError::DslExecutionError) + }) + .transpose()?; + let surcharge_output = + execute_dsl_and_get_conditional_config(backend_input.clone(), interpreter)?; + if let Some(surcharge_details_output) = surcharge_output.surcharge_details { + let surcharge_details = get_surcharge_details_from_surcharge_output( + surcharge_details_output, + payment_attempt, + )?; + surcharge_metadata.insert_surcharge_details( + types::SurchargeKey::Token(customer_payment_method.payment_token.clone()), + surcharge_details.clone(), + ); + customer_payment_method.surcharge_details = Some( + SurchargeDetailsResponse::foreign_try_from((&surcharge_details, payment_attempt)) + .into_report() + .change_context(ConfigError::DslParsingError)?, ); } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 2a3ed0f1596f..16fda276f6a5 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -39,11 +39,8 @@ use self::{ helpers::get_key_params_for_surcharge_details, operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, - types::SurchargeDetails, -}; -use super::{ - errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, }; +use super::{errors::StorageErrorExt, payment_methods::surcharge_decision_configs}; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -408,26 +405,39 @@ where .surcharge_applicable .unwrap_or(false) { - let payment_method_data = payment_data + let raw_card_key = payment_data .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)?; + .as_ref() + .map(get_key_params_for_surcharge_details) + .transpose()? + .map(|(payment_method, payment_method_type, card_network)| { + types::SurchargeKey::PaymentMethodData( + payment_method, + payment_method_type, + card_network, + ) + }); + let saved_card_key = payment_data.token.clone().map(types::SurchargeKey::Token); - 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 surcharge_key = raw_card_key + .or(saved_card_key) + .get_required_value("payment_method_data or payment_token")?; + logger::debug!(surcharge_key_confirm =? surcharge_key); + + let calculated_surcharge_details = + match types::SurchargeMetadata::get_individual_surcharge_detail_from_redis( + state, + surcharge_key, + &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)? + } + }; payment_data.surcharge_details = calculated_surcharge_details; } else { @@ -436,7 +446,10 @@ where .payment_attempt .get_surcharge_details() .map(|surcharge_details| { - SurchargeDetails::from((&surcharge_details, &payment_data.payment_attempt)) + types::SurchargeDetails::from(( + &surcharge_details, + &payment_data.payment_attempt, + )) }); payment_data.surcharge_details = surcharge_details; } @@ -469,7 +482,7 @@ where let final_amount = payment_data.payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; Ok(Some(api::SessionSurchargeDetails::PreDetermined( - SurchargeDetails { + types::SurchargeDetails { surcharge: Surcharge::Fixed(surcharge_amount), tax_on_surcharge: None, surcharge_amount, @@ -501,12 +514,9 @@ where .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?; + surcharge_results + .persist_individual_surcharge_details_in_redis(state, merchant_account) + .await?; Ok(if surcharge_results.is_empty_result() { None @@ -917,6 +927,11 @@ where merchant_connector_account.get_mca_id(); } + operation + .to_domain()? + .populate_payment_data(state, payment_data, merchant_account) + .await?; + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( state, operation, @@ -1846,7 +1861,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub surcharge_details: Option, + pub surcharge_details: Option, pub frm_message: Option, pub payment_link_data: Option, pub incremental_authorization_details: Option, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c9ab77c6a332..4e491964e96c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3626,7 +3626,7 @@ impl ApplePayData { } pub fn get_key_params_for_surcharge_details( - payment_method_data: api_models::payments::PaymentMethodData, + payment_method_data: &api_models::payments::PaymentMethodData, ) -> RouterResult<( common_enums::PaymentMethod, common_enums::PaymentMethodType, @@ -3636,6 +3636,7 @@ pub fn get_key_params_for_surcharge_details( api_models::payments::PaymentMethodData::Card(card) => { let card_network = card .card_network + .clone() .get_required_value("payment_method_data.card.card_network")?; // surcharge generated will always be same for credit as well as debit // since surcharge conditions cannot be defined on card_type diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 578395860c9a..af2a9fa49c8b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -5,7 +5,6 @@ use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::{report, IntoReport, ResultExt}; use futures::FutureExt; -use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; use tracing_futures::Instrument; @@ -19,7 +18,7 @@ use crate::{ self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, }, - utils::{self as core_utils, get_individual_surcharge_detail_from_redis}, + utils::{self as core_utils}, }, db::StorageInterface, routes::AppState, @@ -439,13 +438,6 @@ impl sm }); - Self::validate_request_surcharge_details_with_session_surcharge_details( - state, - &payment_attempt, - request, - ) - .await?; - let additional_pm_data = request .payment_method_data .as_ref() @@ -904,70 +896,3 @@ impl ValidateRequest RouterResult<()> { - match ( - request.surcharge_details, - request.payment_method_data.as_ref(), - ) { - (Some(request_surcharge_details), Some(payment_method_data)) => { - if let Some(payment_method_type) = - payment_method_data.get_payment_method_type_if_session_token_type() - { - let invalid_surcharge_details_error = Err(errors::ApiErrorResponse::InvalidRequestData { - message: "surcharge_details sent in session token flow doesn't match with the one sent in confirm request".into(), - }.into()); - if let Some(attempt_surcharge_amount) = payment_attempt.surcharge_amount { - // payment_attempt.surcharge_amount will be Some if some surcharge was sent in payment create - // if surcharge was sent in payment create call, the same would have been sent to the connector during session call - // So verify the same - if request_surcharge_details.surcharge_amount != attempt_surcharge_amount - || request_surcharge_details.tax_amount != payment_attempt.tax_amount - { - return invalid_surcharge_details_error; - } - } else { - // if not sent in payment create - // verify that any calculated surcharge sent in session flow is same as the one sent in confirm - return match get_individual_surcharge_detail_from_redis( - state, - &payment_method_type.into(), - &payment_method_type, - None, - &payment_attempt.attempt_id, - ) - .await - { - Ok(surcharge_details) => utils::when( - !surcharge_details - .is_request_surcharge_matching(request_surcharge_details), - || invalid_surcharge_details_error, - ), - Err(err) if err.current_context() == &RedisError::NotFound => { - utils::when(!request_surcharge_details.is_surcharge_zero(), || { - invalid_surcharge_details_error - }) - } - Err(err) => Err(err) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to fetch redis value"), - }; - } - } - Ok(()) - } - (Some(_request_surcharge_details), None) => { - Err(errors::ApiErrorResponse::MissingRequiredField { - field_name: "payment_method_data", - } - .into()) - } - _ => Ok(()), - } - } -} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 287e4945951b..eb7f31ba24d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -287,7 +287,7 @@ impl let setup_mandate = setup_mandate.map(MandateData::from); let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_method_data_after_card_bin_call = request diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index e1c373171682..f1a35cffce87 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -337,7 +337,7 @@ impl })?; let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { - payments::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) + payments::types::SurchargeDetails::from((&request_surcharge_details, &payment_attempt)) }); let payment_data = PaymentData { diff --git a/crates/router/src/core/payments/types.rs b/crates/router/src/core/payments/types.rs index f97cdc17d724..001082d2c92e 100644 --- a/crates/router/src/core/payments/types.rs +++ b/crates/router/src/core/payments/types.rs @@ -1,13 +1,18 @@ use std::{collections::HashMap, num::TryFromIntError}; use api_models::{payment_methods::SurchargeDetailsResponse, payments::RequestSurchargeDetails}; -use common_utils::{consts, types as common_types}; +use common_utils::{consts, errors::CustomResult, ext_traits::Encode, types as common_types}; use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::{IntoReport, ResultExt}; +use redis_interface::errors::RedisError; +use router_env::{instrument, tracing}; use crate::{ + consts as router_consts, core::errors::{self, RouterResult}, + routes::AppState, types::{ + domain, storage::{self, enums as storage_enums}, transformers::ForeignTryFrom, }, @@ -215,8 +220,8 @@ impl ForeignTryFrom<(&SurchargeDetails, &PaymentAttempt)> for SurchargeDetailsRe let display_final_amount = currency.to_currency_base_unit_asf64(surcharge_details.final_amount)?; Ok(Self { - surcharge: surcharge_details.surcharge.clone(), - tax_on_surcharge: surcharge_details.tax_on_surcharge.clone(), + surcharge: surcharge_details.surcharge.clone().into(), + tax_on_surcharge: surcharge_details.tax_on_surcharge.clone().map(Into::into), display_surcharge_amount, display_tax_on_surcharge_amount, display_total_surcharge_amount: display_surcharge_amount @@ -239,16 +244,19 @@ impl SurchargeDetails { } } +#[derive(Eq, Hash, PartialEq, Clone, Debug, strum::Display)] +pub enum SurchargeKey { + Token(String), + PaymentMethodData( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), +} + #[derive(Clone, Debug)] pub struct SurchargeMetadata { - surcharge_results: HashMap< - ( - common_enums::PaymentMethod, - common_enums::PaymentMethodType, - Option, - ), - SurchargeDetails, - >, + surcharge_results: HashMap, pub payment_attempt_id: String, } @@ -267,30 +275,14 @@ impl SurchargeMetadata { } pub fn insert_surcharge_details( &mut self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, + surcharge_key: SurchargeKey, surcharge_details: SurchargeDetails, ) { - let key = ( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.insert(key, surcharge_details); + self.surcharge_results + .insert(surcharge_key, surcharge_details); } - pub fn get_surcharge_details( - &self, - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> Option<&SurchargeDetails> { - let key = &( - payment_method.to_owned(), - payment_method_type.to_owned(), - card_network.cloned(), - ); - self.surcharge_results.get(key) + pub fn get_surcharge_details(&self, surcharge_key: SurchargeKey) -> Option<&SurchargeDetails> { + self.surcharge_results.get(&surcharge_key) } pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { format!("surcharge_metadata_{}", payment_attempt_id) @@ -298,25 +290,78 @@ impl SurchargeMetadata { pub fn get_individual_surcharge_key_value_pairs(&self) -> Vec<(String, SurchargeDetails)> { self.surcharge_results .iter() - .map(|((pm, pmt, card_network), surcharge_details)| { - let key = - Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + .map(|(surcharge_key, surcharge_details)| { + let key = Self::get_surcharge_details_redis_hashset_key(surcharge_key); (key, surcharge_details.to_owned()) }) .collect() } - pub fn get_surcharge_details_redis_hashset_key( - payment_method: &common_enums::PaymentMethod, - payment_method_type: &common_enums::PaymentMethodType, - card_network: Option<&common_enums::CardNetwork>, - ) -> String { - if let Some(card_network) = card_network { - format!( - "{}_{}_{}", - payment_method, payment_method_type, card_network - ) - } else { - format!("{}_{}", payment_method, payment_method_type) + pub fn get_surcharge_details_redis_hashset_key(surcharge_key: &SurchargeKey) -> String { + match surcharge_key { + SurchargeKey::Token(token) => { + format!("token_{}", token) + } + SurchargeKey::PaymentMethodData(payment_method, payment_method_type, card_network) => { + if let Some(card_network) = card_network { + format!( + "{}_{}_{}", + payment_method, payment_method_type, card_network + ) + } else { + format!("{}_{}", payment_method, payment_method_type) + } + } } } + #[instrument(skip_all)] + pub async fn persist_individual_surcharge_details_in_redis( + &self, + state: &AppState, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult<()> { + if !self.is_empty_result() { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(&self.payment_attempt_id); + + let mut value_list = Vec::with_capacity(self.get_surcharge_results_size()); + for (key, value) in self.get_individual_surcharge_key_value_pairs().into_iter() { + value_list.push(( + key, + Encode::::encode_to_string_of_json(&value) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode to string of json")?, + )); + } + let intent_fulfillment_time = merchant_account + .intent_fulfillment_time + .unwrap_or(router_consts::DEFAULT_FULFILLMENT_TIME); + redis_conn + .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to write to redis")?; + } + Ok(()) + } + + #[instrument(skip_all)] + pub async fn get_individual_surcharge_detail_from_redis( + state: &AppState, + surcharge_key: SurchargeKey, + payment_attempt_id: &str, + ) -> CustomResult { + let redis_conn = state + .store + .get_redis_conn() + .attach_printable("Failed to get redis connection")?; + let redis_key = Self::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = Self::get_surcharge_details_redis_hashset_key(&surcharge_key); + redis_conn + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") + .await + } } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 6d82c44d803a..724a698ff700 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -4,17 +4,12 @@ use api_models::enums::{DisputeStage, DisputeStatus}; use common_enums::RequestIncrementalAuthorization; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{ - errors::CustomResult, - ext_traits::{AsyncExt, Encode}, -}; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use error_stack::{report, IntoReport, ResultExt}; -use euclid::enums as euclid_enums; -use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; -use super::payments::{helpers, types as payments_types, PaymentAddress}; +use super::payments::{helpers, PaymentAddress}; #[cfg(feature = "payouts")] use super::payouts::PayoutData; #[cfg(feature = "payouts")] @@ -1068,71 +1063,6 @@ pub fn get_flow_name() -> RouterResult { .to_string()) } -#[instrument(skip_all)] -pub async fn persist_individual_surcharge_details_in_redis( - state: &AppState, - merchant_account: &domain::MerchantAccount, - surcharge_metadata: &payments_types::SurchargeMetadata, -) -> RouterResult<()> { - if !surcharge_metadata.is_empty_result() { - let redis_conn = state - .store - .get_redis_conn() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get redis connection")?; - let redis_key = payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key( - &surcharge_metadata.payment_attempt_id, - ); - - let mut value_list = Vec::with_capacity(surcharge_metadata.get_surcharge_results_size()); - for (key, value) in surcharge_metadata - .get_individual_surcharge_key_value_pairs() - .into_iter() - { - value_list.push(( - key, - Encode::::encode_to_string_of_json(&value) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to encode to string of json")?, - )); - } - let intent_fulfillment_time = merchant_account - .intent_fulfillment_time - .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME); - redis_conn - .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) - .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to write to redis")?; - } - Ok(()) -} - -#[instrument(skip_all)] -pub async fn get_individual_surcharge_detail_from_redis( - state: &AppState, - payment_method: &euclid_enums::PaymentMethod, - payment_method_type: &euclid_enums::PaymentMethodType, - card_network: Option, - payment_attempt_id: &str, -) -> CustomResult { - let redis_conn = state - .store - .get_redis_conn() - .attach_printable("Failed to get redis connection")?; - let redis_key = - payments_types::SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); - let value_key = payments_types::SurchargeMetadata::get_surcharge_details_redis_hashset_key( - payment_method, - payment_method_type, - card_network.as_ref(), - ); - - redis_conn - .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetails") - .await -} - pub fn get_request_incremental_authorization_value( request_incremental_authorization: Option, capture_method: Option, diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index 82c98304c62b..d83117c59d76 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -319,6 +319,9 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::IncrementalAuthorizationResponse, api_models::payment_methods::RequiredFieldInfo, api_models::payment_methods::MaskedBankDetails, + api_models::payment_methods::SurchargeDetailsResponse, + api_models::payment_methods::SurchargeResponse, + api_models::payment_methods::SurchargePercentage, api_models::refunds::RefundListRequest, api_models::refunds::RefundListResponse, api_models::payments::TimeRange, @@ -363,7 +366,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::PaymentLinkResponse, api_models::payments::RetrievePaymentLinkResponse, api_models::payments::PaymentLinkInitiateRequest, - api_models::payments::PaymentLinkObject + api_models::payments::PaymentLinkObject, )), modifiers(&SecurityAddon) )] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index ea2ea8b701da..c74608ea20a1 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -238,7 +238,11 @@ impl SessionSurchargeDetails { ) -> Option { match self { Self::Calculated(surcharge_metadata) => surcharge_metadata - .get_surcharge_details(payment_method, payment_method_type, card_network) + .get_surcharge_details(payments_types::SurchargeKey::PaymentMethodData( + *payment_method, + *payment_method_type, + card_network.cloned(), + )) .cloned(), Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4f26589ba029..d67089aea35f 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -5116,6 +5116,14 @@ ], "nullable": true }, + "surcharge_details": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargeDetailsResponse" + } + ], + "nullable": true + }, "requires_cvv": { "type": "boolean", "description": "Whether this payment method requires CVV to be collected", @@ -12043,6 +12051,106 @@ } } }, + "SurchargeDetailsResponse": { + "type": "object", + "required": [ + "surcharge", + "display_surcharge_amount", + "display_tax_on_surcharge_amount", + "display_total_surcharge_amount", + "display_final_amount" + ], + "properties": { + "surcharge": { + "$ref": "#/components/schemas/SurchargeResponse" + }, + "tax_on_surcharge": { + "allOf": [ + { + "$ref": "#/components/schemas/SurchargePercentage" + } + ], + "nullable": true + }, + "display_surcharge_amount": { + "type": "number", + "format": "double", + "description": "surcharge amount for this payment" + }, + "display_tax_on_surcharge_amount": { + "type": "number", + "format": "double", + "description": "tax on surcharge amount for this payment" + }, + "display_total_surcharge_amount": { + "type": "number", + "format": "double", + "description": "sum of display_surcharge_amount and display_tax_on_surcharge_amount" + }, + "display_final_amount": { + "type": "number", + "format": "double", + "description": "sum of original amount," + } + } + }, + "SurchargePercentage": { + "type": "object", + "required": [ + "percentage" + ], + "properties": { + "percentage": { + "type": "number", + "format": "float" + } + } + }, + "SurchargeResponse": { + "oneOf": [ + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "fixed" + ] + }, + "value": { + "type": "integer", + "format": "int64", + "description": "Fixed Surcharge value" + } + } + }, + { + "type": "object", + "required": [ + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "rate" + ] + }, + "value": { + "$ref": "#/components/schemas/SurchargePercentage" + } + } + } + ], + "discriminator": { + "propertyName": "type" + } + }, "SwishQrData": { "type": "object" },