fix: handle session and confirm flow discrepancy in surcharge details (#2696)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Hrithikesh
2023-11-14 16:11:38 +05:30
committed by GitHub
parent 856c7af77e
commit cafea45982
20 changed files with 477 additions and 77 deletions

View File

@ -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

View File

@ -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<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,
})
})
}
#[allow(clippy::too_many_arguments)]
pub async fn payments_core<F, Res, Req, Op, FData, Ctx>(
state: AppState,
@ -920,7 +937,7 @@ pub async fn call_multiple_connectors_service<F, Op, Req, Ctx>(
_operation: &Op,
mut payment_data: PaymentData<F>,
customer: &Option<domain::Customer>,
session_surcharge_metadata: Option<SurchargeMetadata>,
session_surcharge_details: Option<api::SessionSurchargeDetails>,
) -> RouterResult<PaymentData<F>>
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(

View File

@ -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<F: Send + Clone, Ctx: PaymentMethodRetrieve>
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<F: Clone, Ctx: PaymentMethodRetrieve>
.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<F: Clone, Ctx: PaymentMethodRetrieve>
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<F: Send + Clone, Ctx: PaymentMethodRetrieve> ValidateRequest<F, api::Paymen
))
}
}
impl PaymentConfirm {
pub async fn validate_request_surcharge_details_with_session_surcharge_details(
state: &AppState,
payment_attempt: &storage::PaymentAttempt,
request: &api::PaymentsRequest,
) -> 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<SurchargeDetailsResponse> {
payment_request
.surcharge_details
.map(|surcharge_details| {
surcharge_details.get_surcharge_details_object(payment_attempt.amount)
}) // if not passed in confirm request, look inside payment_attempt
.or(payment_attempt
.surcharge_amount
.map(|surcharge_amount| SurchargeDetailsResponse {
surcharge: payment_methods::Surcharge::Fixed(surcharge_amount),
tax_on_surcharge: None,
surcharge_amount,
tax_on_surcharge_amount: payment_attempt.tax_amount.unwrap_or(0),
final_amount: payment_attempt.amount
+ surcharge_amount
+ payment_attempt.tax_amount.unwrap_or(0),
}))
}
}

View File

@ -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<F: Send + Clone, Ctx: PaymentMethodRetrieve>
// The operation merges mandate data from both request and payment_attempt
let setup_mandate: Option<MandateData> = 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<F: Send + Clone, Ctx: PaymentMethodRetrieve>
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<F: Clone, Ctx: PaymentMethodRetrieve>
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<F: Clone, Ctx: PaymentMethodRetrieve>
true => Some(authorized_amount),
false => None,
},
surcharge_amount,
tax_amount,
updated_by: storage_scheme.to_string(),
merchant_connector_id,
},

View File

@ -466,6 +466,8 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
} 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,

View File

@ -304,6 +304,10 @@ impl<F: Send + Clone, Ctx: PaymentMethodRetrieve>
// 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<F: Send + Clone, Ctx: PaymentMethodRetrieve>
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<F: Clone, Ctx: PaymentMethodRetrieve>
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<F: Clone, Ctx: PaymentMethodRetrieve>
business_sub_label,
amount_to_capture,
capture_method,
surcharge_amount,
tax_amount,
updated_by: storage_scheme.to_string(),
},
storage_scheme,

View File

@ -412,6 +412,8 @@ where
} else {
None
},
surcharge_amount: None,
tax_amount: None,
updated_by: storage_scheme.to_string(),
authentication_data,
encoded_data,

View File

@ -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<F>() -> RouterResult<String> {
.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::<SurchargeDetailsResponse>::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<euclid_enums::CardNetwork>,
payment_attempt_id: &str,
) -> CustomResult<SurchargeDetailsResponse, RedisError> {
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
}

View File

@ -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())
}
}

View File

@ -547,11 +547,31 @@ pub trait Capturable {
fn get_capture_amount(&self) -> Option<i64> {
Some(0)
}
fn get_surcharge_amount(&self) -> Option<i64> {
None
}
fn get_tax_on_surcharge_amount(&self) -> Option<i64> {
None
}
}
impl Capturable for PaymentsAuthorizeData {
fn get_capture_amount(&self) -> Option<i64> {
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<i64> {
self.surcharge_details
.as_ref()
.map(|surcharge_details| surcharge_details.surcharge_amount)
}
fn get_tax_on_surcharge_amount(&self) -> Option<i64> {
self.surcharge_details
.as_ref()
.map(|surcharge_details| surcharge_details.tax_on_surcharge_amount)
}
}

View File

@ -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<String>,
}
/// 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<SurchargeDetailsResponse> {
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<SessionConnectorData>),
StraightThrough(serde_json::Value),