Skip to content

Commit

Permalink
feat(currency_conversion): add currency conversion feature (#2948)
Browse files Browse the repository at this point in the history
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 28, 2023
1 parent 0e66b1b commit c0116db
Show file tree
Hide file tree
Showing 27 changed files with 1,501 additions and 10 deletions.
259 changes: 255 additions & 4 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,16 @@ default_hash_ttl = 900 # Default TTL for hashes entries, in seconds
use_legacy_version = false # Resp protocol for fred crate (set this to true if using RESPv2 or redis version < 6)
stream_read_count = 1 # Default number of entries to read from stream if not provided in stream read options

# This section provides configs for currency conversion api
[forex_api]
call_delay = 21600 # Api calls are made after every 6 hrs
local_fetch_retry_count = 5 # Fetch from Local cache has retry count as 5
local_fetch_retry_delay = 1000 # Retry delay for checking write condition
api_timeout = 20000 # Api timeouts once it crosses 2000 ms
api_key = "YOUR API KEY HERE" # Api key for making request to foreign exchange Api
fallback_api_key = "YOUR API KEY" # Api key for the fallback service
redis_lock_timeout = 26000 # Redis remains write locked for 26000 ms once the acquire_redis_lock is called

# Logging configuration. Logging can be either to file or console or both.

# Logging configuration for file logging
Expand Down
9 changes: 9 additions & 0 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ host_rs = ""
mock_locker = true
basilisk_host = ""

[forex_api]
call_delay = 21600
local_fetch_retry_count = 5
local_fetch_retry_delay = 1000
api_timeout = 20000
api_key = "YOUR API KEY HERE"
fallback_api_key = "YOUR API KEY HERE"
redis_lock_timeout = 26000

[jwekey]
locker_key_identifier1 = ""
locker_key_identifier2 = ""
Expand Down
9 changes: 9 additions & 0 deletions config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ port = 5432
dbname = "hyperswitch_db"
pool_size = 5

[forex_api]
call_delay = 21600
local_fetch_retry_count = 5
local_fetch_retry_delay = 1000
api_timeout = 20000
api_key = "YOUR API KEY HERE"
fallback_api_key = "YOUR API KEY HERE"
redis_lock_timeout = 26000

[replica_database]
username = "db_user"
password = "db_pass"
Expand Down
2 changes: 1 addition & 1 deletion crates/api_models/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ mime = "0.3.17"
reqwest = { version = "0.11.18", optional = true }
serde = { version = "1.0.163", features = ["derive"] }
serde_json = "1.0.96"
strum = { version = "0.24.1", features = ["derive"] }
strum = { version = "0.25", 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"] }
Expand Down
21 changes: 21 additions & 0 deletions crates/api_models/src/currency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
use common_utils::events::ApiEventMetric;

/// QueryParams to be send to convert the amount -> from_currency -> to_currency
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CurrencyConversionParams {
pub amount: i64,
pub to_currency: String,
pub from_currency: String,
}

/// Response to be send for convert currency route
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CurrencyConversionResponse {
pub converted_amount: String,
pub currency: String,
}

impl ApiEventMetric for CurrencyConversionResponse {}
impl ApiEventMetric for CurrencyConversionParams {}
1 change: 1 addition & 0 deletions crates/api_models/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod api_keys;
pub mod bank_accounts;
pub mod cards_info;
pub mod conditional_configs;
pub mod currency;
pub mod customers;
pub mod disputes;
pub mod enums;
Expand Down
16 changes: 16 additions & 0 deletions crates/currency_conversion/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "currency_conversion"
description = "Currency conversion for cost based routing"
version = "0.1.0"
edition.workspace = true

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# First party crates
common_enums = { version = "0.1.0", path = "../common_enums", package = "common_enums" }

# Third party crates
rust_decimal = "1.29"
rusty-money = { version = "0.4.0", features = ["iso", "crypto"] }
serde = { version = "1.0.163", features = ["derive"] }
thiserror = "1.0.43"
101 changes: 101 additions & 0 deletions crates/currency_conversion/src/conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use common_enums::Currency;
use rust_decimal::Decimal;
use rusty_money::Money;

use crate::{
error::CurrencyConversionError,
types::{currency_match, ExchangeRates},
};

pub fn convert(
ex_rates: &ExchangeRates,
from_currency: Currency,
to_currency: Currency,
amount: i64,
) -> Result<Decimal, CurrencyConversionError> {
let money_minor = Money::from_minor(amount, currency_match(from_currency));
let base_currency = ex_rates.base_currency;
if to_currency == base_currency {
ex_rates.forward_conversion(*money_minor.amount(), from_currency)
} else if from_currency == base_currency {
ex_rates.backward_conversion(*money_minor.amount(), to_currency)
} else {
let base_conversion_amt =
ex_rates.forward_conversion(*money_minor.amount(), from_currency)?;
ex_rates.backward_conversion(base_conversion_amt, to_currency)
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use std::collections::HashMap;

use crate::types::CurrencyFactors;
#[test]
fn currency_to_currency_conversion() {
use super::*;
let mut conversion: HashMap<Currency, CurrencyFactors> = HashMap::new();
let inr_conversion_rates =
CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5));
let szl_conversion_rates =
CurrencyFactors::new(Decimal::new(194423, 4), Decimal::new(514, 4));
let convert_from = Currency::SZL;
let convert_to = Currency::INR;
let amount = 2000;
let base_currency = Currency::USD;
conversion.insert(convert_from, inr_conversion_rates);
conversion.insert(convert_to, szl_conversion_rates);
let sample_rate = ExchangeRates::new(base_currency, conversion);
let res =
convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency");
println!(
"The conversion from {} {} to {} is {:?}",
amount, convert_from, convert_to, res
);
}

#[test]
fn currency_to_base_conversion() {
use super::*;
let mut conversion: HashMap<Currency, CurrencyFactors> = HashMap::new();
let inr_conversion_rates =
CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5));
let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0));
let convert_from = Currency::INR;
let convert_to = Currency::USD;
let amount = 2000;
let base_currency = Currency::USD;
conversion.insert(convert_from, inr_conversion_rates);
conversion.insert(convert_to, usd_conversion_rates);
let sample_rate = ExchangeRates::new(base_currency, conversion);
let res =
convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency");
println!(
"The conversion from {} {} to {} is {:?}",
amount, convert_from, convert_to, res
);
}

