Skip to content

Commit 3f0d927

Browse files
feat(core): add surcharge_details field to ResponsePaymentMethodTypes struct (#2435)
1 parent 51d9f8d commit 3f0d927

File tree

10 files changed

+417
-12
lines changed

10 files changed

+417
-12
lines changed

Cargo.lock

Lines changed: 101 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/api_models/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ strum = { version = "0.24.1", features = ["derive"] }
2525
time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] }
2626
url = { version = "2.4.0", features = ["serde"] }
2727
utoipa = { version = "3.3.0", features = ["preserve_order"] }
28+
thiserror = "1.0.40"
2829

2930
# First party crates
3031
cards = { version = "0.1.0", path = "../cards" }

crates/api_models/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ pub mod payments;
1616
#[cfg(feature = "payouts")]
1717
pub mod payouts;
1818
pub mod refunds;
19+
pub mod types;
1920
pub mod verifications;
2021
pub mod webhooks;

crates/api_models/src/payment_methods.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::collections::HashMap;
22

33
use cards::CardNumber;
4-
use common_utils::{crypto::OptionalEncryptableName, pii};
4+
use common_utils::{
5+
consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, pii,
6+
};
57
use serde::de;
68
use utoipa::ToSchema;
79

@@ -10,6 +12,7 @@ use crate::payouts;
1012
use crate::{
1113
admin, enums as api_enums,
1214
payments::{self, BankCodeResponse},
15+
types::Percentage,
1316
};
1417

1518
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)]
@@ -263,7 +266,7 @@ pub struct BankDebitTypes {
263266
pub eligible_connectors: Vec<String>,
264267
}
265268

