From 08eefdba4a7f428ffe7f0dac9799c46b82c49864 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Thu, 30 May 2024 19:21:33 +0530 Subject: [PATCH] feat(router): Added amount conversion function in core for connector module (#4710) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Co-authored-by: Narayan Bhat --- Cargo.lock | 2 + crates/api_models/src/refunds.rs | 9 +- crates/common_utils/Cargo.toml | 2 + crates/common_utils/src/errors.rs | 16 + crates/common_utils/src/types.rs | 408 ++++++++++++++++-- crates/diesel_models/src/refund.rs | 13 +- .../src/router_request_types.rs | 29 +- .../src/compatibility/stripe/refunds/types.rs | 4 +- crates/router/src/connector/nmi.rs | 64 +-- .../router/src/connector/nmi/transformers.rs | 32 +- crates/router/src/connector/stripe.rs | 2 +- crates/router/src/connector/utils.rs | 22 +- crates/router/src/core/errors.rs | 2 + crates/router/src/core/errors/utils.rs | 9 +- .../router/src/core/payments/transformers.rs | 4 + crates/router/src/core/refunds.rs | 21 +- crates/router/src/core/refunds/validator.rs | 2 +- crates/router/src/core/utils.rs | 10 +- crates/router/src/db/refund.rs | 14 +- crates/router/src/services/kafka/refund.rs | 5 +- crates/router/src/types.rs | 3 +- crates/router/src/types/api.rs | 12 +- crates/router/src/types/api/authentication.rs | 2 +- crates/router/src/types/api/fraud_check.rs | 2 +- .../router/src/types/api/verify_connector.rs | 4 +- crates/router/src/utils/ext_traits.rs | 11 +- crates/router/src/utils/user/sample_data.rs | 10 +- crates/router/tests/connectors/nmi.rs | 2 +- crates/router/tests/connectors/utils.rs | 7 +- openapi/openapi_spec.json | 4 +- 30 files changed, 577 insertions(+), 150 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b116982bb2..61f4e8f421 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1965,7 +1965,9 @@ dependencies = [ "reqwest", "ring 0.17.8", "router_env", + "rust_decimal", "rustc-hash", + "rusty-money", "semver 1.0.22", "serde", "serde_json", diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 881cc3912e..c97344ff48 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use common_utils::pii; -pub use common_utils::types::ChargeRefunds; +pub use common_utils::types::{ChargeRefunds, MinorUnit}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; use utoipa::ToSchema; @@ -36,8 +36,8 @@ pub struct RefundRequest { pub merchant_id: Option, /// Total amount for which the refund is to be initiated. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc., If not provided, this will default to the full payment amount - #[schema(minimum = 100, example = 6540)] - pub amount: Option, + #[schema(value_type = Option , minimum = 100, example = 6540)] + pub amount: Option, /// Reason for the refund. Often useful for displaying to users and your customer support executive. In case the payment went through Stripe, this field needs to be passed with one of these enums: `duplicate`, `fraudulent`, or `requested_by_customer` #[schema(max_length = 255, example = "Customer returned the product")] @@ -115,7 +115,8 @@ pub struct RefundResponse { /// The payment id against which refund is initiated pub payment_id: String, /// The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc - pub amount: i64, + #[schema(value_type = i64 , minimum = 100, example = 6540)] + pub amount: MinorUnit, /// The three-letter ISO currency code pub currency: String, /// The status for refund diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index ff64d6517c..def64e35db 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -43,6 +43,8 @@ utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order uuid = { version = "1.8.0", features = ["v7"] } # First party crates +rust_decimal = "1.35" +rusty-money = { git = "https://github.com/varunsrin/rusty_money", rev = "bbc0150742a0fff905225ff11ee09388e9babdcc", features = ["iso", "crypto"] } common_enums = { version = "0.1.0", path = "../common_enums" } masking = { version = "0.1.0", path = "../masking" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"], optional = true } diff --git a/crates/common_utils/src/errors.rs b/crates/common_utils/src/errors.rs index 967580f0ae..e4c59ba2a3 100644 --- a/crates/common_utils/src/errors.rs +++ b/crates/common_utils/src/errors.rs @@ -11,6 +11,7 @@ use crate::types::MinorUnit; pub type CustomResult = error_stack::Result; /// Parsing Errors +#[allow(missing_docs)] // Only to prevent warnings about struct fields not being documented #[derive(Debug, thiserror::Error)] pub enum ParsingError { ///Failed to parse enum @@ -34,6 +35,21 @@ pub enum ParsingError { /// Failed to parse phone number #[error("Failed to parse phone number")] PhoneNumberParsingError, + /// Failed to parse Float value for converting to decimal points + #[error("Failed to parse Float value for converting to decimal points")] + FloatToDecimalConversionFailure, + /// Failed to parse Decimal value for i64 value conversion + #[error("Failed to parse Decimal value for i64 value conversion")] + DecimalToI64ConversionFailure, + /// Failed to parse string value for f64 value conversion + #[error("Failed to parse string value for f64 value conversion")] + StringToFloatConversionFailure, + /// Failed to parse i64 value for f64 value conversion + #[error("Failed to parse i64 value for f64 value conversion")] + I64ToDecimalConversionFailure, + /// Failed to parse String value to Decimal value conversion because `error` + #[error("Failed to parse String value to Decimal value conversion because {error}")] + StringToDecimalConversionFailure { error: String }, } /// Validation errors. diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index d9a7aef3f5..7ab78ce325 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -6,6 +6,7 @@ use std::{ str::FromStr, }; +use common_enums::enums; use diesel::{ backend::Backend, deserialize, @@ -16,6 +17,10 @@ use diesel::{ AsExpression, FromSqlRow, Queryable, }; use error_stack::{report, ResultExt}; +use rust_decimal::{ + prelude::{FromPrimitive, ToPrimitive}, + Decimal, +}; use semver::Version; use serde::{de::Visitor, Deserialize, Deserializer, Serialize}; use utoipa::ToSchema; @@ -226,48 +231,92 @@ where } } -#[derive( - Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, ToSchema, -)] -#[diesel(sql_type = Jsonb)] -/// Charge object for refunds -pub struct ChargeRefunds { - /// Identifier for charge created for the payment - pub charge_id: String, +/// Amount convertor trait for connector +pub trait AmountConvertor: Send { + /// Output type for the connector + type Output; + /// helps in conversion of connector required amount type + fn convert( + &self, + amount: MinorUnit, + currency: enums::Currency, + ) -> Result>; - /// Toggle for reverting the application fee that was collected for the payment. - /// If set to false, the funds are pulled from the destination account. - pub revert_platform_fee: Option, - - /// Toggle for reverting the transfer that was made during the charge. - /// If set to false, the funds are pulled from the main platform's account. - pub revert_transfer: Option, + /// helps in converting back connector required amount type to core minor unit + fn convert_back( + &self, + amount: Self::Output, + currency: enums::Currency, + ) -> Result>; } -impl FromSql for ChargeRefunds -where - serde_json::Value: FromSql, -{ - fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { - let value = >::from_sql(bytes)?; - Ok(serde_json::from_value(value)?) +/// Connector required amount type +#[derive(Default, Debug, Clone, Copy, PartialEq)] +pub struct StringMinorUnitForConnector; + +impl AmountConvertor for StringMinorUnitForConnector { + type Output = StringMinorUnit; + fn convert( + &self, + amount: MinorUnit, + _currency: enums::Currency, + ) -> Result> { + amount.to_minor_unit_as_string() + } + + fn convert_back( + &self, + amount: Self::Output, + _currency: enums::Currency, + ) -> Result> { + amount.to_minor_unit_as_i64() } } -impl ToSql for ChargeRefunds -where - serde_json::Value: ToSql, -{ - fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result { - let value = serde_json::to_value(self)?; +/// Connector required amount type +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)] +pub struct StringMajorUnitForConnector; - // the function `reborrow` only works in case of `Pg` backend. But, in case of other backends - // please refer to the diesel migration blog: - // https://github.com/Diesel-rs/Diesel/blob/master/guide_drafts/migration_guide.md#changed-tosql-implementations - >::to_sql(&value, &mut out.reborrow()) +impl AmountConvertor for StringMajorUnitForConnector { + type Output = StringMajorUnit; + fn convert( + &self, + amount: MinorUnit, + currency: enums::Currency, + ) -> Result> { + amount.to_major_unit_as_string(currency) + } + + fn convert_back( + &self, + amount: StringMajorUnit, + currency: enums::Currency, + ) -> Result> { + amount.to_minor_unit_as_i64(currency) } } +/// Connector required amount type +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)] +pub struct FloatMajorUnitForConnector; + +impl AmountConvertor for FloatMajorUnitForConnector { + type Output = FloatMajorUnit; + fn convert( + &self, + amount: MinorUnit, + currency: enums::Currency, + ) -> Result> { + amount.to_major_unit_as_f64(currency) + } + fn convert_back( + &self, + amount: FloatMajorUnit, + currency: enums::Currency, + ) -> Result> { + amount.to_minor_unit_as_i64(currency) + } +} /// This Unit struct represents MinorUnit in which core amount works #[derive( Default, @@ -287,9 +336,8 @@ where pub struct MinorUnit(i64); impl MinorUnit { - /// gets amount as i64 value + /// gets amount as i64 value will be removed in future pub fn get_amount_as_i64(&self) -> i64 { - // will be removed in future self.0 } @@ -297,6 +345,50 @@ impl MinorUnit { pub fn new(value: i64) -> Self { Self(value) } + + /// Convert the amount to its major denomination based on Currency and return String + /// Paypal Connector accepts Zero and Two decimal currency but not three decimal and it should be updated as required for 3 decimal currencies. + /// Paypal Ref - https://developer.paypal.com/docs/reports/reference/paypal-supported-currencies/ + fn to_major_unit_as_string( + self, + currency: enums::Currency, + ) -> Result> { + let amount_f64 = self.to_major_unit_as_f64(currency)?; + let amount_string = if currency.is_zero_decimal_currency() { + amount_f64.0.to_string() + } else if currency.is_three_decimal_currency() { + format!("{:.3}", amount_f64.0) + } else { + format!("{:.2}", amount_f64.0) + }; + Ok(StringMajorUnit::new(amount_string)) + } + + /// Convert the amount to its major denomination based on Currency and return f64 + fn to_major_unit_as_f64( + self, + currency: enums::Currency, + ) -> Result> { + let amount_decimal = + Decimal::from_i64(self.0).ok_or(ParsingError::I64ToDecimalConversionFailure)?; + + let amount = if currency.is_zero_decimal_currency() { + amount_decimal + } else if currency.is_three_decimal_currency() { + amount_decimal / Decimal::from(1000) + } else { + amount_decimal / Decimal::from(100) + }; + let amount_f64 = amount + .to_f64() + .ok_or(ParsingError::FloatToDecimalConversionFailure)?; + Ok(FloatMajorUnit::new(amount_f64)) + } + + ///Convert minor unit to string minor unit + fn to_minor_unit_as_string(self) -> Result> { + Ok(StringMinorUnit::new(self.0.to_string())) + } } impl Display for MinorUnit { @@ -351,3 +443,251 @@ impl Sub for MinorUnit { Self(self.0 - a2.0) } } + +/// Connector specific types to send + +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq)] +pub struct StringMinorUnit(String); + +impl StringMinorUnit { + /// forms a new minor unit in string from amount + fn new(value: String) -> Self { + Self(value) + } + + /// converts to minor unit i64 from minor unit string value + fn to_minor_unit_as_i64(&self) -> Result> { + let amount_string = &self.0; + let amount_decimal = Decimal::from_str(amount_string).map_err(|e| { + ParsingError::StringToDecimalConversionFailure { + error: e.to_string(), + } + })?; + let amount_i64 = amount_decimal + .to_i64() + .ok_or(ParsingError::DecimalToI64ConversionFailure)?; + Ok(MinorUnit::new(amount_i64)) + } +} + +/// Connector specific types to send +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, Copy, PartialEq)] +pub struct FloatMajorUnit(f64); + +impl FloatMajorUnit { + /// forms a new major unit from amount + fn new(value: f64) -> Self { + Self(value) + } + + /// forms a new major unit with zero amount + pub fn zero() -> Self { + Self(0.0) + } + + /// converts to minor unit as i64 from FloatMajorUnit + fn to_minor_unit_as_i64( + self, + currency: enums::Currency, + ) -> Result> { + let amount_decimal = + Decimal::from_f64(self.0).ok_or(ParsingError::FloatToDecimalConversionFailure)?; + + let amount = if currency.is_zero_decimal_currency() { + amount_decimal + } else if currency.is_three_decimal_currency() { + amount_decimal * Decimal::from(1000) + } else { + amount_decimal * Decimal::from(100) + }; + + let amount_i64 = amount + .to_i64() + .ok_or(ParsingError::DecimalToI64ConversionFailure)?; + Ok(MinorUnit::new(amount_i64)) + } +} + +/// Connector specific types to send +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, PartialEq, Eq)] +pub struct StringMajorUnit(String); + +impl StringMajorUnit { + /// forms a new major unit from amount + fn new(value: String) -> Self { + Self(value) + } + + /// Converts to minor unit as i64 from StringMajorUnit + fn to_minor_unit_as_i64( + &self, + currency: enums::Currency, + ) -> Result> { + let amount_decimal = Decimal::from_str(&self.0).map_err(|e| { + ParsingError::StringToDecimalConversionFailure { + error: e.to_string(), + } + })?; + + let amount = if currency.is_zero_decimal_currency() { + amount_decimal + } else if currency.is_three_decimal_currency() { + amount_decimal * Decimal::from(1000) + } else { + amount_decimal * Decimal::from(100) + }; + let amount_i64 = amount + .to_i64() + .ok_or(ParsingError::DecimalToI64ConversionFailure)?; + Ok(MinorUnit::new(amount_i64)) + } +} + +#[cfg(test)] +mod amount_conversion_tests { + #![allow(clippy::unwrap_used)] + use super::*; + const TWO_DECIMAL_CURRENCY: enums::Currency = enums::Currency::USD; + const THREE_DECIMAL_CURRENCY: enums::Currency = enums::Currency::BHD; + const ZERO_DECIMAL_CURRENCY: enums::Currency = enums::Currency::JPY; + #[test] + fn amount_conversion_to_float_major_unit() { + let request_amount = MinorUnit::new(999999999); + let required_conversion = FloatMajorUnitForConnector; + + // Two decimal currency conversions + let converted_amount = required_conversion + .convert(request_amount, TWO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_amount.0, 9999999.99); + let converted_back_amount = required_conversion + .convert_back(converted_amount, TWO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_back_amount, request_amount); + + // Three decimal currency conversions + let converted_amount = required_conversion + .convert(request_amount, THREE_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_amount.0, 999999.999); + let converted_back_amount = required_conversion + .convert_back(converted_amount, THREE_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_back_amount, request_amount); + + // Zero decimal currency conversions + let converted_amount = required_conversion + .convert(request_amount, ZERO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_amount.0, 999999999.0); + + let converted_back_amount = required_conversion + .convert_back(converted_amount, ZERO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_back_amount, request_amount); + } + + #[test] + fn amount_conversion_to_string_major_unit() { + let request_amount = MinorUnit::new(999999999); + let required_conversion = StringMajorUnitForConnector; + + // Two decimal currency conversions + let converted_amount_two_decimal_currency = required_conversion + .convert(request_amount, TWO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!( + converted_amount_two_decimal_currency.0, + "9999999.99".to_string() + ); + let converted_back_amount = required_conversion + .convert_back(converted_amount_two_decimal_currency, TWO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_back_amount, request_amount); + + // Three decimal currency conversions + let converted_amount_three_decimal_currency = required_conversion + .convert(request_amount, THREE_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!( + converted_amount_three_decimal_currency.0, + "999999.999".to_string() + ); + let converted_back_amount = required_conversion + .convert_back( + converted_amount_three_decimal_currency, + THREE_DECIMAL_CURRENCY, + ) + .unwrap(); + assert_eq!(converted_back_amount, request_amount); + + // Zero decimal currency conversions + let converted_amount = required_conversion + .convert(request_amount, ZERO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_amount.0, "999999999".to_string()); + + let converted_back_amount = required_conversion + .convert_back(converted_amount, ZERO_DECIMAL_CURRENCY) + .unwrap(); + assert_eq!(converted_back_amount, request_amount); + } + + #[test] + fn amount_conversion_to_string_minor_unit() { + let request_amount = MinorUnit::new(999999999); + let currency = TWO_DECIMAL_CURRENCY; + let required_conversion = StringMinorUnitForConnector; + let converted_amount = required_conversion + .convert(request_amount, currency) + .unwrap(); + assert_eq!(converted_amount.0, "999999999".to_string()); + let converted_back_amount = required_conversion + .convert_back(converted_amount, currency) + .unwrap(); + assert_eq!(converted_back_amount, request_amount); + } +} + +// Charges structs +#[derive( + Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +/// Charge object for refunds +pub struct ChargeRefunds { + /// Identifier for charge created for the payment + pub charge_id: String, + + /// Toggle for reverting the application fee that was collected for the payment. + /// If set to false, the funds are pulled from the destination account. + pub revert_platform_fee: Option, + + /// Toggle for reverting the transfer that was made during the charge. + /// If set to false, the funds are pulled from the main platform's account. + pub revert_transfer: Option, +} + +impl FromSql for ChargeRefunds +where + serde_json::Value: FromSql, +{ + fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(serde_json::from_value(value)?) + } +} + +impl ToSql for ChargeRefunds +where + serde_json::Value: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, diesel::pg::Pg>) -> diesel::serialize::Result { + let value = serde_json::to_value(self)?; + + // the function `reborrow` only works in case of `Pg` backend. But, in case of other backends + // please refer to the diesel migration blog: + // https://github.com/Diesel-rs/Diesel/blob/master/guide_drafts/migration_guide.md#changed-tosql-implementations + >::to_sql(&value, &mut out.reborrow()) + } +} diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index aac282992a..7d5b20c3d0 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -1,4 +1,7 @@ -use common_utils::{pii, types::ChargeRefunds}; +use common_utils::{ + pii, + types::{ChargeRefunds, MinorUnit}, +}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; @@ -20,9 +23,9 @@ pub struct Refund { pub connector_refund_id: Option, pub external_reference_id: Option, pub refund_type: storage_enums::RefundType, - pub total_amount: i64, + pub total_amount: MinorUnit, pub currency: storage_enums::Currency, - pub refund_amount: i64, + pub refund_amount: MinorUnit, pub refund_status: storage_enums::RefundStatus, pub sent_to_gateway: bool, pub refund_error_message: Option, @@ -65,9 +68,9 @@ pub struct RefundNew { pub connector: String, pub connector_refund_id: Option, pub refund_type: storage_enums::RefundType, - pub total_amount: i64, + pub total_amount: MinorUnit, pub currency: storage_enums::Currency, - pub refund_amount: i64, + pub refund_amount: MinorUnit, pub refund_status: storage_enums::RefundStatus, pub sent_to_gateway: bool, pub metadata: Option, diff --git a/crates/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index 5a4838f9b7..30827e61c7 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -1,7 +1,9 @@ pub mod authentication; pub mod fraud_check; use api_models::payments::RequestSurchargeDetails; -use common_utils::{consts, errors, ext_traits::OptionExt, id_type, pii, types as common_types}; +use common_utils::{ + consts, errors, ext_traits::OptionExt, id_type, pii, types as common_types, types::MinorUnit, +}; use diesel_models::enums as storage_enums; use error_stack::ResultExt; use masking::Secret; @@ -57,6 +59,9 @@ pub struct PaymentsAuthorizeData { pub metadata: Option, pub authentication_data: Option, pub charges: Option, + + // New amount for amount frame work + pub minor_amount: MinorUnit, } #[derive(Debug, serde::Deserialize, Clone)] @@ -77,6 +82,10 @@ pub struct PaymentsCaptureData { pub browser_info: Option, pub metadata: Option, // This metadata is used to store the metadata shared during the payment intent request. + + // New amount for amount frame work + pub minor_payment_amount: MinorUnit, + pub minor_amount_to_capture: MinorUnit, } #[derive(Debug, Clone, Default)] @@ -296,6 +305,9 @@ pub struct CompleteAuthorizeData { pub connector_meta: Option, pub complete_authorize_url: Option, pub metadata: Option, + + // New amount for amount frame work + pub minor_amount: MinorUnit, } #[derive(Debug, Clone)] @@ -387,18 +399,18 @@ impl ResponseId { #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct SurchargeDetails { /// original_amount - pub original_amount: common_utils::types::MinorUnit, + pub original_amount: MinorUnit, /// surcharge value pub surcharge: common_utils::types::Surcharge, /// tax on surcharge value pub tax_on_surcharge: Option>, /// surcharge amount for this payment - pub surcharge_amount: common_utils::types::MinorUnit, + pub surcharge_amount: MinorUnit, /// tax on surcharge amount for this payment - pub tax_on_surcharge_amount: common_utils::types::MinorUnit, + pub tax_on_surcharge_amount: MinorUnit, /// sum of original amount, - pub final_amount: common_utils::types::MinorUnit, + pub final_amount: MinorUnit, } impl SurchargeDetails { @@ -410,7 +422,7 @@ impl SurchargeDetails { && request_surcharge_details.tax_amount.unwrap_or_default() == self.tax_on_surcharge_amount } - pub fn get_total_surcharge_amount(&self) -> common_utils::types::MinorUnit { + pub fn get_total_surcharge_amount(&self) -> MinorUnit { self.surcharge_amount + self.tax_on_surcharge_amount } } @@ -460,6 +472,7 @@ pub struct RefundsData { pub currency: storage_enums::Currency, /// Amount for the payment against which this refund is issued pub payment_amount: i64, + pub reason: Option, pub webhook_url: Option, /// Amount to be refunded @@ -469,6 +482,10 @@ pub struct RefundsData { pub browser_info: Option, /// Charges associated with the payment pub charges: Option, + + // New amount for amount frame work + pub minor_payment_amount: MinorUnit, + pub minor_refund_amount: MinorUnit, } #[derive(Debug, serde::Deserialize, Clone)] diff --git a/crates/router/src/compatibility/stripe/refunds/types.rs b/crates/router/src/compatibility/stripe/refunds/types.rs index 10a0183614..d00da0d858 100644 --- a/crates/router/src/compatibility/stripe/refunds/types.rs +++ b/crates/router/src/compatibility/stripe/refunds/types.rs @@ -46,7 +46,7 @@ impl From for refunds::RefundRequest { fn from(req: StripeCreateRefundRequest) -> Self { Self { refund_id: req.refund_id, - amount: req.amount, + amount: req.amount.map(common_utils::types::MinorUnit::new), payment_id: req.payment_intent, reason: req.reason, refund_type: Some(refunds::RefundType::Instant), @@ -82,7 +82,7 @@ impl From for StripeRefundResponse { fn from(res: refunds::RefundResponse) -> Self { Self { id: res.refund_id, - amount: res.amount, + amount: res.amount.get_amount_as_i64(), currency: res.currency.to_ascii_lowercase(), payment_intent: res.payment_id, status: res.status.into(), diff --git a/crates/router/src/connector/nmi.rs b/crates/router/src/connector/nmi.rs index 744eeb4a97..2c0712c540 100644 --- a/crates/router/src/connector/nmi.rs +++ b/crates/router/src/connector/nmi.rs @@ -1,8 +1,11 @@ pub mod transformers; -use std::fmt::Debug; - -use common_utils::{crypto, ext_traits::ByteSliceExt, request::RequestContent}; +use common_utils::{ + crypto, + ext_traits::ByteSliceExt, + request::RequestContent, + types::{AmountConvertor, FloatMajorUnit, FloatMajorUnitForConnector}, +}; use diesel_models::enums; use error_stack::{report, ResultExt}; use regex::Regex; @@ -25,8 +28,18 @@ use crate::{ }, }; -#[derive(Clone, Debug)] -pub struct Nmi; +#[derive(Clone)] +pub struct Nmi { + amount_converter: &'static (dyn AmountConvertor + Sync), +} + +impl Nmi { + pub const fn new() -> &'static Self { + &Self { + amount_converter: &FloatMajorUnitForConnector, + } + } +} impl api::Payment for Nmi {} impl api::PaymentSession for Nmi {} @@ -325,12 +338,12 @@ impl ConnectorIntegration CustomResult { - let connector_router_data = nmi::NmiRouterData::try_from(( - &self.get_currency_unit(), + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_amount, req.request.currency, - req.request.amount, - req, - ))?; + )?; + let connector_router_data = nmi::NmiRouterData::from((amount, req)); let connector_req = nmi::NmiPaymentsRequest::try_from(&connector_router_data)?; Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } @@ -413,12 +426,12 @@ impl req: &types::PaymentsCompleteAuthorizeRouterData, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_router_data = nmi::NmiRouterData::try_from(( - &self.get_currency_unit(), + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_amount, req.request.currency, - req.request.amount, - req, - ))?; + )?; + let connector_router_data = nmi::NmiRouterData::from((amount, req)); let connector_req = nmi::NmiCompleteRequest::try_from(&connector_router_data)?; Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } @@ -565,12 +578,12 @@ impl ConnectorIntegration CustomResult { - let connector_router_data = nmi::NmiRouterData::try_from(( - &self.get_currency_unit(), + let amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_amount_to_capture, req.request.currency, - req.request.amount_to_capture, - req, - ))?; + )?; + let connector_router_data = nmi::NmiRouterData::from((amount, req)); let connector_req = nmi::NmiCaptureRequest::try_from(&connector_router_data)?; Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } @@ -713,12 +726,13 @@ impl ConnectorIntegration, _connectors: &settings::Connectors, ) -> CustomResult { - let connector_router_data = nmi::NmiRouterData::try_from(( - &self.get_currency_unit(), + let refund_amount = connector_utils::convert_amount( + self.amount_converter, + req.request.minor_refund_amount, req.request.currency, - req.request.refund_amount, - req, - ))?; + )?; + + let connector_router_data = nmi::NmiRouterData::from((refund_amount, req)); let connector_req = nmi::NmiRefundRequest::try_from(&connector_router_data)?; Ok(RequestContent::FormUrlEncoded(Box::new(connector_req))) } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index 4483600202..c18feefa2f 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -5,6 +5,7 @@ use common_utils::{ errors::CustomResult, ext_traits::XmlExt, pii::{self, Email}, + types::FloatMajorUnit, }; use error_stack::{report, Report, ResultExt}; use masking::{ExposeInterface, PeekInterface, Secret}; @@ -57,25 +58,16 @@ impl TryFrom<&ConnectorAuthType> for NmiAuthType { #[derive(Debug, Serialize)] pub struct NmiRouterData { - pub amount: f64, + pub amount: FloatMajorUnit, pub router_data: T, } -impl TryFrom<(&api::CurrencyUnit, enums::Currency, i64, T)> for NmiRouterData { - type Error = Report; - - fn try_from( - (_currency_unit, currency, amount, router_data): ( - &api::CurrencyUnit, - enums::Currency, - i64, - T, - ), - ) -> Result { - Ok(Self { - amount: utils::to_currency_base_unit_asf64(amount, currency)?, +impl From<(FloatMajorUnit, T)> for NmiRouterData { + fn from((amount, router_data): (FloatMajorUnit, T)) -> Self { + Self { + amount, router_data, - }) + } } } @@ -251,7 +243,7 @@ impl #[derive(Debug, Serialize)] pub struct NmiCompleteRequest { - amount: f64, + amount: FloatMajorUnit, #[serde(rename = "type")] transaction_type: TransactionType, security_key: Secret, @@ -422,7 +414,7 @@ impl ForeignFrom<(NmiCompleteResponse, u16)> for types::ErrorResponse { pub struct NmiPaymentsRequest { #[serde(rename = "type")] transaction_type: TransactionType, - amount: f64, + amount: FloatMajorUnit, security_key: Secret, currency: enums::Currency, #[serde(flatten)] @@ -675,7 +667,7 @@ impl TryFrom<&types::SetupMandateRouterData> for NmiPaymentsRequest { Ok(Self { transaction_type: TransactionType::Validate, security_key: auth_type.api_key, - amount: 0.0, + amount: FloatMajorUnit::zero(), currency: item.request.currency, payment_method, merchant_defined_field: None, @@ -707,7 +699,7 @@ pub struct NmiCaptureRequest { pub transaction_type: TransactionType, pub security_key: Secret, pub transactionid: String, - pub amount: Option, + pub amount: Option, } impl TryFrom<&NmiRouterData<&types::PaymentsCaptureRouterData>> for NmiCaptureRequest { @@ -1062,7 +1054,7 @@ pub struct NmiRefundRequest { security_key: Secret, transactionid: String, orderid: String, - amount: f64, + amount: FloatMajorUnit, } impl TryFrom<&NmiRouterData<&types::RefundsRouterData>> for NmiRefundRequest { diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index febb039768..171cde7bc9 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -1,6 +1,6 @@ pub mod transformers; -use std::{collections::HashMap, fmt::Debug, ops::Deref}; +use std::{collections::HashMap, ops::Deref}; use common_utils::request::RequestContent; use diesel_models::enums; diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index a9c78cb4bf..ba0ef16680 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -13,7 +13,7 @@ use common_utils::{ ext_traits::StringExt, id_type, pii::{self, Email, IpAddress}, - types::MinorUnit, + types::{AmountConvertor, MinorUnit}, }; use diesel_models::enums; use error_stack::{report, ResultExt}; @@ -2618,3 +2618,23 @@ impl From for PaymentMethodDataType { } } } + +pub fn convert_amount( + amount_convertor: &dyn AmountConvertor, + amount: MinorUnit, + currency: enums::Currency, +) -> Result> { + amount_convertor + .convert(amount, currency) + .change_context(errors::ConnectorError::AmountConversionFailed) +} + +pub fn convert_back( + amount_convertor: &dyn AmountConvertor, + amount: T, + currency: enums::Currency, +) -> Result> { + amount_convertor + .convert_back(amount, currency) + .change_context(errors::ConnectorError::AmountConversionFailed) +} diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 3a8cab8c51..0e9e402279 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -219,6 +219,8 @@ pub enum ConnectorError { }, #[error("Invalid Configuration")] InvalidConnectorConfig { config: &'static str }, + #[error("Failed to convert amount to required type")] + AmountConversionFailed, } #[derive(Debug, thiserror::Error)] diff --git a/crates/router/src/core/errors/utils.rs b/crates/router/src/core/errors/utils.rs index 1e2f5a2c30..8ec95514f8 100644 --- a/crates/router/src/core/errors/utils.rs +++ b/crates/router/src/core/errors/utils.rs @@ -215,7 +215,8 @@ impl ConnectorErrorExt for error_stack::Result | errors::ConnectorError::InSufficientBalanceInPaymentMethod | errors::ConnectorError::RequestTimeoutReceived | errors::ConnectorError::CurrencyNotSupported { .. } - | errors::ConnectorError::InvalidConnectorConfig { .. } => { + | errors::ConnectorError::InvalidConnectorConfig { .. } + | errors::ConnectorError::AmountConversionFailed { .. } => { err.change_context(errors::ApiErrorResponse::RefundFailed { data: None }) } }) @@ -309,7 +310,8 @@ impl ConnectorErrorExt for error_stack::Result errors::ConnectorError::MissingPaymentMethodType | errors::ConnectorError::InSufficientBalanceInPaymentMethod | errors::ConnectorError::RequestTimeoutReceived | - errors::ConnectorError::ProcessingStepFailed(None) => errors::ApiErrorResponse::InternalServerError + errors::ConnectorError::ProcessingStepFailed(None)| + errors::ConnectorError::AmountConversionFailed => errors::ApiErrorResponse::InternalServerError }; err.change_context(error) }) @@ -399,7 +401,8 @@ impl ConnectorErrorExt for error_stack::Result | errors::ConnectorError::InSufficientBalanceInPaymentMethod | errors::ConnectorError::RequestTimeoutReceived | errors::ConnectorError::CurrencyNotSupported { .. } - | errors::ConnectorError::ProcessingStepFailed(None) => { + | errors::ConnectorError::ProcessingStepFailed(None) + | errors::ConnectorError::AmountConversionFailed { .. } => { logger::error!(%error,"Setup Mandate flow failed"); errors::ApiErrorResponse::PaymentAuthorizationFailed { data: None } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index d8a7092455..a5859ea6dc 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1260,6 +1260,7 @@ impl TryFrom> for types::PaymentsAuthoriz statement_descriptor: payment_data.payment_intent.statement_descriptor_name, capture_method: payment_data.payment_attempt.capture_method, amount: amount.get_amount_as_i64(), + minor_amount: amount, currency: payment_data.currency, browser_info, email: payment_data.email, @@ -1420,12 +1421,14 @@ impl TryFrom> for types::PaymentsCaptureD let amount = MinorUnit::from(payment_data.amount); Ok(Self { amount_to_capture: amount_to_capture.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_amount_to_capture: amount_to_capture, currency: payment_data.currency, connector_transaction_id: connector .connector .connector_transaction_id(payment_data.payment_attempt.clone())? .ok_or(errors::ApiErrorResponse::ResourceIdNotFound)?, payment_amount: amount.get_amount_as_i64(), // This should be removed once we start moving to connector module + minor_payment_amount: amount, connector_meta: payment_data.payment_attempt.connector_metadata, multiple_capture_data: match payment_data.multiple_capture_data { Some(multiple_capture_data) => Some(MultipleCaptureRequestData { @@ -1702,6 +1705,7 @@ impl TryFrom> for types::CompleteAuthoriz statement_descriptor_suffix: payment_data.payment_intent.statement_descriptor_suffix, capture_method: payment_data.payment_attempt.capture_method, amount: amount.get_amount_as_i64(), // need to change once we move to connector module + minor_amount: amount, currency: payment_data.currency, browser_info, email: payment_data.email, diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index b9861ea1f2..9ab1ec0eb8 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -5,7 +5,10 @@ use std::collections::HashMap; #[cfg(feature = "olap")] use api_models::admin::MerchantConnectorInfo; -use common_utils::ext_traits::{AsyncExt, ValueExt}; +use common_utils::{ + ext_traits::{AsyncExt, ValueExt}, + types::MinorUnit, +}; use error_stack::{report, ResultExt}; use masking::PeekInterface; use router_env::{instrument, tracing}; @@ -75,14 +78,12 @@ pub async fn refund_create_core( // Amount is not passed in request refer from payment intent. amount = req .amount - .or(payment_intent - .amount_captured - .map(|capture_amount| capture_amount.get_amount_as_i64())) + .or(payment_intent.amount_captured) .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable("amount captured is none in a successful payment")?; //[#299]: Can we change the flow based on some workflow idea - utils::when(amount <= 0, || { + utils::when(amount <= MinorUnit::new(0), || { Err(report!(errors::ApiErrorResponse::InvalidDataFormat { field_name: "amount".to_string(), expected_format: "positive integer".to_string() @@ -178,7 +179,7 @@ pub async fn trigger_refund_to_gateway( &routed_through, merchant_account, key_store, - (payment_attempt.amount.get_amount_as_i64(), currency), + (payment_attempt.amount, currency), payment_intent, payment_attempt, refund, @@ -458,7 +459,7 @@ pub async fn sync_refund_with_gateway( &connector_id, merchant_account, key_store, - (payment_attempt.amount.get_amount_as_i64(), currency), + (payment_attempt.amount, currency), payment_intent, payment_attempt, refund, @@ -588,7 +589,7 @@ pub async fn validate_and_create_refund( key_store: &domain::MerchantKeyStore, payment_attempt: &storage::PaymentAttempt, payment_intent: &storage::PaymentIntent, - refund_amount: i64, + refund_amount: MinorUnit, req: refunds::RefundRequest, creds_identifier: Option, ) -> RouterResult { @@ -680,7 +681,7 @@ pub async fn validate_and_create_refund( validator::validate_refund_amount( total_amount_captured.get_amount_as_i64(), &all_refunds, - refund_amount, + refund_amount.get_amount_as_i64(), ) .change_context(errors::ApiErrorResponse::RefundAmountExceedsPaymentAmount)?; @@ -705,7 +706,7 @@ pub async fn validate_and_create_refund( .set_connector_transaction_id(connecter_transaction_id.to_string()) .set_connector(connector) .set_refund_type(req.refund_type.unwrap_or_default().foreign_into()) - .set_total_amount(payment_attempt.amount.get_amount_as_i64()) + .set_total_amount(payment_attempt.amount) .set_refund_amount(refund_amount) .set_currency(currency) .set_created_at(Some(common_utils::date_time::now())) diff --git a/crates/router/src/core/refunds/validator.rs b/crates/router/src/core/refunds/validator.rs index 58be964d70..1167fde567 100644 --- a/crates/router/src/core/refunds/validator.rs +++ b/crates/router/src/core/refunds/validator.rs @@ -54,7 +54,7 @@ pub fn validate_refund_amount( if refund.refund_status != enums::RefundStatus::Failure && refund.refund_status != enums::RefundStatus::TransactionFailure { - Some(refund.refund_amount) + Some(refund.refund_amount.get_amount_as_i64()) } else { None } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index ca464cc627..e585724e96 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -6,7 +6,7 @@ use api_models::payouts::PayoutVendorAccountDetails; use common_enums::{IntentStatus, RequestIncrementalAuthorization}; #[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, types::MinorUnit}; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{payment_address::PaymentAddress, router_data::ErrorResponse}; #[cfg(feature = "payouts")] @@ -218,7 +218,7 @@ pub async fn construct_refund_router_data<'a, F>( connector_id: &str, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - money: (i64, enums::Currency), + money: (MinorUnit, enums::Currency), payment_intent: &'a storage::PaymentIntent, payment_attempt: &storage::PaymentAttempt, refund: &'a storage::Refund, @@ -323,9 +323,11 @@ pub async fn construct_refund_router_data<'a, F>( request: types::RefundsData { refund_id: refund.refund_id.clone(), connector_transaction_id: refund.connector_transaction_id.clone(), - refund_amount: refund.refund_amount, + refund_amount: refund.refund_amount.get_amount_as_i64(), + minor_refund_amount: refund.refund_amount, currency, - payment_amount, + payment_amount: payment_amount.get_amount_as_i64(), + minor_payment_amount: payment_amount, webhook_url, connector_metadata: payment_attempt.connector_metadata.clone(), reason: refund.refund_reason.clone(), diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 1b1bfed387..c5edd863ca 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -1,6 +1,8 @@ #[cfg(feature = "olap")] use std::collections::HashSet; +#[cfg(feature = "olap")] +use common_utils::types::MinorUnit; use diesel_models::{errors::DatabaseError, refund::RefundUpdateInternal}; use error_stack::ResultExt; @@ -1024,8 +1026,10 @@ impl RefundInterface for MockDb { .amount_filter .as_ref() .map_or(true, |amount| { - refund.refund_amount >= amount.start_amount.unwrap_or(i64::MIN) - && refund.refund_amount <= amount.end_amount.unwrap_or(i64::MAX) + refund.refund_amount + >= MinorUnit::new(amount.start_amount.unwrap_or(i64::MIN)) + && refund.refund_amount + <= MinorUnit::new(amount.end_amount.unwrap_or(i64::MAX)) }) }) .filter(|refund| { @@ -1173,8 +1177,10 @@ impl RefundInterface for MockDb { .amount_filter .as_ref() .map_or(true, |amount| { - refund.refund_amount >= amount.start_amount.unwrap_or(i64::MIN) - && refund.refund_amount <= amount.end_amount.unwrap_or(i64::MAX) + refund.refund_amount + >= MinorUnit::new(amount.start_amount.unwrap_or(i64::MIN)) + && refund.refund_amount + <= MinorUnit::new(amount.end_amount.unwrap_or(i64::MAX)) }) }) .filter(|refund| { diff --git a/crates/router/src/services/kafka/refund.rs b/crates/router/src/services/kafka/refund.rs index 4bfe2cd31e..d5ef71bf65 100644 --- a/crates/router/src/services/kafka/refund.rs +++ b/crates/router/src/services/kafka/refund.rs @@ -1,3 +1,4 @@ +use common_utils::types::MinorUnit; use diesel_models::{enums as storage_enums, refund::Refund}; use time::OffsetDateTime; @@ -12,9 +13,9 @@ pub struct KafkaRefund<'a> { pub connector_refund_id: Option<&'a String>, pub external_reference_id: Option<&'a String>, pub refund_type: &'a storage_enums::RefundType, - pub total_amount: &'a i64, + pub total_amount: &'a MinorUnit, pub currency: &'a storage_enums::Currency, - pub refund_amount: &'a i64, + pub refund_amount: &'a MinorUnit, pub refund_status: &'a storage_enums::RefundStatus, pub sent_to_gateway: &'a bool, pub refund_error_message: Option<&'a String>, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index fc1b59f431..e17ec79163 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -21,7 +21,7 @@ use std::marker::PhantomData; pub use api_models::{enums::Connector, mandates}; #[cfg(feature = "payouts")] pub use api_models::{enums::PayoutConnectors, payouts as payout_types}; -pub use common_utils::request::RequestContent; +pub use common_utils::{pii, pii::Email, request::RequestContent, types::MinorUnit}; #[cfg(feature = "payouts")] pub use hyperswitch_domain_models::router_request_types::PayoutsData; #[cfg(feature = "payouts")] @@ -761,6 +761,7 @@ impl ForeignFrom<&SetupMandateRouterData> for PaymentsAuthorizeData { email: data.request.email.clone(), customer_name: data.request.customer_name.clone(), amount: 0, + minor_amount: MinorUnit::new(0), statement_descriptor: None, capture_method: None, webhook_url: None, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 9ad0542243..7aa6aad93c 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -170,7 +170,6 @@ pub trait Connector: Send + Refund + Payment - + Debug + ConnectorRedirectResponse + IncomingWebhook + ConnectorAccessToken @@ -192,7 +191,6 @@ pub struct Pe; impl< T: Refund + Payment - + Debug + ConnectorRedirectResponse + Send + IncomingWebhook @@ -224,7 +222,7 @@ pub enum GetToken { /// Routing algorithm will output merchant connector identifier instead of connector name /// In order to support backwards compatibility for older routing algorithms and merchant accounts /// the support for connector name is retained -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct ConnectorData { pub connector: BoxedConnector, pub connector_name: types::Connector, @@ -284,7 +282,7 @@ impl ConnectorData { let connector_name = api_enums::Connector::from_str(name) .change_context(errors::ConnectorError::InvalidConnectorName) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| format!("unable to parse connector name {connector:?}"))?; + .attach_printable_lazy(|| format!("unable to parse connector name {name}"))?; Ok(Self { connector, connector_name, @@ -304,9 +302,7 @@ impl ConnectorData { let payout_connector_name = api_enums::PayoutConnectors::from_str(name) .change_context(errors::ConnectorError::InvalidConnectorName) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!("unable to parse payout connector name {connector:?}") - })?; + .attach_printable_lazy(|| format!("unable to parse payout connector name {name}"))?; let connector_name = api_enums::Connector::from(payout_connector_name); Ok(Self { connector, @@ -364,7 +360,7 @@ impl ConnectorData { enums::Connector::Klarna => Ok(Box::new(&connector::Klarna)), // enums::Connector::Mifinity => Ok(Box::new(&connector::Mifinity)), Added as template code for future usage enums::Connector::Mollie => Ok(Box::new(&connector::Mollie)), - enums::Connector::Nmi => Ok(Box::new(&connector::Nmi)), + enums::Connector::Nmi => Ok(Box::new(connector::Nmi::new())), enums::Connector::Noon => Ok(Box::new(&connector::Noon)), enums::Connector::Nuvei => Ok(Box::new(&connector::Nuvei)), enums::Connector::Opennode => Ok(Box::new(&connector::Opennode)), diff --git a/crates/router/src/types/api/authentication.rs b/crates/router/src/types/api/authentication.rs index 9837059dc0..85d4d3718c 100644 --- a/crates/router/src/types/api/authentication.rs +++ b/crates/router/src/types/api/authentication.rs @@ -119,7 +119,7 @@ pub trait ExternalAuthentication: { } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct AuthenticationConnectorData { pub connector: BoxedConnector, pub connector_name: enums::AuthenticationConnectors, diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs index 45c79930e8..c413126ee0 100644 --- a/crates/router/src/types/api/fraud_check.rs +++ b/crates/router/src/types/api/fraud_check.rs @@ -55,7 +55,7 @@ pub trait FraudCheckRecordReturn: { } -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct FraudCheckConnectorData { pub connector: BoxedConnector, pub connector_name: enums::FrmConnectors, diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index cf7ba2382a..60749f6478 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -1,6 +1,5 @@ pub mod paypal; pub mod stripe; - use error_stack::ResultExt; use crate::{ @@ -12,7 +11,7 @@ use crate::{ AppState, }; -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct VerifyConnectorData { pub connector: &'static (dyn api::Connector + Sync), pub connector_auth: types::ConnectorAuthType, @@ -26,6 +25,7 @@ impl VerifyConnectorData { email: None, customer_name: None, amount: 1000, + minor_amount: common_utils::types::MinorUnit::new(1000), confirm: true, currency: storage_enums::Currency::USD, metadata: None, diff --git a/crates/router/src/utils/ext_traits.rs b/crates/router/src/utils/ext_traits.rs index 7b08dc296c..33fdb4c9b2 100644 --- a/crates/router/src/utils/ext_traits.rs +++ b/crates/router/src/utils/ext_traits.rs @@ -26,15 +26,12 @@ pub trait OptionExt { fn update_value(&mut self, value: Option); } -impl OptionExt for Option -where - T: std::fmt::Debug, -{ +impl OptionExt for Option { fn check_value_present(&self, field_name: &'static str) -> RouterResult<()> { when(self.is_none(), || { Err( Report::new(ApiErrorResponse::MissingRequiredField { field_name }) - .attach_printable(format!("Missing required field {field_name} in {self:?}")), + .attach_printable(format!("Missing required field {field_name}")), ) }) } @@ -46,7 +43,7 @@ where Some(v) => Ok(v), None => Err( Report::new(ApiErrorResponse::MissingRequiredField { field_name }) - .attach_printable(format!("Missing required field {field_name} in {self:?}")), + .attach_printable(format!("Missing required field {field_name}")), ), } } @@ -63,7 +60,7 @@ where E::from_str(value.as_ref()) .change_context(errors::ParsingError::UnknownError) - .attach_printable_lazy(|| format!("Invalid {{ {enum_name}: {value:?} }} ")) + .attach_printable_lazy(|| format!("Invalid {{ {enum_name} }} ")) } fn parse_value(self, type_name: &'static str) -> CustomResult diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 256b115dbb..ea690af35a 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -2,7 +2,7 @@ use api_models::{ enums::Connector::{DummyConnector4, DummyConnector7}, user::sample_data::SampleDataRequest, }; -use common_utils::id_type; +use common_utils::{id_type, types::MinorUnit}; use diesel_models::{user::sample_data::PaymentAttemptBatchNew, RefundNew}; use error_stack::ResultExt; use hyperswitch_domain_models::payments::payment_intent::PaymentIntentNew; @@ -179,7 +179,7 @@ pub async fn generate_sample_data( true => common_enums::IntentStatus::Failed, _ => common_enums::IntentStatus::Succeeded, }, - amount: common_utils::types::MinorUnit::new(amount * 100), + amount: MinorUnit::new(amount * 100), currency: Some( *currency_vec .get((num - 1) % currency_vec_len) @@ -197,7 +197,7 @@ pub async fn generate_sample_data( ), attempt_count: 1, customer_id: Some(dashboard_customer_id.clone()), - amount_captured: Some(common_utils::types::MinorUnit::new(amount * 100)), + amount_captured: Some(MinorUnit::new(amount * 100)), profile_id: Some(profile_id.clone()), return_url: Default::default(), metadata: Default::default(), @@ -291,8 +291,8 @@ pub async fn generate_sample_data( currency: *currency_vec .get((num - 1) % currency_vec_len) .unwrap_or(&common_enums::Currency::USD), - total_amount: amount * 100, - refund_amount: amount * 100, + total_amount: MinorUnit::new(amount * 100), + refund_amount: MinorUnit::new(amount * 100), refund_status: common_enums::RefundStatus::Success, sent_to_gateway: true, refund_type: diesel_models::enums::RefundType::InstantRefund, diff --git a/crates/router/tests/connectors/nmi.rs b/crates/router/tests/connectors/nmi.rs index 1d719e052a..d5f1ebd4fa 100644 --- a/crates/router/tests/connectors/nmi.rs +++ b/crates/router/tests/connectors/nmi.rs @@ -13,7 +13,7 @@ impl utils::Connector for NmiTest { fn get_data(&self) -> types::api::ConnectorData { use router::connector::Nmi; types::api::ConnectorData { - connector: Box::new(&Nmi), + connector: Box::new(Nmi::new()), connector_name: types::Connector::Nmi, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index ae93ca326b..b7be24f2a1 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -11,7 +11,7 @@ use router::{ core::{errors::ConnectorError, payments}, db::StorageImpl, routes, services, - types::{self, storage::enums, AccessToken, PaymentAddress, RouterData}, + types::{self, storage::enums, AccessToken, MinorUnit, PaymentAddress, RouterData}, }; use test_utils::connector_auth::ConnectorAuthType; use tokio::sync::oneshot; @@ -414,11 +414,13 @@ pub trait ConnectorActions: Connector { let request = self.generate_data( payment_data.unwrap_or_else(|| types::RefundsData { payment_amount: 1000, + minor_payment_amount: MinorUnit::new(1000), currency: enums::Currency::USD, refund_id: uuid::Uuid::new_v4().to_string(), connector_transaction_id: "".to_string(), webhook_url: None, refund_amount: 100, + minor_refund_amount: MinorUnit::new(100), connector_metadata: None, reason: None, connector_refund_id: Some(refund_id), @@ -915,6 +917,7 @@ impl Default for PaymentAuthorizeType { let data = types::PaymentsAuthorizeData { payment_method_data: types::domain::PaymentMethodData::Card(CCardType::default().0), amount: 100, + minor_amount: MinorUnit::new(100), currency: enums::Currency::USD, confirm: true, statement_descriptor_suffix: None, @@ -1012,10 +1015,12 @@ impl Default for PaymentRefundType { fn default() -> Self { let data = types::RefundsData { payment_amount: 100, + minor_payment_amount: MinorUnit::new(100), currency: enums::Currency::USD, refund_id: uuid::Uuid::new_v4().to_string(), connector_transaction_id: String::new(), refund_amount: 100, + minor_refund_amount: MinorUnit::new(100), webhook_url: None, connector_metadata: None, reason: Some("Customer returned product".to_string()), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 9defe94f94..fe3c934f4a 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -17400,7 +17400,9 @@ "amount": { "type": "integer", "format": "int64", - "description": "The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc" + "description": "The refund amount, which should be less than or equal to the total payment amount. Amount for the payment in lowest denomination of the currency. (i.e) in cents for USD denomination, in paisa for INR denomination etc", + "example": 6540, + "minimum": 100 }, "currency": { "type": "string",