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:
Sahkal Poddar
2024-05-30 19:21:33 +05:30
committed by GitHub
parent f7e99e1eda
commit 08eefdba4a
30 changed files with 577 additions and 150 deletions

View File

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

View File

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