mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 12:15:40 +08:00
feat(currency_conversion): add currency conversion feature (#2948)
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>
This commit is contained in:
259
Cargo.lock
generated
259
Cargo.lock
generated
@ -381,7 +381,7 @@ dependencies = [
|
||||
"router_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.24.1",
|
||||
"strum 0.25.0",
|
||||
"time",
|
||||
"url",
|
||||
"utoipa",
|
||||
@ -1186,6 +1186,18 @@ version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
|
||||
|
||||
[[package]]
|
||||
name = "bitvec"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c"
|
||||
dependencies = [
|
||||
"funty",
|
||||
"radium",
|
||||
"tap",
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
version = "0.10.6"
|
||||
@ -1227,6 +1239,30 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf617fabf5cdbdc92f774bfe5062d870f228b80056d41180797abf48bed4056e"
|
||||
dependencies = [
|
||||
"borsh-derive",
|
||||
"cfg_aliases",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "borsh-derive"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f404657a7ea7b5249e36808dff544bc88a28f26e0ac40009f674b7a009d14be3"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro-crate",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
"syn_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.4.0"
|
||||
@ -1264,6 +1300,28 @@ version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6372023ac861f6e6dc89c8344a8f398fb42aaba2b5dbc649ca0c0e9dbcb627"
|
||||
dependencies = [
|
||||
"bytecheck_derive",
|
||||
"ptr_meta",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytecheck_derive"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7ec4c6f261935ad534c0c22dbef2201b45918860eb1c574b972bd213a76af61"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.4"
|
||||
@ -1415,6 +1473,12 @@ version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "checked_int_cast"
|
||||
version = "1.0.0"
|
||||
@ -1858,6 +1922,17 @@ dependencies = [
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "currency_conversion"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"common_enums",
|
||||
"rust_decimal",
|
||||
"rusty-money",
|
||||
"serde",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.3"
|
||||
@ -2264,6 +2339,7 @@ name = "euclid_wasm"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"api_models",
|
||||
"currency_conversion",
|
||||
"euclid",
|
||||
"getrandom 0.2.10",
|
||||
"kgraph_utils",
|
||||
@ -2501,6 +2577,12 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||
|
||||
[[package]]
|
||||
name = "funty"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.1.31"
|
||||
@ -4266,6 +4348,15 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8"
|
||||
dependencies = [
|
||||
"toml_edit 0.20.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
@ -4342,6 +4433,26 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1"
|
||||
dependencies = [
|
||||
"ptr_meta_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta_derive"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pulldown-cmark"
|
||||
version = "0.9.3"
|
||||
@ -4415,6 +4526,12 @@ dependencies = [
|
||||
"scheduled-thread-pool",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radium"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.7.3"
|
||||
@ -4643,6 +4760,15 @@ version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "rend"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2571463863a6bd50c32f94402933f03457a3fbaf697a707c5be741e459f08fd"
|
||||
dependencies = [
|
||||
"bytecheck",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.22"
|
||||
@ -4705,6 +4831,34 @@ dependencies = [
|
||||
"winapi 0.3.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0200c8230b013893c0b2d6213d6ec64ed2b9be2e0e016682b7224ff82cff5c58"
|
||||
dependencies = [
|
||||
"bitvec",
|
||||
"bytecheck",
|
||||
"hashbrown 0.12.3",
|
||||
"ptr_meta",
|
||||
"rend",
|
||||
"rkyv_derive",
|
||||
"seahash",
|
||||
"tinyvec",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv_derive"
|
||||
version = "0.7.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2e06b915b5c230a17d7a736d1e2e63ee753c256a8614ef3f5147b13a4f5541d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ron"
|
||||
version = "0.7.1"
|
||||
@ -4755,6 +4909,7 @@ dependencies = [
|
||||
"common_enums",
|
||||
"common_utils",
|
||||
"config",
|
||||
"currency_conversion",
|
||||
"data_models",
|
||||
"derive_deref",
|
||||
"diesel",
|
||||
@ -4793,6 +4948,7 @@ dependencies = [
|
||||
"router_derive",
|
||||
"router_env",
|
||||
"roxmltree",
|
||||
"rust_decimal",
|
||||
"rustc-hash",
|
||||
"scheduler",
|
||||
"serde",
|
||||
@ -4805,7 +4961,7 @@ dependencies = [
|
||||
"sha-1 0.9.8",
|
||||
"sqlx",
|
||||
"storage_impl",
|
||||
"strum 0.24.1",
|
||||
"strum 0.25.0",
|
||||
"tera",
|
||||
"test_utils",
|
||||
"thiserror",
|
||||
@ -4917,6 +5073,32 @@ dependencies = [
|
||||
"ordered-multimap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.33.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06676aec5ccb8fc1da723cc8c0f9a46549f21ebb8753d3915c6c41db1e7f1dc4"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"borsh",
|
||||
"bytes 1.5.0",
|
||||
"num-traits",
|
||||
"rand 0.8.5",
|
||||
"rkyv",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal_macros"
|
||||
version = "1.33.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e43721f4ef7060ebc2c3ede757733209564ca8207f47674181bcd425dd76945"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"rust_decimal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-demangle"
|
||||
version = "0.1.23"
|
||||
@ -5056,6 +5238,16 @@ dependencies = [
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusty-money"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683"
|
||||
dependencies = [
|
||||
"rust_decimal",
|
||||
"rust_decimal_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.15"
|
||||
@ -5136,6 +5328,12 @@ dependencies = [
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "seahash"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
|
||||
|
||||
[[package]]
|
||||
name = "security-framework"
|
||||
version = "2.9.2"
|
||||
@ -5448,6 +5646,12 @@ dependencies = [
|
||||
"tokio 1.32.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a"
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.2"
|
||||
@ -5777,6 +5981,18 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn_derive"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.38",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sync_wrapper"
|
||||
version = "0.1.2"
|
||||
@ -5822,6 +6038,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "tap"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.8.0"
|
||||
@ -6329,7 +6551,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
"toml_edit 0.19.10",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -6351,7 +6573,18 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
"winnow 0.4.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
|
||||
dependencies = [
|
||||
"indexmap 2.0.2",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.19",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7114,6 +7347,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.5.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
@ -7156,6 +7398,15 @@ dependencies = [
|
||||
"winapi-build",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed"
|
||||
dependencies = [
|
||||
"tap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "x509-parser"
|
||||
version = "0.15.1"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = ""
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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"] }
|
||||
|
||||
21
crates/api_models/src/currency.rs
Normal file
21
crates/api_models/src/currency.rs
Normal file
@ -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 {}
|
||||
@ -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;
|
||||
|
||||
16
crates/currency_conversion/Cargo.toml
Normal file
16
crates/currency_conversion/Cargo.toml
Normal file
@ -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
crates/currency_conversion/src/conversion.rs
Normal file
101
crates/currency_conversion/src/conversion.rs
Normal file
@ -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
crates/currency_conversion/src/error.rs
Normal file
8
crates/currency_conversion/src/error.rs
Normal file
@ -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
crates/currency_conversion/src/lib.rs
Normal file
3
crates/currency_conversion/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod conversion;
|
||||
pub mod error;
|
||||
pub mod types;
|
||||
201
crates/currency_conversion/src/types.rs
Normal file
201
crates/currency_conversion/src/types.rs
Normal file
@ -0,0 +1,201 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use common_enums::Currency;
|
||||
use rust_decimal::Decimal;
|
||||
use rusty_money::iso;
|
||||
|
||||
use crate::error::CurrencyConversionError;
|
||||
|
||||
/// Cached currency store of base currency
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct ExchangeRates {
|
||||
pub base_currency: Currency,
|
||||
pub conversion: HashMap<Currency, CurrencyFactors>,
|
||||
}
|
||||
|
||||
/// Stores the multiplicative factor for conversion between currency to base and vice versa
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct CurrencyFactors {
|
||||
/// The factor that will be multiplied to provide Currency output
|
||||
pub to_factor: Decimal,
|
||||
/// The factor that will be multiplied to provide for the base output
|
||||
pub from_factor: Decimal,
|
||||
}
|
||||
|
||||
impl CurrencyFactors {
|
||||
pub fn new(to_factor: Decimal, from_factor: Decimal) -> Self {
|
||||
Self {
|
||||
to_factor,
|
||||
from_factor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ExchangeRates {
|
||||
pub fn new(base_currency: Currency, conversion: HashMap<Currency, CurrencyFactors>) -> Self {
|
||||
Self {
|
||||
base_currency,
|
||||
conversion,
|
||||
}
|
||||
}
|
||||
|
||||
/// The flow here is from_currency -> base_currency -> to_currency
|
||||
/// from to_currency -> base currency
|
||||
pub fn forward_conversion(
|
||||
&self,
|
||||
amt: Decimal,
|
||||
from_currency: Currency,
|
||||
) -> Result<Decimal, CurrencyConversionError> {
|
||||
let from_factor = self
|
||||
.conversion
|
||||
.get(&from_currency)
|
||||
.ok_or_else(|| {
|
||||
CurrencyConversionError::ConversionNotSupported(from_currency.to_string())
|
||||
})?
|
||||
.from_factor;
|
||||
amt.checked_mul(from_factor)
|
||||
.ok_or(CurrencyConversionError::DecimalMultiplicationFailed)
|
||||
}
|
||||
|
||||
/// from base_currency -> to_currency
|
||||
pub fn backward_conversion(
|
||||
&self,
|
||||
amt: Decimal,
|
||||
to_currency: Currency,
|
||||
) -> Result<Decimal, CurrencyConversionError> {
|
||||
let to_factor = self
|
||||
.conversion
|
||||
.get(&to_currency)
|
||||
.ok_or_else(|| {
|
||||
CurrencyConversionError::ConversionNotSupported(to_currency.to_string())
|
||||
})?
|
||||
.to_factor;
|
||||
amt.checked_mul(to_factor)
|
||||
.ok_or(CurrencyConversionError::DecimalMultiplicationFailed)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn currency_match(currency: Currency) -> &'static iso::Currency {
|
||||
match currency {
|
||||
Currency::AED => iso::AED,
|
||||
Currency::ALL => iso::ALL,
|
||||
Currency::AMD => iso::AMD,
|
||||
Currency::ANG => iso::ANG,
|
||||
Currency::ARS => iso::ARS,
|
||||
Currency::AUD => iso::AUD,
|
||||
Currency::AWG => iso::AWG,
|
||||
Currency::AZN => iso::AZN,
|
||||
Currency::BBD => iso::BBD,
|
||||
Currency::BDT => iso::BDT,
|
||||
Currency::BHD => iso::BHD,
|
||||
Currency::BIF => iso::BIF,
|
||||
Currency::BMD => iso::BMD,
|
||||
Currency::BND => iso::BND,
|
||||
Currency::BOB => iso::BOB,
|
||||
Currency::BRL => iso::BRL,
|
||||
Currency::BSD => iso::BSD,
|
||||
Currency::BWP => iso::BWP,
|
||||
Currency::BZD => iso::BZD,
|
||||
Currency::CAD => iso::CAD,
|
||||
Currency::CHF => iso::CHF,
|
||||
Currency::CLP => iso::CLP,
|
||||
Currency::CNY => iso::CNY,
|
||||
Currency::COP => iso::COP,
|
||||
Currency::CRC => iso::CRC,
|
||||
Currency::CUP => iso::CUP,
|
||||
Currency::CZK => iso::CZK,
|
||||
Currency::DJF => iso::DJF,
|
||||
Currency::DKK => iso::DKK,
|
||||
Currency::DOP => iso::DOP,
|
||||
Currency::DZD => iso::DZD,
|
||||
Currency::EGP => iso::EGP,
|
||||
Currency::ETB => iso::ETB,
|
||||
Currency::EUR => iso::EUR,
|
||||
Currency::FJD => iso::FJD,
|
||||
Currency::GBP => iso::GBP,
|
||||
Currency::GHS => iso::GHS,
|
||||
Currency::GIP => iso::GIP,
|
||||
Currency::GMD => iso::GMD,
|
||||
Currency::GNF => iso::GNF,
|
||||
Currency::GTQ => iso::GTQ,
|
||||
Currency::GYD => iso::GYD,
|
||||
Currency::HKD => iso::HKD,
|
||||
Currency::HNL => iso::HNL,
|
||||
Currency::HRK => iso::HRK,
|
||||
Currency::HTG => iso::HTG,
|
||||
Currency::HUF => iso::HUF,
|
||||
Currency::IDR => iso::IDR,
|
||||
Currency::ILS => iso::ILS,
|
||||
Currency::INR => iso::INR,
|
||||
Currency::JMD => iso::JMD,
|
||||
Currency::JOD => iso::JOD,
|
||||
Currency::JPY => iso::JPY,
|
||||
Currency::KES => iso::KES,
|
||||
Currency::KGS => iso::KGS,
|
||||
Currency::KHR => iso::KHR,
|
||||
Currency::KMF => iso::KMF,
|
||||
Currency::KRW => iso::KRW,
|
||||
Currency::KWD => iso::KWD,
|
||||
Currency::KYD => iso::KYD,
|
||||
Currency::KZT => iso::KZT,
|
||||
Currency::LAK => iso::LAK,
|
||||
Currency::LBP => iso::LBP,
|
||||
Currency::LKR => iso::LKR,
|
||||
Currency::LRD => iso::LRD,
|
||||
Currency::LSL => iso::LSL,
|
||||
Currency::MAD => iso::MAD,
|
||||
Currency::MDL => iso::MDL,
|
||||
Currency::MGA => iso::MGA,
|
||||
Currency::MKD => iso::MKD,
|
||||
Currency::MMK => iso::MMK,
|
||||
Currency::MNT => iso::MNT,
|
||||
Currency::MOP => iso::MOP,
|
||||
Currency::MUR => iso::MUR,
|
||||
Currency::MVR => iso::MVR,
|
||||
Currency::MWK => iso::MWK,
|
||||
Currency::MXN => iso::MXN,
|
||||
Currency::MYR => iso::MYR,
|
||||
Currency::NAD => iso::NAD,
|
||||
Currency::NGN => iso::NGN,
|
||||
Currency::NIO => iso::NIO,
|
||||
Currency::NOK => iso::NOK,
|
||||
Currency::NPR => iso::NPR,
|
||||
Currency::NZD => iso::NZD,
|
||||
Currency::OMR => iso::OMR,
|
||||
Currency::PEN => iso::PEN,
|
||||
Currency::PGK => iso::PGK,
|
||||
Currency::PHP => iso::PHP,
|
||||
Currency::PKR => iso::PKR,
|
||||
Currency::PLN => iso::PLN,
|
||||
Currency::PYG => iso::PYG,
|
||||
Currency::QAR => iso::QAR,
|
||||
Currency::RON => iso::RON,
|
||||
Currency::RUB => iso::RUB,
|
||||
Currency::RWF => iso::RWF,
|
||||
Currency::SAR => iso::SAR,
|
||||
Currency::SCR => iso::SCR,
|
||||
Currency::SEK => iso::SEK,
|
||||
Currency::SGD => iso::SGD,
|
||||
Currency::SLL => iso::SLL,
|
||||
Currency::SOS => iso::SOS,
|
||||
Currency::SSP => iso::SSP,
|
||||
Currency::SVC => iso::SVC,
|
||||
Currency::SZL => iso::SZL,
|
||||
Currency::THB => iso::THB,
|
||||
Currency::TTD => iso::TTD,
|
||||
Currency::TRY => iso::TRY,
|
||||
Currency::TWD => iso::TWD,
|
||||
Currency::TZS => iso::TZS,
|
||||
Currency::UGX => iso::UGX,
|
||||
Currency::USD => iso::USD,
|
||||
Currency::UYU => iso::UYU,
|
||||
Currency::UZS => iso::UZS,
|
||||
Currency::VND => iso::VND,
|
||||
Currency::VUV => iso::VUV,
|
||||
Currency::XAF => iso::XAF,
|
||||
Currency::XOF => iso::XOF,
|
||||
Currency::XPF => iso::XPF,
|
||||
Currency::YER => iso::YER,
|
||||
Currency::ZAR => iso::ZAR,
|
||||
}
|
||||
}
|
||||
@ -17,6 +17,7 @@ dummy_connector = ["kgraph_utils/dummy_connector"]
|
||||
|
||||
[dependencies]
|
||||
api_models = { version = "0.1.0", path = "../api_models", package = "api_models" }
|
||||
currency_conversion = { version = "0.1.0", path = "../currency_conversion" }
|
||||
euclid = { path = "../euclid", features = [] }
|
||||
kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" }
|
||||
|
||||
|
||||
@ -7,6 +7,9 @@ use std::{
|
||||
};
|
||||
|
||||
use api_models::{admin as admin_api, routing::ConnectorSelection};
|
||||
use currency_conversion::{
|
||||
conversion::convert as convert_currency, types as currency_conversion_types,
|
||||
};
|
||||
use euclid::{
|
||||
backend::{inputs, interpreter::InterpreterBackend, EuclidBackend},
|
||||
dssa::{
|
||||
@ -33,6 +36,39 @@ struct SeedData<'a> {
|
||||
}
|
||||
|
||||
static SEED_DATA: OnceCell<SeedData<'_>> = OnceCell::new();
|
||||
static SEED_FOREX: OnceCell<currency_conversion_types::ExchangeRates> = OnceCell::new();
|
||||
|
||||
/// This function can be used by the frontend to educate wasm about the forex rates data.
|
||||
/// The input argument is a struct fields base_currency and conversion where later is all the conversions associated with the base_currency
|
||||
/// to all different currencies present.
|
||||
#[wasm_bindgen(js_name = setForexData)]
|
||||
pub fn seed_forex(forex: JsValue) -> JsResult {
|
||||
let forex: currency_conversion_types::ExchangeRates = serde_wasm_bindgen::from_value(forex)?;
|
||||
SEED_FOREX
|
||||
.set(forex)
|
||||
.map_err(|_| "Forex has already been seeded".to_string())
|
||||
.err_to_js()?;
|
||||
|
||||
Ok(JsValue::NULL)
|
||||
}
|
||||
|
||||
/// This function can be used to perform currency_conversion on the input amount, from_currency,
|
||||
/// to_currency which are all expected to be one of currencies we already have in our Currency
|
||||
/// enum.
|
||||
#[wasm_bindgen(js_name = convertCurrency)]
|
||||
pub fn convert_forex_value(amount: i64, from_currency: JsValue, to_currency: JsValue) -> JsResult {
|
||||
let forex_data = SEED_FOREX
|
||||
.get()
|
||||
.ok_or("Forex Data not seeded")
|
||||
.err_to_js()?;
|
||||
let from_currency: enums::Currency = serde_wasm_bindgen::from_value(from_currency)?;
|
||||
let to_currency: enums::Currency = serde_wasm_bindgen::from_value(to_currency)?;
|
||||
let converted_amount = convert_currency(forex_data, from_currency, to_currency, amount)
|
||||
.map_err(|_| "conversion not possible for provided values")
|
||||
.err_to_js()?;
|
||||
|
||||
Ok(serde_wasm_bindgen::to_value(&converted_amount)?)
|
||||
}
|
||||
|
||||
/// This function can be used by the frontend to provide the WASM with information about
|
||||
/// all the merchant's connector accounts. The input argument is a vector of all the merchant's
|
||||
|
||||
@ -76,6 +76,7 @@ regex = "1.8.4"
|
||||
reqwest = { version = "0.11.18", features = ["json", "native-tls", "gzip", "multipart"] }
|
||||
ring = "0.16.20"
|
||||
roxmltree = "0.18.0"
|
||||
rust_decimal = { version = "1.30.0", features = ["serde-with-float", "serde-with-str"] }
|
||||
rustc-hash = "1.1.0"
|
||||
serde = { version = "1.0.163", features = ["derive"] }
|
||||
serde_json = "1.0.96"
|
||||
@ -85,7 +86,7 @@ serde_urlencoded = "0.7.1"
|
||||
serde_with = "3.0.0"
|
||||
sha-1 = { version = "0.9" }
|
||||
sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] }
|
||||
strum = { version = "0.24.1", features = ["derive"] }
|
||||
strum = { version = "0.25", features = ["derive"] }
|
||||
tera = "1.19.1"
|
||||
thiserror = "1.0.40"
|
||||
time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] }
|
||||
@ -104,6 +105,7 @@ api_models = { version = "0.1.0", path = "../api_models", features = ["errors"]
|
||||
cards = { version = "0.1.0", path = "../cards" }
|
||||
common_enums = { version = "0.1.0", path = "../common_enums" }
|
||||
common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals", "async_ext", "logs"] }
|
||||
currency_conversion = { version = "0.1.0", path = "../currency_conversion" }
|
||||
data_models = { version = "0.1.0", path = "../data_models", default-features = false }
|
||||
diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] }
|
||||
euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] }
|
||||
|
||||
@ -13,6 +13,7 @@ use external_services::email::EmailSettings;
|
||||
use external_services::kms;
|
||||
use redis_interface::RedisSettings;
|
||||
pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry};
|
||||
use rust_decimal::Decimal;
|
||||
use scheduler::SchedulerSettings;
|
||||
use serde::{de::Error, Deserialize, Deserializer};
|
||||
|
||||
@ -70,6 +71,7 @@ pub struct Settings {
|
||||
pub secrets: Secrets,
|
||||
pub locker: Locker,
|
||||
pub connectors: Connectors,
|
||||
pub forex_api: ForexApi,
|
||||
pub refund: Refund,
|
||||
pub eph_key: EphemeralConfig,
|
||||
pub scheduler: Option<SchedulerSettings>,
|
||||
@ -119,6 +121,37 @@ pub struct PaymentLink {
|
||||
pub sdk_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ForexApi {
|
||||
pub local_fetch_retry_count: u64,
|
||||
pub api_key: masking::Secret<String>,
|
||||
pub fallback_api_key: masking::Secret<String>,
|
||||
/// in ms
|
||||
pub call_delay: i64,
|
||||
/// in ms
|
||||
pub local_fetch_retry_delay: u64,
|
||||
/// in ms
|
||||
pub api_timeout: u64,
|
||||
/// in ms
|
||||
pub redis_lock_timeout: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
pub struct DefaultExchangeRates {
|
||||
pub base_currency: String,
|
||||
pub conversion: HashMap<String, Conversion>,
|
||||
pub timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
pub struct Conversion {
|
||||
#[serde(with = "rust_decimal::serde::str")]
|
||||
pub to_factor: Decimal,
|
||||
#[serde(with = "rust_decimal::serde::str")]
|
||||
pub from_factor: Decimal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct ApplepayMerchantConfigs {
|
||||
|
||||
@ -5,6 +5,8 @@ pub mod cache;
|
||||
pub mod cards_info;
|
||||
pub mod conditional_config;
|
||||
pub mod configs;
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
pub mod currency;
|
||||
pub mod customers;
|
||||
pub mod disputes;
|
||||
pub mod errors;
|
||||
|
||||
51
crates/router/src/core/currency.rs
Normal file
51
crates/router/src/core/currency.rs
Normal file
@ -0,0 +1,51 @@
|
||||
use common_utils::errors::CustomResult;
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{
|
||||
core::errors::ApiErrorResponse,
|
||||
services::ApplicationResponse,
|
||||
utils::currency::{self, convert_currency, get_forex_rates},
|
||||
AppState,
|
||||
};
|
||||
|
||||
pub async fn retrieve_forex(
|
||||
state: AppState,
|
||||
) -> CustomResult<ApplicationResponse<currency::FxExchangeRatesCacheEntry>, ApiErrorResponse> {
|
||||
Ok(ApplicationResponse::Json(
|
||||
get_forex_rates(
|
||||
&state,
|
||||
state.conf.forex_api.call_delay,
|
||||
state.conf.forex_api.local_fetch_retry_delay,
|
||||
state.conf.forex_api.local_fetch_retry_count,
|
||||
#[cfg(feature = "kms")]
|
||||
&state.conf.kms,
|
||||
)
|
||||
.await
|
||||
.change_context(ApiErrorResponse::GenericNotFoundError {
|
||||
message: "Unable to fetch forex rates".to_string(),
|
||||
})?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn convert_forex(
|
||||
state: AppState,
|
||||
amount: i64,
|
||||
to_currency: String,
|
||||
from_currency: String,
|
||||
) -> CustomResult<
|
||||
ApplicationResponse<api_models::currency::CurrencyConversionResponse>,
|
||||
ApiErrorResponse,
|
||||
> {
|
||||
Ok(ApplicationResponse::Json(
|
||||
Box::pin(convert_currency(
|
||||
state.clone(),
|
||||
amount,
|
||||
to_currency,
|
||||
from_currency,
|
||||
#[cfg(feature = "kms")]
|
||||
&state.conf.kms,
|
||||
))
|
||||
.await
|
||||
.change_context(ApiErrorResponse::InternalServerError)?,
|
||||
))
|
||||
}
|
||||
@ -122,6 +122,7 @@ pub fn mk_app(
|
||||
.service(routes::Payments::server(state.clone()))
|
||||
.service(routes::Customers::server(state.clone()))
|
||||
.service(routes::Configs::server(state.clone()))
|
||||
.service(routes::Forex::server(state.clone()))
|
||||
.service(routes::Refunds::server(state.clone()))
|
||||
.service(routes::MerchantConnectorAccount::server(state.clone()))
|
||||
.service(routes::Mandates::server(state.clone()))
|
||||
|
||||
@ -4,6 +4,8 @@ pub mod app;
|
||||
pub mod cache;
|
||||
pub mod cards_info;
|
||||
pub mod configs;
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
pub mod currency;
|
||||
pub mod customers;
|
||||
pub mod disputes;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
@ -32,6 +34,8 @@ pub mod webhooks;
|
||||
pub mod locker_migration;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
pub use self::app::DummyConnector;
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
pub use self::app::Forex;
|
||||
#[cfg(feature = "payouts")]
|
||||
pub use self::app::Payouts;
|
||||
#[cfg(feature = "olap")]
|
||||
|
||||
@ -10,6 +10,8 @@ use scheduler::SchedulerInterface;
|
||||
use storage_impl::MockDb;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
use super::currency;
|
||||
#[cfg(feature = "dummy_connector")]
|
||||
use super::dummy_connector::*;
|
||||
#[cfg(feature = "payouts")]
|
||||
@ -28,7 +30,7 @@ use super::{cache::*, health::*};
|
||||
use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*};
|
||||
#[cfg(feature = "oltp")]
|
||||
use super::{ephemeral_key::*, payment_methods::*, webhooks::*};
|
||||
use crate::{
|
||||
pub use crate::{
|
||||
configs::settings,
|
||||
db::{StorageImpl, StorageInterface},
|
||||
events::{event_logger::EventLogger, EventHandler},
|
||||
@ -302,6 +304,22 @@ impl Payments {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
pub struct Forex;
|
||||
|
||||
#[cfg(any(feature = "olap", feature = "oltp"))]
|
||||
impl Forex {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
web::scope("/forex")
|
||||
.app_data(web::Data::new(state.clone()))
|
||||
.app_data(web::Data::new(state.clone()))
|
||||
.service(web::resource("/rates").route(web::get().to(currency::retrieve_forex)))
|
||||
.service(
|
||||
web::resource("/convert_from_minor").route(web::get().to(currency::convert_forex)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "olap")]
|
||||
pub struct Routing;
|
||||
|
||||
|
||||
58
crates/router/src/routes/currency.rs
Normal file
58
crates/router/src/routes/currency.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use router_env::Flow;
|
||||
|
||||
use crate::{
|
||||
core::{api_locking, currency},
|
||||
routes::AppState,
|
||||
services::{api, authentication as auth, authorization::permissions::Permission},
|
||||
};
|
||||
|
||||
pub async fn retrieve_forex(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
|
||||
let flow = Flow::RetrieveForexFlow;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
(),
|
||||
|state, _auth: auth::AuthenticationData, _| currency::retrieve_forex(state),
|
||||
auth::auth_type(
|
||||
&auth::ApiKeyAuth,
|
||||
&auth::JWTAuth(Permission::ForexRead),
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn convert_forex(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
params: web::Query<api_models::currency::CurrencyConversionParams>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::RetrieveForexFlow;
|
||||
let amount = ¶ms.amount;
|
||||
let to_currency = ¶ms.to_currency;
|
||||
let from_currency = ¶ms.from_currency;
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
(),
|
||||
|state, _, _| {
|
||||
currency::convert_forex(
|
||||
state,
|
||||
*amount,
|
||||
to_currency.to_string(),
|
||||
from_currency.to_string(),
|
||||
)
|
||||
},
|
||||
auth::auth_type(
|
||||
&auth::ApiKeyAuth,
|
||||
&auth::JWTAuth(Permission::ForexRead),
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
@ -23,6 +23,7 @@ pub enum ApiIdentifier {
|
||||
ApiKeys,
|
||||
PaymentLink,
|
||||
Routing,
|
||||
Forex,
|
||||
RustLockerMigration,
|
||||
Gsm,
|
||||
User,
|
||||
@ -51,6 +52,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::DecisionManagerRetrieveConfig
|
||||
| Flow::DecisionManagerUpsertConfig => Self::Routing,
|
||||
|
||||
Flow::RetrieveForexFlow => Self::Forex,
|
||||
|
||||
Flow::MerchantConnectorsCreate
|
||||
| Flow::MerchantConnectorsRetrieve
|
||||
| Flow::MerchantConnectorsUpdate
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
pub mod currency;
|
||||
pub mod custom_serde;
|
||||
pub mod db_utils;
|
||||
pub mod ext_traits;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
|
||||
#[cfg(feature = "kv_store")]
|
||||
pub mod storage_partitioning;
|
||||
#[cfg(feature = "olap")]
|
||||
pub mod user;
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
|
||||
641
crates/router/src/utils/currency.rs
Normal file
641
crates/router/src/utils/currency.rs
Normal file
@ -0,0 +1,641 @@
|
||||
use std::{collections::HashMap, ops::Deref, str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use api_models::enums;
|
||||
use common_utils::{date_time, errors::CustomResult, events::ApiEventMetric, ext_traits::AsyncExt};
|
||||
use currency_conversion::types::{CurrencyFactors, ExchangeRates};
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
#[cfg(feature = "kms")]
|
||||
use external_services::kms;
|
||||
use masking::PeekInterface;
|
||||
use once_cell::sync::Lazy;
|
||||
use redis_interface::DelReply;
|
||||
use rust_decimal::Decimal;
|
||||
use strum::IntoEnumIterator;
|
||||
use tokio::{sync::RwLock, time::sleep};
|
||||
|
||||
use crate::{
|
||||
logger,
|
||||
routes::app::settings::{Conversion, DefaultExchangeRates},
|
||||
services, AppState,
|
||||
};
|
||||
const REDIX_FOREX_CACHE_KEY: &str = "{forex_cache}_lock";
|
||||
const REDIX_FOREX_CACHE_DATA: &str = "{forex_cache}_data";
|
||||
const FOREX_API_TIMEOUT: u64 = 5;
|
||||
const FOREX_BASE_URL: &str = "https://openexchangerates.org/api/latest.json?app_id=";
|
||||
const FOREX_BASE_CURRENCY: &str = "&base=USD";
|
||||
const FALLBACK_FOREX_BASE_URL: &str = "http://apilayer.net/api/live?access_key=";
|
||||
const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD";
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FxExchangeRatesCacheEntry {
|
||||
data: Arc<ExchangeRates>,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
static FX_EXCHANGE_RATES_CACHE: Lazy<RwLock<Option<FxExchangeRatesCacheEntry>>> =
|
||||
Lazy::new(|| RwLock::new(None));
|
||||
|
||||
impl ApiEventMetric for FxExchangeRatesCacheEntry {}
|
||||
|
||||
#[derive(Debug, Clone, thiserror::Error)]
|
||||
pub enum ForexCacheError {
|
||||
#[error("API error")]
|
||||
ApiError,
|
||||
#[error("API timeout")]
|
||||
ApiTimeout,
|
||||
#[error("API unresponsive")]
|
||||
ApiUnresponsive,
|
||||
#[error("Conversion error")]
|
||||
ConversionError,
|
||||
#[error("Could not acquire the lock for cache entry")]
|
||||
CouldNotAcquireLock,
|
||||
#[error("Provided currency not acceptable")]
|
||||
CurrencyNotAcceptable,
|
||||
#[error("Incorrect entries in default Currency response")]
|
||||
DefaultCurrencyParsingError,
|
||||
#[error("Entry not found in cache")]
|
||||
EntryNotFound,
|
||||
#[error("Expiration time invalid")]
|
||||
InvalidLogExpiry,
|
||||
#[error("Error reading local")]
|
||||
LocalReadError,
|
||||
#[error("Error writing to local cache")]
|
||||
LocalWriteError,
|
||||
#[error("Json Parsing error")]
|
||||
ParsingError,
|
||||
#[error("Kms decryption error")]
|
||||
KmsDecryptionFailed,
|
||||
#[error("Error connecting to redis")]
|
||||
RedisConnectionError,
|
||||
#[error("Not able to release write lock")]
|
||||
RedisLockReleaseFailed,
|
||||
#[error("Error writing to redis")]
|
||||
RedisWriteError,
|
||||
#[error("Not able to acquire write lock")]
|
||||
WriteLockNotAcquired,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
struct ForexResponse {
|
||||
pub rates: HashMap<String, FloatDecimal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
struct FallbackForexResponse {
|
||||
pub quotes: HashMap<String, FloatDecimal>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(transparent)]
|
||||
struct FloatDecimal(#[serde(with = "rust_decimal::serde::float")] Decimal);
|
||||
|
||||
impl Deref for FloatDecimal {
|
||||
type Target = Decimal;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl FxExchangeRatesCacheEntry {
|
||||
fn new(exchange_rate: ExchangeRates) -> Self {
|
||||
Self {
|
||||
data: Arc::new(exchange_rate),
|
||||
timestamp: date_time::now_unix_timestamp(),
|
||||
}
|
||||
}
|
||||
fn is_expired(&self, call_delay: i64) -> bool {
|
||||
self.timestamp + call_delay < date_time::now_unix_timestamp()
|
||||
}
|
||||
}
|
||||
|
||||
async fn retrieve_forex_from_local() -> Option<FxExchangeRatesCacheEntry> {
|
||||
FX_EXCHANGE_RATES_CACHE.read().await.clone()
|
||||
}
|
||||
|
||||
async fn save_forex_to_local(
|
||||
exchange_rates_cache_entry: FxExchangeRatesCacheEntry,
|
||||
) -> CustomResult<(), ForexCacheError> {
|
||||
let mut local = FX_EXCHANGE_RATES_CACHE.write().await;
|
||||
*local = Some(exchange_rates_cache_entry);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Alternative handler for handling the case, When no data in local as well as redis
|
||||
#[allow(dead_code)]
|
||||
async fn waited_fetch_and_update_caches(
|
||||
state: &AppState,
|
||||
local_fetch_retry_delay: u64,
|
||||
local_fetch_retry_count: u64,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
for _n in 1..local_fetch_retry_count {
|
||||
sleep(Duration::from_millis(local_fetch_retry_delay)).await;
|
||||
//read from redis and update local plus break the loop and return
|
||||
match retrieve_forex_from_redis(state).await {
|
||||
Ok(Some(rates)) => {
|
||||
save_forex_to_local(rates.clone()).await?;
|
||||
return Ok(rates.clone());
|
||||
}
|
||||
Ok(None) => continue,
|
||||
Err(e) => {
|
||||
logger::error!(?e);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
//acquire lock one last time and try to fetch and update local & redis
|
||||
successive_fetch_and_save_forex(
|
||||
state,
|
||||
None,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
impl TryFrom<DefaultExchangeRates> for ExchangeRates {
|
||||
type Error = error_stack::Report<ForexCacheError>;
|
||||
fn try_from(value: DefaultExchangeRates) -> Result<Self, Self::Error> {
|
||||
let mut conversion_usable: HashMap<enums::Currency, CurrencyFactors> = HashMap::new();
|
||||
for (curr, conversion) in value.conversion {
|
||||
let enum_curr = enums::Currency::from_str(curr.as_str())
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::ConversionError)?;
|
||||
conversion_usable.insert(enum_curr, CurrencyFactors::from(conversion));
|
||||
}
|
||||
let base_curr = enums::Currency::from_str(value.base_currency.as_str())
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::ConversionError)?;
|
||||
Ok(Self {
|
||||
base_currency: base_curr,
|
||||
conversion: conversion_usable,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Conversion> for CurrencyFactors {
|
||||
fn from(value: Conversion) -> Self {
|
||||
Self {
|
||||
to_factor: value.to_factor,
|
||||
from_factor: value.from_factor,
|
||||
}
|
||||
}
|
||||
}
|
||||
pub async fn get_forex_rates(
|
||||
state: &AppState,
|
||||
call_delay: i64,
|
||||
local_fetch_retry_delay: u64,
|
||||
local_fetch_retry_count: u64,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
if let Some(local_rates) = retrieve_forex_from_local().await {
|
||||
if local_rates.is_expired(call_delay) {
|
||||
// expired local data
|
||||
handler_local_expired(
|
||||
state,
|
||||
call_delay,
|
||||
local_rates,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
// Valid data present in local
|
||||
Ok(local_rates)
|
||||
}
|
||||
} else {
|
||||
// No data in local
|
||||
handler_local_no_data(
|
||||
state,
|
||||
call_delay,
|
||||
local_fetch_retry_delay,
|
||||
local_fetch_retry_count,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn handler_local_no_data(
|
||||
state: &AppState,
|
||||
call_delay: i64,
|
||||
_local_fetch_retry_delay: u64,
|
||||
_local_fetch_retry_count: u64,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
match retrieve_forex_from_redis(state).await {
|
||||
Ok(Some(data)) => {
|
||||
fallback_forex_redis_check(
|
||||
state,
|
||||
data,
|
||||
call_delay,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
Ok(None) => {
|
||||
// No data in local as well as redis
|
||||
Ok(successive_fetch_and_save_forex(
|
||||
state,
|
||||
None,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
Err(err) => {
|
||||
logger::error!(?err);
|
||||
Ok(successive_fetch_and_save_forex(
|
||||
state,
|
||||
None,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn successive_fetch_and_save_forex(
|
||||
state: &AppState,
|
||||
stale_redis_data: Option<FxExchangeRatesCacheEntry>,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
match acquire_redis_lock(state).await {
|
||||
Ok(lock_acquired) => {
|
||||
if !lock_acquired {
|
||||
return stale_redis_data.ok_or(ForexCacheError::CouldNotAcquireLock.into());
|
||||
}
|
||||
let api_rates = fetch_forex_rates(
|
||||
state,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await;
|
||||
match api_rates {
|
||||
Ok(rates) => successive_save_data_to_redis_local(state, rates).await,
|
||||
Err(err) => {
|
||||
// API not able to fetch data call secondary service
|
||||
logger::error!(?err);
|
||||
let secondary_api_rates = fallback_fetch_forex_rates(
|
||||
state,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await;
|
||||
match secondary_api_rates {
|
||||
Ok(rates) => Ok(successive_save_data_to_redis_local(state, rates).await?),
|
||||
Err(err) => stale_redis_data.ok_or({
|
||||
logger::error!(?err);
|
||||
ForexCacheError::ApiUnresponsive.into()
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => stale_redis_data.ok_or({
|
||||
logger::error!(?e);
|
||||
ForexCacheError::ApiUnresponsive.into()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
async fn successive_save_data_to_redis_local(
|
||||
state: &AppState,
|
||||
forex: FxExchangeRatesCacheEntry,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
Ok(save_forex_to_redis(state, &forex)
|
||||
.await
|
||||
.async_and_then(|_rates| async { release_redis_lock(state).await })
|
||||
.await
|
||||
.async_and_then(|_val| async { Ok(save_forex_to_local(forex.clone()).await) })
|
||||
.await
|
||||
.map_or_else(
|
||||
|e| {
|
||||
logger::error!(?e);
|
||||
forex.clone()
|
||||
},
|
||||
|_| forex.clone(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn fallback_forex_redis_check(
|
||||
state: &AppState,
|
||||
redis_data: FxExchangeRatesCacheEntry,
|
||||
call_delay: i64,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
match is_redis_expired(Some(redis_data.clone()).as_ref(), call_delay).await {
|
||||
Some(redis_forex) => {
|
||||
// Valid data present in redis
|
||||
let exchange_rates = FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone());
|
||||
save_forex_to_local(exchange_rates.clone()).await?;
|
||||
Ok(exchange_rates)
|
||||
}
|
||||
None => {
|
||||
// redis expired
|
||||
successive_fetch_and_save_forex(
|
||||
state,
|
||||
Some(redis_data),
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handler_local_expired(
|
||||
state: &AppState,
|
||||
call_delay: i64,
|
||||
local_rates: FxExchangeRatesCacheEntry,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
match retrieve_forex_from_redis(state).await {
|
||||
Ok(redis_data) => {
|
||||
match is_redis_expired(redis_data.as_ref(), call_delay).await {
|
||||
Some(redis_forex) => {
|
||||
// Valid data present in redis
|
||||
let exchange_rates =
|
||||
FxExchangeRatesCacheEntry::new(redis_forex.as_ref().clone());
|
||||
save_forex_to_local(exchange_rates.clone()).await?;
|
||||
Ok(exchange_rates)
|
||||
}
|
||||
None => {
|
||||
// Redis is expired going for API request
|
||||
successive_fetch_and_save_forex(
|
||||
state,
|
||||
Some(local_rates),
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// data not present in redis waited fetch
|
||||
logger::error!(?e);
|
||||
successive_fetch_and_save_forex(
|
||||
state,
|
||||
Some(local_rates),
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_forex_rates(
|
||||
state: &AppState,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> Result<FxExchangeRatesCacheEntry, error_stack::Report<ForexCacheError>> {
|
||||
#[cfg(feature = "kms")]
|
||||
let forex_api_key = kms::get_kms_client(kms_config)
|
||||
.await
|
||||
.decrypt(state.conf.forex_api.api_key.peek())
|
||||
.await
|
||||
.change_context(ForexCacheError::KmsDecryptionFailed)?;
|
||||
|
||||
#[cfg(not(feature = "kms"))]
|
||||
let forex_api_key = state.conf.forex_api.api_key.peek();
|
||||
|
||||
let forex_url: String = format!("{}{}{}", FOREX_BASE_URL, forex_api_key, FOREX_BASE_CURRENCY);
|
||||
let forex_request = services::RequestBuilder::new()
|
||||
.method(services::Method::Get)
|
||||
.url(&forex_url)
|
||||
.build();
|
||||
|
||||
logger::info!(?forex_request);
|
||||
let response = state
|
||||
.api_client
|
||||
.send_request(
|
||||
&state.clone(),
|
||||
forex_request,
|
||||
Some(FOREX_API_TIMEOUT),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.change_context(ForexCacheError::ApiUnresponsive)?;
|
||||
let forex_response = response
|
||||
.json::<ForexResponse>()
|
||||
.await
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::ParsingError)?;
|
||||
|
||||
logger::info!("{:?}", forex_response);
|
||||
|
||||
let mut conversions: HashMap<enums::Currency, CurrencyFactors> = HashMap::new();
|
||||
for enum_curr in enums::Currency::iter() {
|
||||
match forex_response.rates.get(&enum_curr.to_string()) {
|
||||
Some(rate) => {
|
||||
let from_factor = match Decimal::new(1, 0).checked_div(**rate) {
|
||||
Some(rate) => rate,
|
||||
None => {
|
||||
logger::error!("Rates for {} not received from API", &enum_curr);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let currency_factors = CurrencyFactors::new(**rate, from_factor);
|
||||
conversions.insert(enum_curr, currency_factors);
|
||||
}
|
||||
None => {
|
||||
logger::error!("Rates for {} not received from API", &enum_curr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(FxExchangeRatesCacheEntry::new(ExchangeRates::new(
|
||||
enums::Currency::USD,
|
||||
conversions,
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn fallback_fetch_forex_rates(
|
||||
state: &AppState,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<FxExchangeRatesCacheEntry, ForexCacheError> {
|
||||
#[cfg(feature = "kms")]
|
||||
let fallback_forex_api_key = kms::get_kms_client(kms_config)
|
||||
.await
|
||||
.decrypt(state.conf.forex_api.fallback_api_key.peek())
|
||||
.await
|
||||
.change_context(ForexCacheError::KmsDecryptionFailed)?;
|
||||
|
||||
#[cfg(not(feature = "kms"))]
|
||||
let fallback_forex_api_key = state.conf.forex_api.fallback_api_key.peek();
|
||||
|
||||
let fallback_forex_url: String =
|
||||
format!("{}{}", FALLBACK_FOREX_BASE_URL, fallback_forex_api_key,);
|
||||
let fallback_forex_request = services::RequestBuilder::new()
|
||||
.method(services::Method::Get)
|
||||
.url(&fallback_forex_url)
|
||||
.build();
|
||||
|
||||
logger::info!(?fallback_forex_request);
|
||||
let response = state
|
||||
.api_client
|
||||
.send_request(
|
||||
&state.clone(),
|
||||
fallback_forex_request,
|
||||
Some(FOREX_API_TIMEOUT),
|
||||
false,
|
||||
)
|
||||
.await
|
||||
.change_context(ForexCacheError::ApiUnresponsive)?;
|
||||
let fallback_forex_response = response
|
||||
.json::<FallbackForexResponse>()
|
||||
.await
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::ParsingError)?;
|
||||
|
||||
logger::info!("{:?}", fallback_forex_response);
|
||||
let mut conversions: HashMap<enums::Currency, CurrencyFactors> = HashMap::new();
|
||||
for enum_curr in enums::Currency::iter() {
|
||||
match fallback_forex_response.quotes.get(
|
||||
format!(
|
||||
"{}{}",
|
||||
FALLBACK_FOREX_API_CURRENCY_PREFIX,
|
||||
&enum_curr.to_string()
|
||||
)
|
||||
.as_str(),
|
||||
) {
|
||||
Some(rate) => {
|
||||
let from_factor = match Decimal::new(1, 0).checked_div(**rate) {
|
||||
Some(rate) => rate,
|
||||
None => {
|
||||
logger::error!("Rates for {} not received from API", &enum_curr);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let currency_factors = CurrencyFactors::new(**rate, from_factor);
|
||||
conversions.insert(enum_curr, currency_factors);
|
||||
}
|
||||
None => {
|
||||
logger::error!("Rates for {} not received from API", &enum_curr);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let rates =
|
||||
FxExchangeRatesCacheEntry::new(ExchangeRates::new(enums::Currency::USD, conversions));
|
||||
match acquire_redis_lock(state).await {
|
||||
Ok(_) => Ok(successive_save_data_to_redis_local(state, rates).await?),
|
||||
Err(e) => {
|
||||
logger::error!(?e);
|
||||
Ok(rates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn release_redis_lock(
|
||||
state: &AppState,
|
||||
) -> Result<DelReply, error_stack::Report<ForexCacheError>> {
|
||||
state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(ForexCacheError::RedisConnectionError)?
|
||||
.delete_key(REDIX_FOREX_CACHE_KEY)
|
||||
.await
|
||||
.change_context(ForexCacheError::RedisLockReleaseFailed)
|
||||
}
|
||||
|
||||
async fn acquire_redis_lock(app_state: &AppState) -> CustomResult<bool, ForexCacheError> {
|
||||
app_state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(ForexCacheError::RedisConnectionError)?
|
||||
.set_key_if_not_exists_with_expiry(
|
||||
REDIX_FOREX_CACHE_KEY,
|
||||
"",
|
||||
Some(
|
||||
(app_state.conf.forex_api.local_fetch_retry_count
|
||||
* app_state.conf.forex_api.local_fetch_retry_delay
|
||||
+ app_state.conf.forex_api.api_timeout)
|
||||
.try_into()
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::ConversionError)?,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.map(|val| matches!(val, redis_interface::SetnxReply::KeySet))
|
||||
.change_context(ForexCacheError::CouldNotAcquireLock)
|
||||
}
|
||||
|
||||
async fn save_forex_to_redis(
|
||||
app_state: &AppState,
|
||||
forex_exchange_cache_entry: &FxExchangeRatesCacheEntry,
|
||||
) -> CustomResult<(), ForexCacheError> {
|
||||
app_state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(ForexCacheError::RedisConnectionError)?
|
||||
.serialize_and_set_key(REDIX_FOREX_CACHE_DATA, forex_exchange_cache_entry)
|
||||
.await
|
||||
.change_context(ForexCacheError::RedisWriteError)
|
||||
}
|
||||
|
||||
async fn retrieve_forex_from_redis(
|
||||
app_state: &AppState,
|
||||
) -> CustomResult<Option<FxExchangeRatesCacheEntry>, ForexCacheError> {
|
||||
app_state
|
||||
.store
|
||||
.get_redis_conn()
|
||||
.change_context(ForexCacheError::RedisConnectionError)?
|
||||
.get_and_deserialize_key(REDIX_FOREX_CACHE_DATA, "FxExchangeRatesCache")
|
||||
.await
|
||||
.change_context(ForexCacheError::EntryNotFound)
|
||||
}
|
||||
|
||||
async fn is_redis_expired(
|
||||
redis_cache: Option<&FxExchangeRatesCacheEntry>,
|
||||
call_delay: i64,
|
||||
) -> Option<Arc<ExchangeRates>> {
|
||||
redis_cache.and_then(|cache| {
|
||||
if cache.timestamp + call_delay > date_time::now_unix_timestamp() {
|
||||
Some(cache.data.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn convert_currency(
|
||||
state: AppState,
|
||||
amount: i64,
|
||||
to_currency: String,
|
||||
from_currency: String,
|
||||
#[cfg(feature = "kms")] kms_config: &kms::KmsConfig,
|
||||
) -> CustomResult<api_models::currency::CurrencyConversionResponse, ForexCacheError> {
|
||||
let rates = get_forex_rates(
|
||||
&state,
|
||||
state.conf.forex_api.call_delay,
|
||||
state.conf.forex_api.local_fetch_retry_delay,
|
||||
state.conf.forex_api.local_fetch_retry_count,
|
||||
#[cfg(feature = "kms")]
|
||||
kms_config,
|
||||
)
|
||||
.await
|
||||
.change_context(ForexCacheError::ApiError)?;
|
||||
|
||||
let to_currency = api_models::enums::Currency::from_str(to_currency.as_str())
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::CurrencyNotAcceptable)?;
|
||||
|
||||
let from_currency = api_models::enums::Currency::from_str(from_currency.as_str())
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::CurrencyNotAcceptable)?;
|
||||
|
||||
let converted_amount =
|
||||
currency_conversion::conversion::convert(&rates.data, from_currency, to_currency, amount)
|
||||
.into_report()
|
||||
.change_context(ForexCacheError::ConversionError)?;
|
||||
|
||||
Ok(api_models::currency::CurrencyConversionResponse {
|
||||
converted_amount: converted_amount.to_string(),
|
||||
currency: to_currency.to_string(),
|
||||
})
|
||||
}
|
||||
@ -163,6 +163,8 @@ pub enum Flow {
|
||||
RefundsUpdate,
|
||||
/// Refunds list flow.
|
||||
RefundsList,
|
||||
// Retrieve forex flow.
|
||||
RetrieveForexFlow,
|
||||
/// Routing create flow,
|
||||
RoutingCreateConfig,
|
||||
/// Routing link config
|
||||
|
||||
@ -34,6 +34,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
|
||||
|
||||
[eph_key]
|
||||
validity = 1
|
||||
|
||||
|
||||
Reference in New Issue
Block a user