mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
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 <narayan.bhat@juspay.in>
This commit is contained in:
@ -11,6 +11,7 @@ use crate::types::MinorUnit;
|
||||
pub type CustomResult<T, E> = error_stack::Result<T, E>;
|
||||
|
||||
/// 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.
|
||||
|
||||
@ -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<Self::Output, error_stack::Report<ParsingError>>;
|
||||
|
||||
/// 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<bool>,
|
||||
|
||||
/// 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<bool>,
|
||||
/// helps in converting back connector required amount type to core minor unit
|
||||
fn convert_back(
|
||||
&self,
|
||||
amount: Self::Output,
|
||||
currency: enums::Currency,
|
||||
) -> Result<MinorUnit, error_stack::Report<ParsingError>>;
|
||||
}
|
||||
|
||||
impl<DB: Backend> FromSql<Jsonb, DB> for ChargeRefunds
|
||||
where
|
||||
serde_json::Value: FromSql<Jsonb, DB>,
|
||||
{
|
||||
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
|
||||
let value = <serde_json::Value as FromSql<Jsonb, DB>>::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<Self::Output, error_stack::Report<ParsingError>> {
|
||||
amount.to_minor_unit_as_string()
|
||||
}
|
||||
|
||||
fn convert_back(
|
||||
&self,
|
||||
amount: Self::Output,
|
||||
_currency: enums::Currency,
|
||||
) -> Result<MinorUnit, error_stack::Report<ParsingError>> {
|
||||
amount.to_minor_unit_as_i64()
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<Jsonb, diesel::pg::Pg> for ChargeRefunds
|
||||
where
|
||||
serde_json::Value: ToSql<Jsonb, diesel::pg::Pg>,
|
||||
{
|
||||
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
|
||||
<serde_json::Value as ToSql<Jsonb, diesel::pg::Pg>>::to_sql(&value, &mut out.reborrow())
|
||||
impl AmountConvertor for StringMajorUnitForConnector {
|
||||
type Output = StringMajorUnit;
|
||||
fn convert(
|
||||
&self,
|
||||
amount: MinorUnit,
|
||||
currency: enums::Currency,
|
||||
) -> Result<Self::Output, error_stack::Report<ParsingError>> {
|
||||
amount.to_major_unit_as_string(currency)
|
||||
}
|
||||
|
||||
fn convert_back(
|
||||
&self,
|
||||
amount: StringMajorUnit,
|
||||
currency: enums::Currency,
|
||||
) -> Result<MinorUnit, error_stack::Report<ParsingError>> {
|
||||
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<Self::Output, error_stack::Report<ParsingError>> {
|
||||
amount.to_major_unit_as_f64(currency)
|
||||
}
|
||||
fn convert_back(
|
||||
&self,
|
||||
amount: FloatMajorUnit,
|
||||
currency: enums::Currency,
|
||||
) -> Result<MinorUnit, error_stack::Report<ParsingError>> {
|
||||
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<StringMajorUnit, error_stack::Report<ParsingError>> {
|
||||
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<FloatMajorUnit, error_stack::Report<ParsingError>> {
|
||||
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<StringMinorUnit, error_stack::Report<ParsingError>> {
|
||||
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<MinorUnit, error_stack::Report<ParsingError>> {
|
||||
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<MinorUnit, error_stack::Report<ParsingError>> {
|
||||
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<MinorUnit, error_stack::Report<ParsingError>> {
|
||||
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<bool>,
|
||||
|
||||
/// 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<bool>,
|
||||
}
|
||||
|
||||
impl<DB: Backend> FromSql<Jsonb, DB> for ChargeRefunds
|
||||
where
|
||||
serde_json::Value: FromSql<Jsonb, DB>,
|
||||
{
|
||||
fn from_sql(bytes: DB::RawValue<'_>) -> deserialize::Result<Self> {
|
||||
let value = <serde_json::Value as FromSql<Jsonb, DB>>::from_sql(bytes)?;
|
||||
Ok(serde_json::from_value(value)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql<Jsonb, diesel::pg::Pg> for ChargeRefunds
|
||||
where
|
||||
serde_json::Value: ToSql<Jsonb, diesel::pg::Pg>,
|
||||
{
|
||||
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
|
||||
<serde_json::Value as ToSql<Jsonb, diesel::pg::Pg>>::to_sql(&value, &mut out.reborrow())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user