#[test]
fn base_to_currency_conversion() {
use super::*;
let mut conversion: HashMap<Currency, CurrencyFactors> = HashMap::new();
let inr_conversion_rates =
CurrencyFactors::new(Decimal::new(823173, 4), Decimal::new(1214, 5));
let usd_conversion_rates = CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0));
let convert_from = Currency::USD;
let convert_to = Currency::INR;
let amount = 2000;
let base_currency = Currency::USD;
conversion.insert(convert_from, usd_conversion_rates);
conversion.insert(convert_to, inr_conversion_rates);
let sample_rate = ExchangeRates::new(base_currency, conversion);
let res =
convert(&sample_rate, convert_from, convert_to, amount).expect("converted_currency");
println!(
"The conversion from {} {} to {} is {:?}",
amount, convert_from, convert_to, res
);
}
}
8 changes: 8 additions & 0 deletions crates/currency_conversion/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[derive(Debug, thiserror::Error, serde::Serialize)]
#[serde(tag = "type", content = "info", rename_all = "snake_case")]
pub enum CurrencyConversionError {
#[error("Currency Conversion isn't possible")]
DecimalMultiplicationFailed,
#[error("Currency not supported: '{0}'")]
ConversionNotSupported(String),
}
3 changes: 3 additions & 0 deletions crates/currency_conversion/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod conversion;
pub mod error;
pub mod types;
Loading

0 comments on commit c0116db

Please sign in to comment.