diff --git a/Cargo.lock b/Cargo.lock index 222bc02212ec..1574933810b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,7 +381,6 @@ dependencies = [ "router_derive", "serde", "serde_json", - "serde_with", "strum 0.24.1", "time", "url", @@ -1659,9 +1658,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc16" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ac624c899c6d..ce882e913282 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -25,7 +25,6 @@ mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -serde_with = "3.0.0" strum = { version = "0.24.1", features = ["derive"] } time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } url = { version = "2.4.0", features = ["serde"] } diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 289f652981eb..755acbf7f425 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -6,7 +6,6 @@ use common_utils::{ types::Percentage, }; use serde::de; -use serde_with::serde_as; use utoipa::ToSchema; #[cfg(feature = "payouts")] @@ -15,7 +14,7 @@ use crate::{ admin, customers::CustomerId, enums as api_enums, - payments::{self, BankCodeResponse}, + payments::{self, BankCodeResponse, RequestSurchargeDetails}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -342,15 +341,85 @@ pub struct SurchargeDetailsResponse { pub final_amount: i64, } -#[serde_as] -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +impl SurchargeDetailsResponse { + pub fn is_request_surcharge_matching( + &self, + request_surcharge_details: RequestSurchargeDetails, + ) -> bool { + request_surcharge_details.surcharge_amount == self.surcharge_amount + && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount + } +} + +#[derive(Clone, Debug)] pub struct SurchargeMetadata { - #[serde_as(as = "HashMap<_, _>")] - pub surcharge_results: HashMap, + surcharge_results: HashMap< + ( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), + SurchargeDetailsResponse, + >, + pub payment_attempt_id: String, } impl SurchargeMetadata { - pub fn get_key_for_surcharge_details_hash_map( + pub fn new(payment_attempt_id: String) -> Self { + Self { + surcharge_results: HashMap::new(), + payment_attempt_id, + } + } + pub fn is_empty_result(&self) -> bool { + self.surcharge_results.is_empty() + } + pub fn get_surcharge_results_size(&self) -> usize { + self.surcharge_results.len() + } + 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_details: SurchargeDetailsResponse, + ) { + let key = ( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.insert(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<&SurchargeDetailsResponse> { + let key = &( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.get(key) + } + pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { + format!("surcharge_metadata_{}", payment_attempt_id) + } + pub fn get_individual_surcharge_key_value_pairs( + &self, + ) -> Vec<(String, SurchargeDetailsResponse)> { + 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()); + (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>, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 22579ed6d6ea..cf0259f26951 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -16,6 +16,7 @@ use crate::{ admin, disputes, enums::{self as api_enums}, ephemeral_key::EphemeralKeyCreateResponse, + payment_methods::{Surcharge, SurchargeDetailsResponse}, refunds, }; @@ -319,6 +320,23 @@ pub struct RequestSurchargeDetails { pub tax_amount: Option, } +impl RequestSurchargeDetails { + pub fn is_surcharge_zero(&self) -> bool { + self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 + } + pub fn get_surcharge_details_object(&self, original_amount: i64) -> SurchargeDetailsResponse { + let surcharge_amount = self.surcharge_amount; + let tax_on_surcharge_amount = self.tax_amount.unwrap_or(0); + SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(self.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, + } + } +} + #[derive(Default, Debug, Clone, Copy)] pub struct HeaderPayload { pub payment_confirm_source: Option, @@ -810,6 +828,36 @@ pub enum PaymentMethodData { GiftCard(Box), } +impl PaymentMethodData { + pub fn get_payment_method_type_if_session_token_type( + &self, + ) -> Option { + match self { + Self::Wallet(wallet) => match wallet { + WalletData::ApplePay(_) => Some(api_enums::PaymentMethodType::ApplePay), + WalletData::GooglePay(_) => Some(api_enums::PaymentMethodType::GooglePay), + WalletData::PaypalSdk(_) => Some(api_enums::PaymentMethodType::Paypal), + _ => None, + }, + Self::PayLater(pay_later) => match pay_later { + PayLaterData::KlarnaSdk { .. } => Some(api_enums::PaymentMethodType::Klarna), + _ => None, + }, + Self::Card(_) + | Self::CardRedirect(_) + | Self::BankRedirect(_) + | Self::BankDebit(_) + | Self::BankTransfer(_) + | Self::Crypto(_) + | Self::MandatePayment + | Self::Reward + | Self::Upi(_) + | Self::Voucher(_) + | Self::GiftCard(_) => None, + } + } +} + pub trait GetPaymentMethodType { fn get_payment_method_type(&self) -> api_enums::PaymentMethodType; } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index cdd41ea9db2d..88fc7b3b524a 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -224,6 +224,8 @@ pub enum PaymentAttemptUpdate { business_sub_label: Option, amount_to_capture: Option, capture_method: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, UpdateTrackers { @@ -231,6 +233,8 @@ pub enum PaymentAttemptUpdate { connector: Option, straight_through_algorithm: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -255,8 +259,6 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -285,6 +287,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index ce388fea10eb..cd976b9e19db 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -141,6 +141,8 @@ pub enum PaymentAttemptUpdate { business_sub_label: Option, amount_to_capture: Option, capture_method: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, UpdateTrackers { @@ -148,6 +150,8 @@ pub enum PaymentAttemptUpdate { connector: Option, straight_through_algorithm: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -172,8 +176,6 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -202,6 +204,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, @@ -370,6 +374,8 @@ impl From for PaymentAttemptUpdateInternal { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => Self { amount: Some(amount), @@ -386,6 +392,8 @@ impl From for PaymentAttemptUpdateInternal { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, ..Default::default() }, @@ -415,8 +423,6 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id, } => Self { @@ -437,8 +443,6 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id, ..Default::default() @@ -479,6 +483,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, connector_response_reference_id, amount_capturable, + surcharge_amount, + tax_amount, updated_by, authentication_data, encoded_data, @@ -498,6 +504,8 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, ..Default::default() @@ -531,6 +539,8 @@ impl From for PaymentAttemptUpdateInternal { connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, } => Self { @@ -538,6 +548,8 @@ impl From for PaymentAttemptUpdateInternal { connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, ..Default::default() diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index d53fd1625fe4..ca85d19d38b0 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -248,7 +248,7 @@ impl super::RedisConnectionPool { &self, key: &str, values: V, - ttl: Option, + ttl: Option, ) -> CustomResult<(), errors::RedisError> where V: TryInto + Debug + Send + Sync, @@ -260,11 +260,10 @@ impl super::RedisConnectionPool { .await .into_report() .change_context(errors::RedisError::SetHashFailed); - // setting expiry for the key output .async_and_then(|_| { - self.set_expiry(key, ttl.unwrap_or(self.config.default_hash_ttl).into()) + self.set_expiry(key, ttl.unwrap_or(self.config.default_hash_ttl.into())) }) .await } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 234323f0179a..6b3cf11f5891 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1052,6 +1052,8 @@ pub async fn list_payment_methods( amount_capturable: None, updated_by: merchant_account.storage_scheme.to_string(), merchant_connector_id: None, + surcharge_amount: None, + tax_amount: None, }; state diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 5c8089271bd9..e7408cecf163 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -14,7 +14,7 @@ use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoI use api_models::{ enums, - payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, + payment_methods::{Surcharge, SurchargeDetailsResponse}, payments::HeaderPayload, }; use common_utils::{ext_traits::AsyncExt, pii}; @@ -290,6 +290,8 @@ where } api::ConnectorCallType::SessionMultiple(connectors) => { + let session_surcharge_data = + get_session_surcharge_data(&payment_data.payment_attempt); call_multiple_connectors_service( state, &merchant_account, @@ -298,7 +300,7 @@ where &operation, payment_data, &customer, - None, + session_surcharge_data, ) .await? } @@ -353,6 +355,21 @@ 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 { + 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, + }) + }) +} #[allow(clippy::too_many_arguments)] pub async fn payments_core( state: AppState, @@ -920,7 +937,7 @@ pub async fn call_multiple_connectors_service( _operation: &Op, mut payment_data: PaymentData, customer: &Option, - session_surcharge_metadata: Option, + session_surcharge_details: Option, ) -> RouterResult> where Op: Debug, @@ -957,18 +974,16 @@ where ) .await?; - payment_data.surcharge_details = session_surcharge_metadata - .as_ref() - .and_then(|surcharge_metadata| { - surcharge_metadata.surcharge_results.get( - &SurchargeMetadata::get_key_for_surcharge_details_hash_map( + payment_data.surcharge_details = + session_surcharge_details + .as_ref() + .and_then(|session_surcharge_details| { + session_surcharge_details.fetch_surcharge_details( &session_connector_data.payment_method_type.into(), &session_connector_data.payment_method_type, None, - ), - ) - }) - .cloned(); + ) + }); let router_data = payment_data .construct_router_data( diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 21f7db3d0b41..96cd4f5c622f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,10 +1,14 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payment_methods}; +use api_models::{ + enums::FrmSuggestion, + payment_methods::{self, SurchargeDetailsResponse}, +}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::ResultExt; use futures::FutureExt; +use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -14,6 +18,7 @@ use crate::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + utils::get_individual_surcharge_detail_from_redis, }, db::StorageInterface, routes::AppState, @@ -305,19 +310,17 @@ impl sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm }); + Self::validate_request_surcharge_details_with_session_surcharge_details( + state, + &payment_attempt, + request, + ) + .await?; - // populate payment_data.surcharge_details from request - let surcharge_details = request.surcharge_details.map(|surcharge_details| { - payment_methods::SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount: surcharge_details.surcharge_amount, - tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_details.surcharge_amount - + surcharge_details.tax_amount.unwrap_or(0), - } - }); + let surcharge_details = Self::get_surcharge_details_from_payment_request_or_payment_attempt( + request, + &payment_attempt, + ); Ok(( Box::new(self), @@ -529,14 +532,6 @@ impl .take(); let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); - let surcharge_amount = payment_data - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.surcharge_amount); - let tax_amount = payment_data - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); let authorized_amount = payment_data .surcharge_details .as_ref() @@ -562,8 +557,6 @@ impl error_code, error_message, amount_capturable: Some(authorized_amount), - surcharge_amount, - tax_amount, updated_by: storage_scheme.to_string(), merchant_connector_id, }, @@ -672,3 +665,92 @@ 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(()), + } + } + + fn get_surcharge_details_from_payment_request_or_payment_attempt( + payment_request: &api::PaymentsRequest, + payment_attempt: &storage::PaymentAttempt, + ) -> Option { + 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), + })) + } +} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 97bb84371306..fad7212c61d3 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::FrmSuggestion; +use api_models::{enums::FrmSuggestion, payment_methods}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; @@ -267,6 +267,19 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate: Option = setup_mandate.map(Into::into); + // populate payment_data.surcharge_details from request + let surcharge_details = request.surcharge_details.map(|surcharge_details| { + payment_methods::SurchargeDetailsResponse { + surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount: surcharge_details.surcharge_amount, + tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), + final_amount: payment_attempt.amount + + surcharge_details.surcharge_amount + + surcharge_details.tax_amount.unwrap_or(0), + } + }); + Ok(( operation, PaymentData { @@ -299,7 +312,7 @@ impl ephemeral_key, multiple_capture_data: None, redirect_response: None, - surcharge_details: None, + surcharge_details, frm_message: None, payment_link_data, }, @@ -421,6 +434,15 @@ impl let authorized_amount = payment_data.payment_attempt.amount; let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); + payment_data.payment_attempt = db .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, @@ -432,6 +454,8 @@ impl true => Some(authorized_amount), false => None, }, + surcharge_amount, + tax_amount, updated_by: storage_scheme.to_string(), merchant_connector_id, }, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 77c344949660..d6346a512ef1 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -466,6 +466,8 @@ async fn payment_response_update_tracker( } else { None }, + surcharge_amount: router_data.request.get_surcharge_amount(), + tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), authentication_data, encoded_data, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 0a49c830b732..26bda6d6bee6 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -304,6 +304,10 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { + request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) + }); + Ok(( next_operation, PaymentData { @@ -336,7 +340,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, - surcharge_details: None, + surcharge_details, frm_message: None, payment_link_data: None, }, @@ -467,6 +471,14 @@ impl let payment_experience = payment_data.payment_attempt.payment_experience; let amount_to_capture = payment_data.payment_attempt.amount_to_capture; let capture_method = payment_data.payment_attempt.capture_method; + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); payment_data.payment_attempt = db .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, @@ -483,6 +495,8 @@ impl business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by: storage_scheme.to_string(), }, storage_scheme, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index f58e9ea298f7..376b9048c856 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -412,6 +412,8 @@ where } else { None }, + surcharge_amount: None, + tax_amount: None, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 1eb9029ae398..fb3dc3e7d281 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,10 +1,18 @@ use std::{marker::PhantomData, str::FromStr}; -use api_models::enums::{DisputeStage, DisputeStatus}; +use api_models::{ + enums::{DisputeStage, DisputeStatus}, + payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, +}; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; +use common_utils::{ + errors::CustomResult, + ext_traits::{AsyncExt, Encode}, +}; 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; @@ -1073,3 +1081,65 @@ pub fn get_flow_name() -> RouterResult { .attach_printable("Flow stringify failed")? .to_string()) } + +pub async fn persist_individual_surcharge_details_in_redis( + state: &AppState, + merchant_account: &domain::MerchantAccount, + surcharge_metadata: &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 = 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(()) +} + +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 = SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = 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, "SurchargeDetailsResponse") + .await +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 321bf909ea0c..0a8b84ffd11c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -98,11 +98,7 @@ pub trait ConnectorValidation: ConnectorCommon { } fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented(format!( - "Surcharge not implemented for {}", - self.id() - )) - .into()) + Err(errors::ConnectorError::NotImplemented(format!("Surcharge for {}", self.id())).into()) } } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index f2e86a4bf335..7e9725d1a3b7 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -547,11 +547,31 @@ pub trait Capturable { fn get_capture_amount(&self) -> Option { Some(0) } + fn get_surcharge_amount(&self) -> Option { + None + } + fn get_tax_on_surcharge_amount(&self) -> Option { + None + } } impl Capturable for PaymentsAuthorizeData { fn get_capture_amount(&self) -> Option { - Some(self.amount) + let final_amount = self + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount); + final_amount.or(Some(self.amount)) + } + fn get_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount) + } + fn get_tax_on_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount) } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index e815740cac48..67d2d37f4fea 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -16,6 +16,7 @@ pub mod webhooks; use std::{fmt::Debug, str::FromStr}; +use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ @@ -214,6 +215,30 @@ pub struct SessionConnectorData { pub business_sub_label: Option, } +/// Session Surcharge type +pub enum SessionSurchargeDetails { + /// Surcharge is calculated by hyperswitch + Calculated(SurchargeMetadata), + /// Surcharge is sent by merchant + PreDetermined(SurchargeDetailsResponse), +} + +impl SessionSurchargeDetails { + pub fn fetch_surcharge_details( + &self, + payment_method: &enums::PaymentMethod, + payment_method_type: &enums::PaymentMethodType, + card_network: Option<&enums::CardNetwork>, + ) -> Option { + match self { + Self::Calculated(surcharge_metadata) => surcharge_metadata + .get_surcharge_details(payment_method, payment_method_type, card_network) + .cloned(), + Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), + } + } +} + pub enum ConnectorChoice { SessionMultiple(Vec), StraightThrough(serde_json::Value), diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 21002917df83..d34230e2cb49 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1138,6 +1138,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => DieselPaymentAttemptUpdate::Update { amount, @@ -1152,6 +1154,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, }, Self::UpdateTrackers { @@ -1160,12 +1164,16 @@ impl DataModelExt for PaymentAttemptUpdate { straight_through_algorithm, amount_capturable, updated_by, + surcharge_amount, + tax_amount, merchant_connector_id, } => DieselPaymentAttemptUpdate::UpdateTrackers { payment_token, connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, }, @@ -1193,8 +1201,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { @@ -1214,8 +1220,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1243,6 +1247,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, } => DieselPaymentAttemptUpdate::ResponseUpdate { @@ -1260,6 +1266,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, }, @@ -1379,6 +1387,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => Self::Update { amount, @@ -1393,6 +1403,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, }, DieselPaymentAttemptUpdate::UpdateTrackers { @@ -1401,12 +1413,16 @@ impl DataModelExt for PaymentAttemptUpdate { straight_through_algorithm, amount_capturable, updated_by, + surcharge_amount, + tax_amount, merchant_connector_id: connector_id, } => Self::UpdateTrackers { payment_token, connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1434,8 +1450,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, } => Self::ConfirmUpdate { @@ -1455,8 +1469,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1484,6 +1496,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, } => Self::ResponseUpdate { @@ -1501,6 +1515,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, }, diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 0c615d74f89a..3eadd8b83ade 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -111,7 +111,9 @@ where KvOperation::Hset(value, sql) => { logger::debug!(kv_operation= %operation, value = ?value); - redis_conn.set_hash_fields(key, value, Some(ttl)).await?; + redis_conn + .set_hash_fields(key, value, Some(ttl.into())) + .await?; store .push_to_drainer_stream::(sql, partition_key)