diff --git a/Cargo.lock b/Cargo.lock index 0275312161ad..cca3b6f58d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,6 +471,18 @@ dependencies = [ "serde_json", ] +[[package]] +name = "async-bb8-diesel" +version = "0.1.0" +source = "git+https://github.com/juspay/async-bb8-diesel?rev=9a71d142726dbc33f41c1fd935ddaa79841c7be5#9a71d142726dbc33f41c1fd935ddaa79841c7be5" +dependencies = [ + "async-trait", + "bb8", + "diesel", + "thiserror", + "tokio", +] + [[package]] name = "async-bb8-diesel" version = "0.1.0" @@ -723,6 +735,39 @@ dependencies = [ "tracing", ] +[[package]] +name = "aws-sdk-s3" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "392b9811ca489747ac84349790e49deaa1f16631949e7dd4156000251c260eae" +dependencies = [ + "aws-credential-types", + "aws-endpoint", + "aws-http", + "aws-sig-auth", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-client", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-http-tower", + "aws-smithy-json", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "http", + "http-body", + "once_cell", + "percent-encoding", + "regex", + "tokio-stream", + "tower", + "tracing", + "url", +] + [[package]] name = "aws-sdk-s3" version = "0.28.0" @@ -1808,9 +1853,9 @@ dependencies = [ name = "diesel_models" version = "0.1.0" dependencies = [ - "async-bb8-diesel", + "async-bb8-diesel 0.1.0 (git+https://github.com/oxidecomputer/async-bb8-diesel?rev=be3d9bce50051d8c0e0c06078e8066cc27db3001)", "aws-config", - "aws-sdk-s3", + "aws-sdk-s3 0.28.0", "common_enums", "common_utils", "diesel", @@ -1895,7 +1940,7 @@ checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" name = "drainer" version = "0.1.0" dependencies = [ - "async-bb8-diesel", + "async-bb8-diesel 0.1.0 (git+https://github.com/oxidecomputer/async-bb8-diesel?rev=be3d9bce50051d8c0e0c06078e8066cc27db3001)", "bb8", "clap", "common_utils", @@ -1943,7 +1988,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", - "humantime", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime 2.1.0", + "is-terminal", "log", "regex", "termcolor", @@ -2531,6 +2589,12 @@ dependencies = [ "quick-error", ] +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.27" @@ -2715,6 +2779,18 @@ version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "itertools" version = "0.10.5" @@ -3604,7 +3680,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" dependencies = [ - "env_logger", + "env_logger 0.7.1", "log", ] @@ -4058,11 +4134,11 @@ dependencies = [ "actix-rt", "actix-web", "api_models", - "async-bb8-diesel", + "async-bb8-diesel 0.1.0 (git+https://github.com/oxidecomputer/async-bb8-diesel?rev=be3d9bce50051d8c0e0c06078e8066cc27db3001)", "async-trait", "awc", "aws-config", - "aws-sdk-s3", + "aws-sdk-s3 0.28.0", "base64 0.21.2", "bb8", "blake3", @@ -4352,19 +4428,36 @@ dependencies = [ name = "scheduler" version = "0.1.0" dependencies = [ + "actix-multipart", + "actix-rt", + "actix-web", + "api_models", + "async-bb8-diesel 0.1.0 (git+https://github.com/juspay/async-bb8-diesel?rev=9a71d142726dbc33f41c1fd935ddaa79841c7be5)", "async-trait", + "aws-config", + "aws-sdk-s3 0.25.1", + "cards", + "clap", "common_utils", + "diesel", "diesel_models", + "dyn-clone", + "env_logger 0.10.0", "error-stack", "external_services", + "frunk", + "frunk_core", "futures", + "infer 0.13.0", "masking", "once_cell", "rand 0.8.5", "redis_interface", + "router_derive", "router_env", "serde", "serde_json", + "signal-hook", "signal-hook-tokio", "storage_impl", "strum 0.24.1", @@ -4731,7 +4824,7 @@ version = "0.1.0" dependencies = [ "actix-web", "api_models", - "async-bb8-diesel", + "async-bb8-diesel 0.1.0 (git+https://github.com/oxidecomputer/async-bb8-diesel?rev=be3d9bce50051d8c0e0c06078e8066cc27db3001)", "async-trait", "bb8", "bytes", diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index f1c7e8396da9..662129204c29 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -25,6 +25,7 @@ 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"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } +thiserror = "1.0.40" # First party crates cards = { version = "0.1.0", path = "../cards" } diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 29ad9be051b6..caff4e1780c7 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -16,5 +16,6 @@ pub mod payments; #[cfg(feature = "payouts")] pub mod payouts; pub mod refunds; +pub mod types; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 2e9cf4093300..067a3a341402 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -1,7 +1,9 @@ use std::collections::HashMap; use cards::CardNumber; -use common_utils::{crypto::OptionalEncryptableName, pii}; +use common_utils::{ + consts::SURCHARGE_PERCENTAGE_PRECISION_LENGTH, crypto::OptionalEncryptableName, pii, +}; use serde::de; use utoipa::ToSchema; @@ -10,6 +12,7 @@ use crate::payouts; use crate::{ admin, enums as api_enums, payments::{self, BankCodeResponse}, + types::Percentage, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -263,7 +266,7 @@ pub struct BankDebitTypes { pub eligible_connectors: Vec, } -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema, PartialEq, Eq)] +#[derive(Debug, Clone, serde::Serialize, ToSchema, PartialEq)] pub struct ResponsePaymentMethodTypes { /// The payment method type enabled #[schema(example = "klarna")] @@ -285,6 +288,39 @@ pub struct ResponsePaymentMethodTypes { /// Required fields for the payment_method_type. 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, +} +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct SurchargeDetails { + /// surcharge value + surcharge: Surcharge, + /// tax on surcharge value + tax_on_surcharge: Option>, +} + +#[derive(Clone, Debug, PartialEq, serde::Serialize, ToSchema)] +#[serde(rename_all = "snake_case", tag = "type", content = "value")] +pub enum Surcharge { + /// Fixed Surcharge value + Fixed(i64), + /// Surcharge percentage + Rate(Percentage), } /// Required fields info used while listing the payment_method_data @@ -303,7 +339,7 @@ pub struct RequiredFieldInfo { pub value: Option, } -#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] +#[derive(Debug, Clone, serde::Serialize, ToSchema)] pub struct ResponsePaymentMethodsEnabled { /// The payment method enabled #[schema(value_type = PaymentMethod)] @@ -539,6 +575,10 @@ pub struct PaymentMethodListResponse { #[schema(value_type = Option)] pub merchant_name: OptionalEncryptableName, + /// flag to indicate if surcharge and tax breakup screen should be shown or not + #[schema(value_type = bool)] + pub show_surcharge_breakup_screen: bool, + #[schema(value_type = Option)] pub payment_type: Option, } diff --git a/crates/api_models/src/types.rs b/crates/api_models/src/types.rs new file mode 100644 index 000000000000..bd594ba62766 --- /dev/null +++ b/crates/api_models/src/types.rs @@ -0,0 +1,92 @@ +use common_utils::errors::{ApiModelsError, CustomResult}; +use error_stack::ResultExt; +use serde::{de::Visitor, Deserialize, Deserializer}; +use utoipa::ToSchema; + +#[derive(Clone, Default, Debug, PartialEq, serde::Serialize, ToSchema)] +pub struct Percentage { + // this value will range from 0 to 100, decimal length defined by precision macro + /// Percentage value ranging between 0 and 100 + #[schema(example = 2.5)] + percentage: f32, +} + +fn get_invalid_percentage_error_message(precision: u8) -> String { + format!( + "value should be between 0 to 100 and precise to only upto {} decimal digits", + precision + ) +} + +impl Percentage { + pub fn from_float(value: f32) -> CustomResult { + if Self::is_valid_value(value) { + Ok(Self { percentage: value }) + } else { + Err(ApiModelsError::InvalidPercentageValue.into()) + .attach_printable(get_invalid_percentage_error_message(PRECISION)) + } + } + pub fn get_percentage(&self) -> f32 { + self.percentage + } + fn is_valid_value(value: f32) -> bool { + Self::is_valid_range(value) && Self::is_valid_precision_length(value) + } + fn is_valid_range(value: f32) -> bool { + (0.0..=100.0).contains(&value) + } + fn is_valid_precision_length(value: f32) -> bool { + let multiplier = f32::powf(10.0, PRECISION.into()); + let multiplied_value = value * multiplier; + // if fraction part is 0, then the percentage value is valid + multiplied_value.fract() == 0.0 + } +} + +// custom serde deserialization function +struct PercentageVisitor {} +impl<'de, const PRECISION: u8> Visitor<'de> for PercentageVisitor { + type Value = Percentage; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("Percentage object") + } + fn visit_map(self, mut map: A) -> Result + where + A: serde::de::MapAccess<'de>, + { + let mut percentage_value = None; + while let Some(key) = map.next_key::()? { + if key.eq("percentage") { + if percentage_value.is_some() { + return Err(serde::de::Error::duplicate_field("percentage")); + } + percentage_value = Some(map.next_value::()?); + } else { + // Ignore unknown fields + let _: serde::de::IgnoredAny = map.next_value()?; + } + } + if let Some(value) = percentage_value { + let str_value = value.to_string(); + Ok(Percentage::from_float(value).map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Other(&format!("percentage value `{}`", str_value)), + &&*get_invalid_percentage_error_message(PRECISION), + ) + })?) + } else { + Err(serde::de::Error::missing_field("percentage")) + } + } +} + +impl<'de, const PRECISION: u8> Deserialize<'de> for Percentage { + fn deserialize(data: D) -> Result + where + D: Deserializer<'de>, + { + data.deserialize_map(PercentageVisitor:: {}) + } +} diff --git a/crates/api_models/tests/percentage.rs b/crates/api_models/tests/percentage.rs new file mode 100644 index 000000000000..3e137a2592d1 --- /dev/null +++ b/crates/api_models/tests/percentage.rs @@ -0,0 +1,156 @@ +#![allow(clippy::panic_in_result_fn)] +use api_models::types::Percentage; +use common_utils::errors::ApiModelsError; +const PRECISION_2: u8 = 2; +const PRECISION_0: u8 = 0; + +#[test] +fn invalid_range_more_than_100() -> Result<(), Box> { + let percentage = Percentage::::from_float(100.01); + assert!(percentage.is_err()); + if let Err(err) = percentage { + assert_eq!( + *err.current_context(), + ApiModelsError::InvalidPercentageValue + ) + } + Ok(()) +} +#[test] +fn invalid_range_less_than_0() -> Result<(), Box> { + let percentage = Percentage::::from_float(-0.01); + assert!(percentage.is_err()); + if let Err(err) = percentage { + assert_eq!( + *err.current_context(), + ApiModelsError::InvalidPercentageValue + ) + } + Ok(()) +} +#[test] +fn valid_range() -> Result<(), Box> { + let percentage = Percentage::::from_float(2.22); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 2.22) + } + + let percentage = Percentage::::from_float(0.0); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 0.0) + } + + let percentage = Percentage::::from_float(100.0); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 100.0) + } + Ok(()) +} +#[test] +fn valid_precision() -> Result<(), Box> { + let percentage = Percentage::::from_float(2.2); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 2.2) + } + + let percentage = Percentage::::from_float(2.20000); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 2.2) + } + + let percentage = Percentage::::from_float(2.0); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 2.0) + } + + Ok(()) +} + +#[test] +fn invalid_precision() -> Result<(), Box> { + let percentage = Percentage::::from_float(2.221); + assert!(percentage.is_err()); + if let Err(err) = percentage { + assert_eq!( + *err.current_context(), + ApiModelsError::InvalidPercentageValue + ) + } + Ok(()) +} + +#[test] +fn deserialization_test_ok() -> Result<(), Box> { + let json_string = r#" + { + "percentage" : 12.4 + } + "#; + let percentage = serde_json::from_str::>(json_string); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 12.4) + } + + let json_string = r#" + { + "percentage" : 12.0 + } + "#; + let percentage = serde_json::from_str::>(json_string); + assert!(percentage.is_ok()); + if let Ok(percentage) = percentage { + assert_eq!(percentage.get_percentage(), 12.0) + } + Ok(()) +} + +#[test] +fn deserialization_test_err() -> Result<(), Box> { + // invalid percentage precision + let json_string = r#" + { + "percentage" : 12.4 + } + "#; + let percentage = serde_json::from_str::>(json_string); + assert!(percentage.is_err()); + if let Err(err) = percentage { + assert_eq!(err.to_string(), "invalid value: percentage value `12.4`, expected value should be between 0 to 100 and precise to only upto 0 decimal digits at line 4 column 9".to_string()) + } + + // invalid percentage value + let json_string = r#" + { + "percentage" : 123.42 + } + "#; + let percentage = serde_json::from_str::>(json_string); + assert!(percentage.is_err()); + if let Err(err) = percentage { + assert_eq!(err.to_string(), "invalid value: percentage value `123.42`, expected value should be between 0 to 100 and precise to only upto 2 decimal digits at line 4 column 9".to_string()) + } + + // missing percentage field + let json_string = r#" + { + "percent": 22.0 + } + "#; + let percentage = serde_json::from_str::>(json_string); + assert!(percentage.is_err()); + if let Err(err) = percentage { + dbg!(err.to_string()); + assert_eq!( + err.to_string(), + "missing field `percentage` at line 4 column 9".to_string() + ) + } + Ok(()) +} diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 5797e7b487b2..ef688f728b09 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -23,3 +23,6 @@ pub static FRM_CONFIGS_EG: &str = r#" pub const PAYMENTS_LIST_MAX_LIMIT_V1: u32 = 100; /// Maximum limit for payments list post api with filters pub const PAYMENTS_LIST_MAX_LIMIT_V2: u32 = 20; + +/// surcharge percentage maximum precision length +pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; diff --git a/crates/common_utils/src/errors.rs b/crates/common_utils/src/errors.rs index cc29cd2971f3..6541633a56ee 100644 --- a/crates/common_utils/src/errors.rs +++ b/crates/common_utils/src/errors.rs @@ -76,6 +76,14 @@ pub enum QrCodeError { FailedToCreateQrCode, } +/// Api Models construction error +#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)] +pub enum ApiModelsError { + /// Percentage Value provided was invalid + #[error("Invalid Percentage value")] + InvalidPercentageValue, +} + /// Allows [error_stack::Report] to change between error contexts /// using the dependent [ErrorSwitch] trait to define relations & mappings between traits pub trait ReportSwitchExt { diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index bcb52164471d..4177a8d3627b 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1156,6 +1156,7 @@ pub async fn list_payment_methods( .get(key.0) .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), + surcharge_details: None, }) } @@ -1188,6 +1189,7 @@ pub async fn list_payment_methods( .get(key.0) .and_then(|inner_hm| inner_hm.get(payment_method_types_hm.0)) .cloned(), + surcharge_details: None, }) } @@ -1216,6 +1218,7 @@ pub async fn list_payment_methods( .get(&api_enums::PaymentMethod::BankRedirect) .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), + surcharge_details: None, } }) } @@ -1247,6 +1250,7 @@ pub async fn list_payment_methods( .get(&api_enums::PaymentMethod::BankDebit) .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), + surcharge_details: None, } }) } @@ -1278,6 +1282,7 @@ pub async fn list_payment_methods( .get(&api_enums::PaymentMethod::BankTransfer) .and_then(|inner_hm| inner_hm.get(key.0)) .cloned(), + surcharge_details: None, } }) } @@ -1320,6 +1325,7 @@ pub async fn list_payment_methods( } }, ), + show_surcharge_breakup_screen: false, }, )) } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index d442127b0697..1ac3eb2a8585 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -8048,7 +8048,8 @@ "type": "object", "required": [ "payment_methods", - "mandate_payment" + "mandate_payment", + "show_surcharge_breakup_screen" ], "properties": { "redirect_url": { @@ -8081,6 +8082,10 @@ "type": "string", "nullable": true }, + "show_surcharge_breakup_screen": { + "type": "boolean", + "description": "flag to indicate if surcharge and tax breakup screen should be shown or not" + }, "payment_type": { "allOf": [ {