266-
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, PartialEq, Eq)]
269+
#[derive(Debug, Clone, serde::Serialize, ToSchema, PartialEq)]
267270
pub struct ResponsePaymentMethodTypes {
268271
/// The payment method type enabled
269272
#[schema(example = "klarna")]
@@ -285,6 +288,39 @@ pub struct ResponsePaymentMethodTypes {
285288

286289
/// Required fields for the payment_method_type.
287290
pub required_fields: Option<HashMap<String, RequiredFieldInfo>>,
291+
292+
/// surcharge details for this payment method type if exists
293+
#[schema(example = r#"
294+
{
295+
"surcharge": {
296+
"type": "rate",
297+
"value": {
298+
"percentage": 2.5
299+
}
300+
},
301+
"tax_on_surcharge": {
302+
"percentage": 1.5
303+
}
304+
}
305+
"#)]
306+
pub surcharge_details: Option<SurchargeDetails>,
307+
}
308+
#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)]
309+
#[serde(rename_all = "snake_case")]
310+
pub struct SurchargeDetails {
311+
/// surcharge value
312+
surcharge: Surcharge,
313+
/// tax on surcharge value
314+
tax_on_surcharge: Option<Percentage<SURCHARGE_PERCENTAGE_PRECISION_LENGTH>>,
315+
}
316+
317+
#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)]
318+
#[serde(rename_all = "snake_case", tag = "type", content = "value")]
319+
pub enum Surcharge {
320+
/// Fixed Surcharge value
321+
Fixed(i64),
322+
/// Surcharge percentage
323+
Rate(Percentage<SURCHARGE_PERCENTAGE_PRECISION_LENGTH>),
288324
}
289325

290326
/// Required fields info used while listing the payment_method_data
@@ -303,7 +339,7 @@ pub struct RequiredFieldInfo {
303339
pub value: Option<String>,
304340
}
305341

306-
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)]
342+
#[derive(Debug, Clone, serde::Serialize, ToSchema)]
307343
pub struct ResponsePaymentMethodsEnabled {
308344
/// The payment method enabled
309345
#[schema(value_type = PaymentMethod)]
@@ -539,6 +575,10 @@ pub struct PaymentMethodListResponse {
539575
#[schema(value_type = Option<String>)]
540576
pub merchant_name: OptionalEncryptableName,
541577

578+
/// flag to indicate if surcharge and tax breakup screen should be shown or not
579+
#[schema(value_type = bool)]
580+
pub show_surcharge_breakup_screen: bool,
581+
542582
#[schema(value_type = Option<PaymentType>)]
543583
pub payment_type: Option<api_enums::PaymentType>,
544584
}

crates/api_models/src/types.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use common_utils::errors::{ApiModelsError, CustomResult};
2+
use error_stack::ResultExt;
3+
use serde::{de::Visitor, Deserialize, Deserializer};
4+
use utoipa::ToSchema;
5+
6+
#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)]
7+
pub struct Percentage<const PRECISION: u8> {
8+
// this value will range from 0 to 100, decimal length defined by precision macro
9+
/// Percentage value ranging between 0 and 100
10+
#[schema(example = 2.5)]
11+
percentage: f32,
12+
}
13+
14+
fn get_invalid_percentage_error_message(precision: u8) -> String {
15+
format!(
16+
"value should be between 0 to 100 and precise to only upto {} decimal digits",
17+
precision
18+
)
19+
}
20+
21+
impl<const PRECISION: u8> Percentage<PRECISION> {
22+
pub fn from_float(value: f32) -> CustomResult<Self, ApiModelsError> {
23+
if Self::is_valid_value(value) {
24+
Ok(Self { percentage: value })
25+
} else {
26+
Err(ApiModelsError::InvalidPercentageValue.into())
27+
.attach_printable(get_invalid_percentage_error_message(PRECISION))
28+
}
29+
}
30+
pub fn get_percentage(&self) -> f32 {
31+
self.percentage
32+
}
33+
fn is_valid_value(value: f32) -> bool {
34+
Self::is_valid_range(value) && Self::is_valid_precision_length(value)
35+
}
36+
fn is_valid_range(value: f32) -> bool {
37+
(0.0..=100.0).contains(&value)
38+
}
39+
fn is_valid_precision_length(value: f32) -> bool {
40+
let multiplier = f32::powf(10.0, PRECISION.into());
41+
let multiplied_value = value * multiplier;
42+
// if fraction part is 0, then the percentage value is valid
43+
multiplied_value.fract() == 0.0
44+
}
45+
}
46+
47+
// custom serde deserialization function
48+
struct PercentageVisitor<const PRECISION: u8> {}
49+
impl<'de, const PRECISION: u8> Visitor<'de> for PercentageVisitor<PRECISION> {
50+
type Value = Percentage<PRECISION>;
51+
52+
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53+
formatter.write_str("Percentage object")
54+
}
55+
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
56+
where
57+
A: serde::de::MapAccess<'de>,
58+
{
59+
let mut percentage_value = None;
60+
while let Some(key) = map.next_key::<String>()? {
61+
if key.eq("percentage") {
62+
if percentage_value.is_some() {
63+
return Err(serde::de::Error::duplicate_field("percentage"));
64+
}
65+
percentage_value = Some(map.next_value::<f32>()?);
66+
} else {
67+
// Ignore unknown fields
68+
let _: serde::de::IgnoredAny = map.next_value()?;
69+
}
70+
}
71+
if let Some(value) = percentage_value {
72+
let str_value = value.to_string();
73+
Ok(Percentage::from_float(value).map_err(|_| {
74+
serde::de::Error::invalid_value(
75+
serde::de::Unexpected::Other(&format!("percentage value `{}`", str_value)),
76+
&&*get_invalid_percentage_error_message(PRECISION),
77+
)
78+
})?)
79+
} else {
80+
Err(serde::de::Error::missing_field("percentage"))
81+
}
82+
}
83+
}
84+
85+
impl<'de, const PRECISION: u8> Deserialize<'de> for Percentage<PRECISION> {
86+
fn deserialize<D>(data: D) -> Result<Self, D::Error>
87+
where
88+
D: Deserializer<'de>,
89+
{
90+
data.deserialize_map(PercentageVisitor::<PRECISION> {})
91+
}
92+
}

0 commit comments

Comments
 (0)