diff --git a/Cargo.lock b/Cargo.lock index bf0ee2d110c7..e4a317d74f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/config/config.example.toml b/config/config.example.toml index 7815f2400d04..0b8730ca114a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -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 diff --git a/config/development.toml b/config/development.toml index c82607a704c3..bcf561dd5857 100644 --- a/config/development.toml +++ b/config/development.toml @@ -20,6 +20,7 @@ port = 5432 dbname = "hyperswitch_db" pool_size = 5 connection_timeout = 10 +min_idle = 2 [replica_database] username = "db_user" @@ -52,6 +53,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 = "" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 986240f0a36b..445e1e856846 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -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" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ce882e913282..73c2d673c972 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -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"] } diff --git a/crates/api_models/src/currency.rs b/crates/api_models/src/currency.rs new file mode 100644 index 000000000000..c1d7e422d041 --- /dev/null +++ b/crates/api_models/src/currency.rs @@ -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 {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 1abeff7b6ddb..ab40a96582bb 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -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; diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 74559f8ed69a..acb9bbdd6cd4 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -717,6 +717,14 @@ pub struct Card { pub nick_name: Option>, } +#[derive(Eq, PartialEq, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub struct CardToken { + /// The card holder's name + #[schema(value_type = String, example = "John Test")] + pub card_holder_name: Option>, +} + #[derive(Eq, PartialEq, Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum CardRedirectData { @@ -846,6 +854,7 @@ pub enum PaymentMethodData { Upi(UpiData), Voucher(VoucherData), GiftCard(Box), + CardToken(CardToken), } impl PaymentMethodData { @@ -873,7 +882,8 @@ impl PaymentMethodData { | Self::Reward | Self::Upi(_) | Self::Voucher(_) - | Self::GiftCard(_) => None, + | Self::GiftCard(_) + | Self::CardToken(_) => None, } } } @@ -1092,6 +1102,7 @@ pub enum AdditionalPaymentData { GiftCard {}, Voucher {}, CardRedirect {}, + CardToken {}, } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] @@ -1660,6 +1671,7 @@ pub enum PaymentMethodDataResponse { Voucher, GiftCard, CardRedirect, + CardToken, } #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -2455,6 +2467,7 @@ impl From for PaymentMethodDataResponse { AdditionalPaymentData::Voucher {} => Self::Voucher, AdditionalPaymentData::GiftCard {} => Self::GiftCard, AdditionalPaymentData::CardRedirect {} => Self::CardRedirect, + AdditionalPaymentData::CardToken {} => Self::CardToken, } } } diff --git a/crates/currency_conversion/Cargo.toml b/crates/currency_conversion/Cargo.toml new file mode 100644 index 000000000000..7eb3af7d526d --- /dev/null +++ b/crates/currency_conversion/Cargo.toml @@ -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" diff --git a/crates/currency_conversion/src/conversion.rs b/crates/currency_conversion/src/conversion.rs new file mode 100644 index 000000000000..4cdca8fe0ea2 --- /dev/null +++ b/crates/currency_conversion/src/conversion.rs @@ -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 { + 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 = 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 = 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 = 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 + ); + } +} diff --git a/crates/currency_conversion/src/error.rs b/crates/currency_conversion/src/error.rs new file mode 100644 index 000000000000..b04c147147c3 --- /dev/null +++ b/crates/currency_conversion/src/error.rs @@ -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), +} diff --git a/crates/currency_conversion/src/lib.rs b/crates/currency_conversion/src/lib.rs new file mode 100644 index 000000000000..48e1ae11e5d3 --- /dev/null +++ b/crates/currency_conversion/src/lib.rs @@ -0,0 +1,3 @@ +pub mod conversion; +pub mod error; +pub mod types; diff --git a/crates/currency_conversion/src/types.rs b/crates/currency_conversion/src/types.rs new file mode 100644 index 000000000000..fec25b9fc601 --- /dev/null +++ b/crates/currency_conversion/src/types.rs @@ -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, +} + +/// 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) -> 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 { + 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 { + 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, + } +} diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 4fc8cd970f40..47e349847ef7 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -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" } diff --git a/crates/euclid_wasm/src/lib.rs b/crates/euclid_wasm/src/lib.rs index e85a002544ff..48d9ac0d82a8 100644 --- a/crates/euclid_wasm/src/lib.rs +++ b/crates/euclid_wasm/src/lib.rs @@ -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> = OnceCell::new(); +static SEED_FOREX: OnceCell = 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 diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 25feb373b734..f0316d69249e 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -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"] } diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 90126a8bc893..f5c3b46b27f2 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -30,6 +30,8 @@ impl Default for super::settings::Database { pool_size: 5, connection_timeout: 10, queue_strategy: Default::default(), + min_idle: None, + max_lifetime: None, } } } diff --git a/crates/router/src/configs/kms.rs b/crates/router/src/configs/kms.rs index 205169fa291b..c2f159d16cf1 100644 --- a/crates/router/src/configs/kms.rs +++ b/crates/router/src/configs/kms.rs @@ -64,6 +64,8 @@ impl KmsDecrypt for settings::Database { pool_size: self.pool_size, connection_timeout: self.connection_timeout, queue_strategy: self.queue_strategy.into(), + min_idle: self.min_idle, + max_lifetime: self.max_lifetime, }) } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 0007e636926c..918ae6647eef 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -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, @@ -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, + pub fallback_api_key: masking::Secret, + /// 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, + 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 { @@ -484,6 +517,8 @@ pub struct Database { pub pool_size: u32, pub connection_timeout: u64, pub queue_strategy: QueueStrategy, + pub min_idle: Option, + pub max_lifetime: Option, } #[derive(Debug, Deserialize, Clone, Default)] @@ -515,6 +550,8 @@ impl From for storage_impl::config::Database { pool_size: val.pool_size, connection_timeout: val.connection_timeout, queue_strategy: val.queue_strategy.into(), + min_idle: val.min_idle, + max_lifetime: val.max_lifetime, } } } diff --git a/crates/router/src/connector/aci/transformers.rs b/crates/router/src/connector/aci/transformers.rs index f56369ed31ab..66aeb3bb6b2b 100644 --- a/crates/router/src/connector/aci/transformers.rs +++ b/crates/router/src/connector/aci/transformers.rs @@ -409,7 +409,8 @@ impl TryFrom<&AciRouterData<&types::PaymentsAuthorizeRouterData>> for AciPayment | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.router_data.payment_method), connector: "Aci", })?, diff --git a/crates/router/src/connector/adyen.rs b/crates/router/src/connector/adyen.rs index e101b796b8d4..ddd93bc289a9 100644 --- a/crates/router/src/connector/adyen.rs +++ b/crates/router/src/connector/adyen.rs @@ -14,11 +14,8 @@ use crate::{ configs::settings, connector::utils as connector_utils, consts, - core::{ - self, - errors::{self, CustomResult}, - }, - headers, logger, routes, + core::errors::{self, CustomResult}, + headers, logger, services::{ self, request::{self, Mask}, @@ -560,7 +557,6 @@ impl } } -#[async_trait::async_trait] impl services::ConnectorIntegration< api::Authorize, @@ -568,49 +564,6 @@ impl types::PaymentsResponseData, > for Adyen { - async fn execute_pretasks( - &self, - router_data: &mut types::PaymentsAuthorizeRouterData, - app_state: &routes::AppState, - ) -> CustomResult<(), errors::ConnectorError> { - match &router_data.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card_data) => { - match gift_card_data.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let integ: Box< - &(dyn services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - > + Send - + Sync - + 'static), - > = Box::new(&Self); - - let authorize_data = &types::PaymentsBalanceRouterData::from(( - &router_data.to_owned(), - router_data.request.clone(), - )); - - let resp = services::execute_connector_processing_step( - app_state, - integ, - authorize_data, - core::payments::CallConnectorAction::Trigger, - None, - ) - .await?; - router_data.payment_method_balance = resp.payment_method_balance; - - Ok(()) - } - _ => Ok(()), - } - } - _ => Ok(()), - } - } - fn get_headers( &self, req: &types::PaymentsAuthorizeRouterData, @@ -667,7 +620,6 @@ impl req: &types::PaymentsAuthorizeRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - check_for_payment_method_balance(req)?; Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) @@ -725,28 +677,23 @@ impl } } +impl api::PaymentsPreProcessing for Adyen {} + impl services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, + api::PreProcessing, + types::PaymentsPreProcessingData, types::PaymentsResponseData, > for Adyen { fn get_headers( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, - ) -> CustomResult)>, errors::ConnectorError> - where - Self: services::ConnectorIntegration< - api::Balance, - types::PaymentsAuthorizeData, - types::PaymentsResponseData, - >, - { + ) -> CustomResult)>, errors::ConnectorError> { let mut header = vec![( headers::CONTENT_TYPE.to_string(), - types::PaymentsBalanceType::get_content_type(self) + types::PaymentsPreProcessingType::get_content_type(self) .to_string() .into(), )]; @@ -757,7 +704,7 @@ impl fn get_url( &self, - _req: &types::PaymentsBalanceRouterData, + _req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult { Ok(format!( @@ -768,7 +715,7 @@ impl fn get_request_body( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { let connector_req = adyen::AdyenBalanceRequest::try_from(req)?; @@ -783,18 +730,20 @@ impl fn build_request( &self, - req: &types::PaymentsBalanceRouterData, + req: &types::PaymentsPreProcessingRouterData, connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( services::RequestBuilder::new() .method(services::Method::Post) - .url(&types::PaymentsBalanceType::get_url(self, req, connectors)?) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) .attach_default_headers() - .headers(types::PaymentsBalanceType::get_headers( + .headers(types::PaymentsPreProcessingType::get_headers( self, req, connectors, )?) - .body(types::PaymentsBalanceType::get_request_body( + .body(types::PaymentsPreProcessingType::get_request_body( self, req, connectors, )?) .build(), @@ -803,19 +752,47 @@ impl fn handle_response( &self, - data: &types::PaymentsBalanceRouterData, + data: &types::PaymentsPreProcessingRouterData, res: types::Response, - ) -> CustomResult { + ) -> CustomResult { let response: adyen::AdyenBalanceResponse = res .response .parse_struct("AdyenBalanceResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - types::RouterData::try_from(types::ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - .change_context(errors::ConnectorError::ResponseHandlingFailed) + + let currency = match data.request.currency { + Some(currency) => currency, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "currency", + })?, + }; + let amount = match data.request.amount { + Some(amount) => amount, + None => Err(errors::ConnectorError::MissingRequiredField { + field_name: "amount", + })?, + }; + + if response.balance.currency != currency || response.balance.value < amount { + Ok(types::RouterData { + response: Err(types::ErrorResponse { + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + reason: Some(consts::LOW_BALANCE_ERROR_MESSAGE.to_string()), + status_code: res.status_code, + attempt_status: Some(enums::AttemptStatus::Failure), + connector_transaction_id: None, + }), + ..data.clone() + }) + } else { + types::RouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } } fn get_error_response( @@ -1634,7 +1611,7 @@ impl api::IncomingWebhook for Adyen { .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; Ok(api::disputes::DisputePayload { amount: notif.amount.value.to_string(), - currency: notif.amount.currency, + currency: notif.amount.currency.to_string(), dispute_stage: api_models::enums::DisputeStage::from(notif.event_code.clone()), connector_dispute_id: notif.psp_reference, connector_reason: notif.reason, @@ -1646,27 +1623,3 @@ impl api::IncomingWebhook for Adyen { }) } } - -pub fn check_for_payment_method_balance( - req: &types::PaymentsAuthorizeRouterData, -) -> CustomResult<(), errors::ConnectorError> { - match &req.request.payment_method_data { - api_models::payments::PaymentMethodData::GiftCard(gift_card) => match gift_card.as_ref() { - api_models::payments::GiftCardData::Givex(_) => { - let payment_method_balance = req - .payment_method_balance - .as_ref() - .ok_or(errors::ConnectorError::RequestEncodingFailed)?; - if payment_method_balance.currency != req.request.currency.to_string() - || payment_method_balance.amount < req.request.amount - { - Err(errors::ConnectorError::InSufficientBalanceInPaymentMethod.into()) - } else { - Ok(()) - } - } - _ => Ok(()), - }, - _ => Ok(()), - } -} diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index a75e3b8ff179..cfa601112677 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -213,8 +213,8 @@ pub struct AdyenBalanceRequest<'a> { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenBalanceResponse { - psp_reference: String, - balance: Amount, + pub psp_reference: String, + pub balance: Amount, } /// This implementation will be used only in Authorize, Automatic capture flow. @@ -397,8 +397,8 @@ pub enum ActionType { #[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct Amount { - currency: String, - value: i64, + pub currency: storage_enums::Currency, + pub value: i64, } #[derive(Debug, Clone, Serialize)] @@ -1380,7 +1380,8 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> payments::PaymentMethodData::Crypto(_) | payments::PaymentMethodData::MandatePayment | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) => { + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Adyen", @@ -1391,11 +1392,11 @@ impl<'a> TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> } } -impl<'a> TryFrom<&types::PaymentsBalanceRouterData> for AdyenBalanceRequest<'a> { +impl<'a> TryFrom<&types::PaymentsPreProcessingRouterData> for AdyenBalanceRequest<'a> { type Error = Error; - fn try_from(item: &types::PaymentsBalanceRouterData) -> Result { + fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { let payment_method = match &item.request.payment_method_data { - payments::PaymentMethodData::GiftCard(gift_card_data) => { + Some(payments::PaymentMethodData::GiftCard(gift_card_data)) => { match gift_card_data.as_ref() { payments::GiftCardData::Givex(gift_card_data) => { let balance_pm = BalancePmData { @@ -1509,7 +1510,7 @@ fn get_channel_type(pm_type: &Option) -> Optio fn get_amount_data(item: &AdyenRouterData<&types::PaymentsAuthorizeRouterData>) -> Amount { Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), } } @@ -2276,7 +2277,8 @@ impl<'a> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: "Network tokenization for payment method".to_string(), connector: "Adyen", @@ -2855,12 +2857,24 @@ impl TryFrom> } } -impl TryFrom> - for types::PaymentsBalanceRouterData +impl + TryFrom< + types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, + > for types::RouterData { type Error = Error; fn try_from( - item: types::PaymentsBalanceResponseRouterData, + item: types::ResponseRouterData< + F, + AdyenBalanceResponse, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + >, ) -> Result { Ok(Self { response: Ok(types::PaymentsResponseData::TransactionResponse { @@ -3455,7 +3469,7 @@ impl TryFrom<&AdyenRouterData<&types::PaymentsCaptureRouterData>> for AdyenCaptu merchant_account: auth_type.merchant_account, reference, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.amount.to_owned(), }, }) @@ -3545,7 +3559,7 @@ impl TryFrom<&AdyenRouterData<&types::RefundsRouterData>> for AdyenRefundR Ok(Self { merchant_account: auth_type.merchant_account, amount: Amount { - currency: item.router_data.request.currency.to_string(), + currency: item.router_data.request.currency, value: item.router_data.request.refund_amount, }, merchant_refund_reason: item.router_data.request.reason.clone(), @@ -3627,7 +3641,7 @@ pub struct AdyenAdditionalDataWH { #[derive(Debug, Deserialize)] pub struct AdyenAmountWH { pub value: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[derive(Clone, Debug, Deserialize, Serialize, strum::Display, PartialEq)] @@ -3953,7 +3967,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutE )?; Ok(Self { amount: Amount { - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, value: item.amount.to_owned(), }, merchant_account: auth_type.merchant_account, @@ -4028,7 +4042,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC Ok(Self { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, recurring: RecurringContract { contract: Contract::Payout, @@ -4075,7 +4089,7 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutF Ok(Self::Card(Box::new(PayoutFulfillCardRequest { amount: Amount { value: item.amount.to_owned(), - currency: item.router_data.request.destination_currency.to_string(), + currency: item.router_data.request.destination_currency, }, card: get_payout_card_details(&item.router_data.get_payout_method_data()?) .map_or( diff --git a/crates/router/src/connector/airwallex/transformers.rs b/crates/router/src/connector/airwallex/transformers.rs index 457b8d075487..3785e02d4747 100644 --- a/crates/router/src/connector/airwallex/transformers.rs +++ b/crates/router/src/connector/airwallex/transformers.rs @@ -196,7 +196,8 @@ impl TryFrom<&AirwallexRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("airwallex"), )), }?; diff --git a/crates/router/src/connector/bankofamerica/transformers.rs b/crates/router/src/connector/bankofamerica/transformers.rs index 70db9a6d8797..12170deb1a00 100644 --- a/crates/router/src/connector/bankofamerica/transformers.rs +++ b/crates/router/src/connector/bankofamerica/transformers.rs @@ -410,7 +410,8 @@ impl TryFrom<&BankOfAmericaRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Bank of America"), ) diff --git a/crates/router/src/connector/bluesnap/transformers.rs b/crates/router/src/connector/bluesnap/transformers.rs index fe92c337a012..b4ed314e706a 100644 --- a/crates/router/src/connector/bluesnap/transformers.rs +++ b/crates/router/src/connector/bluesnap/transformers.rs @@ -221,7 +221,8 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::CardRedirect(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( "Selected payment method via Token flow through bluesnap".to_string(), )) @@ -240,160 +241,160 @@ impl TryFrom<&BluesnapRouterData<&types::PaymentsAuthorizeRouterData>> for Blues Some(enums::CaptureMethod::Manual) => BluesnapTxnType::AuthOnly, _ => BluesnapTxnType::AuthCapture, }; - let (payment_method, card_holder_info) = - match item.router_data.request.payment_method_data.clone() { - api::PaymentMethodData::Card(ref ccard) => Ok(( - PaymentMethodDetails::CreditCard(Card { - card_number: ccard.card_number.clone(), - expiration_month: ccard.card_exp_month.clone(), - expiration_year: ccard.get_expiry_year_4_digit(), - security_code: ccard.card_cvc.clone(), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )), - api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { - api_models::payments::WalletData::GooglePay(payment_method_data) => { - let gpay_object = - Encode::::encode_to_string_of_json( - &BluesnapGooglePayObject { - payment_method_data: utils::GooglePayWalletData::from( - payment_method_data, - ), - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::GooglePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(gpay_object), - ), - }), - None, - )) + let (payment_method, card_holder_info) = match item + .router_data + .request + .payment_method_data + .clone() + { + api::PaymentMethodData::Card(ref ccard) => Ok(( + PaymentMethodDetails::CreditCard(Card { + card_number: ccard.card_number.clone(), + expiration_month: ccard.card_exp_month.clone(), + expiration_year: ccard.get_expiry_year_4_digit(), + security_code: ccard.card_cvc.clone(), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )), + api::PaymentMethodData::Wallet(wallet_data) => match wallet_data { + api_models::payments::WalletData::GooglePay(payment_method_data) => { + let gpay_object = Encode::::encode_to_string_of_json( + &BluesnapGooglePayObject { + payment_method_data: utils::GooglePayWalletData::from( + payment_method_data, + ), + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::GooglePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(gpay_object), + ), + }), + None, + )) + } + api_models::payments::WalletData::ApplePay(payment_method_data) => { + let apple_pay_payment_data = payment_method_data + .get_applepay_decoded_payment_data() + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + let apple_pay_payment_data: ApplePayEncodedPaymentData = apple_pay_payment_data + .expose()[..] + .as_bytes() + .parse_struct("ApplePayEncodedPaymentData") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let billing = item + .router_data + .address + .billing + .to_owned() + .get_required_value("billing") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let billing_address = billing + .address + .get_required_value("billing_address") + .change_context(errors::ConnectorError::MissingRequiredField { + field_name: "billing", + })?; + + let mut address = Vec::new(); + if let Some(add) = billing_address.line1.to_owned() { + address.push(add) } - api_models::payments::WalletData::ApplePay(payment_method_data) => { - let apple_pay_payment_data = payment_method_data - .get_applepay_decoded_payment_data() - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - let apple_pay_payment_data: ApplePayEncodedPaymentData = - apple_pay_payment_data.expose()[..] - .as_bytes() - .parse_struct("ApplePayEncodedPaymentData") - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - let billing = item - .router_data - .address - .billing - .to_owned() - .get_required_value("billing") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let billing_address = billing - .address - .get_required_value("billing_address") - .change_context(errors::ConnectorError::MissingRequiredField { - field_name: "billing", - })?; - - let mut address = Vec::new(); - if let Some(add) = billing_address.line1.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line2.to_owned() { - address.push(add) - } - if let Some(add) = billing_address.line3.to_owned() { - address.push(add) - } - - let apple_pay_object = - Encode::::encode_to_string_of_json( - &EncodedPaymentToken { - token: ApplepayPaymentData { - payment_data: apple_pay_payment_data, - payment_method: payment_method_data - .payment_method - .to_owned() - .into(), - transaction_identifier: payment_method_data - .transaction_identifier, - }, - billing_contact: BillingDetails { - country_code: billing_address.country, - address_lines: Some(address), - family_name: billing_address.last_name.to_owned(), - given_name: billing_address.first_name.to_owned(), - postal_code: billing_address.zip, - }, - }, - ) - .change_context(errors::ConnectorError::RequestEncodingFailed)?; - - Ok(( - PaymentMethodDetails::Wallet(BluesnapWallet { - wallet_type: BluesnapWalletTypes::ApplePay, - encoded_payment_token: Secret::new( - consts::BASE64_ENGINE.encode(apple_pay_object), - ), - }), - get_card_holder_info( - item.router_data.get_billing_address()?, - item.router_data.request.get_email()?, - )?, - )) + if let Some(add) = billing_address.line2.to_owned() { + address.push(add) } - payments::WalletData::AliPayQr(_) - | payments::WalletData::AliPayRedirect(_) - | payments::WalletData::AliPayHkRedirect(_) - | payments::WalletData::MomoRedirect(_) - | payments::WalletData::KakaoPayRedirect(_) - | payments::WalletData::GoPayRedirect(_) - | payments::WalletData::GcashRedirect(_) - | payments::WalletData::ApplePayRedirect(_) - | payments::WalletData::ApplePayThirdPartySdk(_) - | payments::WalletData::DanaRedirect {} - | payments::WalletData::GooglePayRedirect(_) - | payments::WalletData::GooglePayThirdPartySdk(_) - | payments::WalletData::MbWayRedirect(_) - | payments::WalletData::MobilePayRedirect(_) - | payments::WalletData::PaypalRedirect(_) - | payments::WalletData::PaypalSdk(_) - | payments::WalletData::SamsungPay(_) - | payments::WalletData::TwintRedirect {} - | payments::WalletData::VippsRedirect {} - | payments::WalletData::TouchNGoRedirect(_) - | payments::WalletData::WeChatPayRedirect(_) - | payments::WalletData::CashappQr(_) - | payments::WalletData::SwishQr(_) - | payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("bluesnap"), - )) + if let Some(add) = billing_address.line3.to_owned() { + address.push(add) } - }, - payments::PaymentMethodData::PayLater(_) - | payments::PaymentMethodData::BankRedirect(_) - | payments::PaymentMethodData::BankDebit(_) - | payments::PaymentMethodData::BankTransfer(_) - | payments::PaymentMethodData::Crypto(_) - | payments::PaymentMethodData::MandatePayment - | payments::PaymentMethodData::Reward - | payments::PaymentMethodData::Upi(_) - | payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + + let apple_pay_object = Encode::::encode_to_string_of_json( + &EncodedPaymentToken { + token: ApplepayPaymentData { + payment_data: apple_pay_payment_data, + payment_method: payment_method_data + .payment_method + .to_owned() + .into(), + transaction_identifier: payment_method_data.transaction_identifier, + }, + billing_contact: BillingDetails { + country_code: billing_address.country, + address_lines: Some(address), + family_name: billing_address.last_name.to_owned(), + given_name: billing_address.first_name.to_owned(), + postal_code: billing_address.zip, + }, + }, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + Ok(( + PaymentMethodDetails::Wallet(BluesnapWallet { + wallet_type: BluesnapWalletTypes::ApplePay, + encoded_payment_token: Secret::new( + consts::BASE64_ENGINE.encode(apple_pay_object), + ), + }), + get_card_holder_info( + item.router_data.get_billing_address()?, + item.router_data.request.get_email()?, + )?, + )) + } + payments::WalletData::AliPayQr(_) + | payments::WalletData::AliPayRedirect(_) + | payments::WalletData::AliPayHkRedirect(_) + | payments::WalletData::MomoRedirect(_) + | payments::WalletData::KakaoPayRedirect(_) + | payments::WalletData::GoPayRedirect(_) + | payments::WalletData::GcashRedirect(_) + | payments::WalletData::ApplePayRedirect(_) + | payments::WalletData::ApplePayThirdPartySdk(_) + | payments::WalletData::DanaRedirect {} + | payments::WalletData::GooglePayRedirect(_) + | payments::WalletData::GooglePayThirdPartySdk(_) + | payments::WalletData::MbWayRedirect(_) + | payments::WalletData::MobilePayRedirect(_) + | payments::WalletData::PaypalRedirect(_) + | payments::WalletData::PaypalSdk(_) + | payments::WalletData::SamsungPay(_) + | payments::WalletData::TwintRedirect {} + | payments::WalletData::VippsRedirect {} + | payments::WalletData::TouchNGoRedirect(_) + | payments::WalletData::WeChatPayRedirect(_) + | payments::WalletData::CashappQr(_) + | payments::WalletData::SwishQr(_) + | payments::WalletData::WeChatPayQr(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("bluesnap"), )) } - }?; + }, + payments::PaymentMethodData::PayLater(_) + | payments::PaymentMethodData::BankRedirect(_) + | payments::PaymentMethodData::BankDebit(_) + | payments::PaymentMethodData::BankTransfer(_) + | payments::PaymentMethodData::Crypto(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::Reward + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::Voucher(_) + | payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("bluesnap"), + )), + }?; Ok(Self { amount: item.amount.to_owned(), payment_method, diff --git a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs index 5069a9fe38d2..009177e961e7 100644 --- a/crates/router/src/connector/braintree/braintree_graphql_transformers.rs +++ b/crates/router/src/connector/braintree/braintree_graphql_transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&BraintreeRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("braintree"), ) @@ -879,12 +880,11 @@ impl TryFrom<&types::TokenizationRouterData> for BraintreeTokenRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("braintree"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("braintree"), + ) + .into()), } } } @@ -1423,9 +1423,10 @@ fn get_braintree_redirect_form( | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => Err( - errors::ConnectorError::NotImplemented("given payment method".to_owned()), - )?, + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "given payment method".to_owned(), + ))?, }, }) } diff --git a/crates/router/src/connector/checkout/transformers.rs b/crates/router/src/connector/checkout/transformers.rs index 90e65c8b0474..173ac0b8f585 100644 --- a/crates/router/src/connector/checkout/transformers.rs +++ b/crates/router/src/connector/checkout/transformers.rs @@ -138,7 +138,8 @@ impl TryFrom<&types::TokenizationRouterData> for TokenRequest { | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("checkout"), ) @@ -375,11 +376,10 @@ impl TryFrom<&CheckoutRouterData<&types::PaymentsAuthorizeRouterData>> for Payme | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("checkout"), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("checkout"), + )), }?; let three_ds = match item.router_data.auth_type { diff --git a/crates/router/src/connector/cryptopay/transformers.rs b/crates/router/src/connector/cryptopay/transformers.rs index 0bc4ff3b3ae6..446da0761d1f 100644 --- a/crates/router/src/connector/cryptopay/transformers.rs +++ b/crates/router/src/connector/cryptopay/transformers.rs @@ -80,7 +80,8 @@ impl TryFrom<&CryptopayRouterData<&types::PaymentsAuthorizeRouterData>> | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "CryptoPay", diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs index 33b8fa56d00e..656c45b6d6b6 100644 --- a/crates/router/src/connector/cybersource/transformers.rs +++ b/crates/router/src/connector/cybersource/transformers.rs @@ -367,7 +367,8 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsAuthorizeRouterData>> | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Cybersource"), ))? diff --git a/crates/router/src/connector/dlocal/transformers.rs b/crates/router/src/connector/dlocal/transformers.rs index 668a335cce88..a9033e53d666 100644 --- a/crates/router/src/connector/dlocal/transformers.rs +++ b/crates/router/src/connector/dlocal/transformers.rs @@ -168,7 +168,8 @@ impl TryFrom<&DlocalRouterData<&types::PaymentsAuthorizeRouterData>> for DlocalP | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( crate::connector::utils::get_unimplemented_payment_method_error_message("Dlocal"), ))?, } diff --git a/crates/router/src/connector/forte/transformers.rs b/crates/router/src/connector/forte/transformers.rs index dd78324c9b8b..2197b4558a20 100644 --- a/crates/router/src/connector/forte/transformers.rs +++ b/crates/router/src/connector/forte/transformers.rs @@ -112,7 +112,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for FortePaymentsRequest { | api_models::payments::PaymentMethodData::Reward {} | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Forte", diff --git a/crates/router/src/connector/gocardless/transformers.rs b/crates/router/src/connector/gocardless/transformers.rs index 72204b511518..63e199657af0 100644 --- a/crates/router/src/connector/gocardless/transformers.rs +++ b/crates/router/src/connector/gocardless/transformers.rs @@ -108,7 +108,8 @@ impl TryFrom<&types::ConnectorCustomerRouterData> for GocardlessCustomerRequest | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Gocardless"), )) @@ -297,12 +298,11 @@ impl TryFrom<&types::TokenizationRouterData> for CustomerBankAccount { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Gocardless"), - ) - .into()) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Gocardless"), + ) + .into()), } } } @@ -483,11 +483,10 @@ impl TryFrom<&types::SetupMandateRouterData> for GocardlessMandateRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotImplemented( - "Setup Mandate flow for selected payment method through Gocardless".to_string(), - )) - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( + "Setup Mandate flow for selected payment method through Gocardless".to_string(), + )), }?; let payment_method_token = item.get_payment_method_token()?; let customer_bank_account = match payment_method_token { diff --git a/crates/router/src/connector/helcim/transformers.rs b/crates/router/src/connector/helcim/transformers.rs index 9510ff6e67ad..9f405e2e2ea1 100644 --- a/crates/router/src/connector/helcim/transformers.rs +++ b/crates/router/src/connector/helcim/transformers.rs @@ -141,7 +141,8 @@ impl TryFrom<&types::SetupMandateRouterData> for HelcimVerifyRequest { | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Helcim", @@ -223,12 +224,11 @@ impl TryFrom<&HelcimRouterData<&types::PaymentsAuthorizeRouterData>> for HelcimP | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { - Err(errors::ConnectorError::NotSupported { - message: format!("{:?}", item.router_data.request.payment_method_data), - connector: "Helcim", - })? - } + | api_models::payments::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: format!("{:?}", item.router_data.request.payment_method_data), + connector: "Helcim", + })?, } } } diff --git a/crates/router/src/connector/klarna.rs b/crates/router/src/connector/klarna.rs index 3bd3407c3aef..91eaf94c01ee 100644 --- a/crates/router/src/connector/klarna.rs +++ b/crates/router/src/connector/klarna.rs @@ -406,7 +406,8 @@ impl | api_payments::PaymentMethodData::Reward | api_payments::PaymentMethodData::Upi(_) | api_payments::PaymentMethodData::Voucher(_) - | api_payments::PaymentMethodData::GiftCard(_) => Err(error_stack::report!( + | api_payments::PaymentMethodData::GiftCard(_) + | api_payments::PaymentMethodData::CardToken(_) => Err(error_stack::report!( errors::ConnectorError::MismatchedPaymentData )), } diff --git a/crates/router/src/connector/multisafepay/transformers.rs b/crates/router/src/connector/multisafepay/transformers.rs index a067818b743d..1780b77379c7 100644 --- a/crates/router/src/connector/multisafepay/transformers.rs +++ b/crates/router/src/connector/multisafepay/transformers.rs @@ -365,7 +365,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; @@ -509,7 +510,8 @@ impl TryFrom<&MultisafepayRouterData<&types::PaymentsAuthorizeRouterData>> | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("multisafepay"), ))?, }; diff --git a/crates/router/src/connector/nexinets/transformers.rs b/crates/router/src/connector/nexinets/transformers.rs index 2af3ee0a1bb8..15cbe9a7e28e 100644 --- a/crates/router/src/connector/nexinets/transformers.rs +++ b/crates/router/src/connector/nexinets/transformers.rs @@ -624,7 +624,8 @@ fn get_payment_details_and_product( | PaymentMethodData::Reward | PaymentMethodData::Upi(_) | PaymentMethodData::Voucher(_) - | PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | PaymentMethodData::GiftCard(_) + | PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nexinets"), ))?, } diff --git a/crates/router/src/connector/nmi/transformers.rs b/crates/router/src/connector/nmi/transformers.rs index c8721d0d8f6b..ff3a1e6a1c54 100644 --- a/crates/router/src/connector/nmi/transformers.rs +++ b/crates/router/src/connector/nmi/transformers.rs @@ -188,7 +188,8 @@ impl TryFrom<&api_models::payments::PaymentMethodData> for PaymentMethod { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "nmi", }) diff --git a/crates/router/src/connector/noon/transformers.rs b/crates/router/src/connector/noon/transformers.rs index 5ff92582051a..ee3a8ba8c532 100644 --- a/crates/router/src/connector/noon/transformers.rs +++ b/crates/router/src/connector/noon/transformers.rs @@ -284,7 +284,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for NoonPaymentsRequest { | api::PaymentMethodData::Reward {} | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: conn_utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Noon", diff --git a/crates/router/src/connector/nuvei/transformers.rs b/crates/router/src/connector/nuvei/transformers.rs index b79b2c892643..36244b8bc0d8 100644 --- a/crates/router/src/connector/nuvei/transformers.rs +++ b/crates/router/src/connector/nuvei/transformers.rs @@ -856,8 +856,9 @@ impl | payments::PaymentMethodData::Reward | payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::CardRedirect(_) - | payments::PaymentMethodData::GiftCard(_) => { + | payments::PaymentMethodData::CardRedirect(_) + | payments::PaymentMethodData::GiftCard(_) + | payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), ) @@ -1037,6 +1038,7 @@ impl TryFrom<(&types::PaymentsCompleteAuthorizeRouterData, String)> for NuveiPay | Some(api::PaymentMethodData::CardRedirect(..)) | Some(api::PaymentMethodData::Reward) | Some(api::PaymentMethodData::Upi(..)) + | Some(api::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("nuvei"), )), diff --git a/crates/router/src/connector/opayo/transformers.rs b/crates/router/src/connector/opayo/transformers.rs index 41bcc1500ed1..5e9fb066c78d 100644 --- a/crates/router/src/connector/opayo/transformers.rs +++ b/crates/router/src/connector/opayo/transformers.rs @@ -52,7 +52,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for OpayoPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Opayo"), ) .into()), diff --git a/crates/router/src/connector/payeezy/transformers.rs b/crates/router/src/connector/payeezy/transformers.rs index 817ab43ac717..90c58c3a9bce 100644 --- a/crates/router/src/connector/payeezy/transformers.rs +++ b/crates/router/src/connector/payeezy/transformers.rs @@ -260,7 +260,8 @@ fn get_payment_method_data( | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Payeezy"), ))?, } diff --git a/crates/router/src/connector/payme/transformers.rs b/crates/router/src/connector/payme/transformers.rs index 092a8b49fd86..e751de20e219 100644 --- a/crates/router/src/connector/payme/transformers.rs +++ b/crates/router/src/connector/payme/transformers.rs @@ -431,7 +431,8 @@ impl TryFrom<&PaymentMethodData> for SalePaymentMethod { | PaymentMethodData::GiftCard(_) | PaymentMethodData::CardRedirect(_) | PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => { + | PaymentMethodData::Voucher(_) + | PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Payment methods".to_string()).into()) } } @@ -666,7 +667,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PayRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("payme"), ))?, } @@ -725,6 +727,7 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for Pay3dsRequest { | Some(api::PaymentMethodData::Upi(_)) | Some(api::PaymentMethodData::Voucher(_)) | Some(api::PaymentMethodData::GiftCard(_)) + | Some(api::PaymentMethodData::CardToken(_)) | None => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } @@ -761,7 +764,8 @@ impl TryFrom<&types::TokenizationRouterData> for CaptureBuyerRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented("Tokenize Flow".to_string()).into()) } } diff --git a/crates/router/src/connector/paypal.rs b/crates/router/src/connector/paypal.rs index 4e50bc924b33..9ab19b295570 100644 --- a/crates/router/src/connector/paypal.rs +++ b/crates/router/src/connector/paypal.rs @@ -30,6 +30,7 @@ use crate::{ types::{ self, api::{self, CompleteAuthorize, ConnectorCommon, ConnectorCommonExt, VerifyWebhookSource}, + storage::enums as storage_enums, transformers::ForeignFrom, ConnectorAuthType, ErrorResponse, Response, }, @@ -506,6 +507,161 @@ impl ConnectorIntegration for Paypal +{ + fn get_headers( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_url( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + let order_id = req + .request + .connector_transaction_id + .to_owned() + .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?; + Ok(format!( + "{}v2/checkout/orders/{}?fields=payment_source", + self.base_url(connectors), + order_id, + )) + } + + fn build_request( + &self, + req: &types::PaymentsPreProcessingRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Get) + .url(&types::PaymentsPreProcessingType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(types::PaymentsPreProcessingType::get_headers( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &types::PaymentsPreProcessingRouterData, + res: Response, + ) -> CustomResult { + let response: paypal::PaypalPreProcessingResponse = res + .response + .parse_struct("paypal PaypalPreProcessingResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + // permutation for status to continue payment + match ( + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .as_ref(), + response + .payment_source + .card + .authentication_result + .liability_shift + .clone(), + ) { + ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Success), + paypal::LiabilityShift::Possible, + ) + | ( + Some(paypal::EnrollementStatus::Ready), + Some(paypal::AuthenticationStatus::Attempted), + paypal::LiabilityShift::Possible, + ) + | (Some(paypal::EnrollementStatus::NotReady), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Unavailable), None, paypal::LiabilityShift::No) + | (Some(paypal::EnrollementStatus::Bypassed), None, paypal::LiabilityShift::No) => { + Ok(types::PaymentsPreProcessingRouterData { + status: storage_enums::AttemptStatus::AuthenticationSuccessful, + response: Ok(types::PaymentsResponseData::TransactionResponse { + resource_id: types::ResponseId::NoResponseId, + redirection_data: None, + mandate_reference: None, + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: None, + }), + ..data.clone() + }) + } + _ => Ok(types::PaymentsPreProcessingRouterData { + response: Err(ErrorResponse { + attempt_status: Some(enums::AttemptStatus::Failure), + code: consts::NO_ERROR_CODE.to_string(), + message: consts::NO_ERROR_MESSAGE.to_string(), + connector_transaction_id: None, + reason: Some(format!("{} Connector Responsded with LiabilityShift: {:?}, EnrollmentStatus: {:?}, and AuthenticationStatus: {:?}", + consts::CANNOT_CONTINUE_AUTH, + response + .payment_source + .card + .authentication_result + .liability_shift, + response + .payment_source + .card + .authentication_result + .three_d_secure + .enrollment_status + .unwrap_or(paypal::EnrollementStatus::Null), + response + .payment_source + .card + .authentication_result + .three_d_secure + .authentication_status + .unwrap_or(paypal::AuthenticationStatus::Null), + )), + status_code: res.status_code, + }), + ..data.clone() + }), + } + } + + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + impl ConnectorIntegration< CompleteAuthorize, diff --git a/crates/router/src/connector/paypal/transformers.rs b/crates/router/src/connector/paypal/transformers.rs index d023077ff008..04328cead233 100644 --- a/crates/router/src/connector/paypal/transformers.rs +++ b/crates/router/src/connector/paypal/transformers.rs @@ -584,7 +584,8 @@ impl TryFrom<&PaypalRouterData<&types::PaymentsAuthorizeRouterData>> for PaypalP } api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Crypto(_) - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Paypal", @@ -924,6 +925,74 @@ pub struct PaypalThreeDsResponse { links: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalPreProcessingResponse { + pub payment_source: CardParams, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CardParams { + pub card: AuthResult, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AuthResult { + pub authentication_result: PaypalThreeDsParams, +} +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PaypalThreeDsParams { + pub liability_shift: LiabilityShift, + pub three_d_secure: ThreeDsCheck, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ThreeDsCheck { + pub enrollment_status: Option, + pub authentication_status: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "UPPERCASE")] +pub enum LiabilityShift { + Possible, + No, + Unknown, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum EnrollementStatus { + Null, + #[serde(rename = "Y")] + Ready, + #[serde(rename = "N")] + NotReady, + #[serde(rename = "U")] + Unavailable, + #[serde(rename = "B")] + Bypassed, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum AuthenticationStatus { + Null, + #[serde(rename = "Y")] + Success, + #[serde(rename = "N")] + Failed, + #[serde(rename = "R")] + Rejected, + #[serde(rename = "A")] + Attempted, + #[serde(rename = "U")] + Unable, + #[serde(rename = "C")] + ChallengeRequired, + #[serde(rename = "I")] + InfoOnly, + #[serde(rename = "D")] + Decoupled, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PaypalOrdersResponse { id: String, diff --git a/crates/router/src/connector/powertranz/transformers.rs b/crates/router/src/connector/powertranz/transformers.rs index 7f62c1939c07..a631a126ed3f 100644 --- a/crates/router/src/connector/powertranz/transformers.rs +++ b/crates/router/src/connector/powertranz/transformers.rs @@ -113,7 +113,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PowertranzPaymentsRequest | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "powertranz", }) diff --git a/crates/router/src/connector/shift4/transformers.rs b/crates/router/src/connector/shift4/transformers.rs index 0dd3b8583490..c272a5b6fc12 100644 --- a/crates/router/src/connector/shift4/transformers.rs +++ b/crates/router/src/connector/shift4/transformers.rs @@ -166,11 +166,14 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Shift4", + | payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::CardToken(_) => { + Err(errors::ConnectorError::NotSupported { + message: utils::SELECTED_PAYMENT_METHOD.to_string(), + connector: "Shift4", + } + .into()) } - .into()), } } } @@ -397,6 +400,7 @@ impl TryFrom<&types::RouterData Err(errors::ConnectorError::NotSupported { message: "Flow".to_string(), connector: "Shift4", diff --git a/crates/router/src/connector/square/transformers.rs b/crates/router/src/connector/square/transformers.rs index dfb49e8e6775..6024a20fa6ab 100644 --- a/crates/router/src/connector/square/transformers.rs +++ b/crates/router/src/connector/square/transformers.rs @@ -191,7 +191,8 @@ impl TryFrom<&types::TokenizationRouterData> for SquareTokenRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, @@ -307,7 +308,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for SquarePaymentsRequest { | api::PaymentMethodData::MandatePayment | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{:?}", item.request.payment_method_data), connector: "Square", })?, diff --git a/crates/router/src/connector/stax/transformers.rs b/crates/router/src/connector/stax/transformers.rs index f2aae442ddd6..bb37bf1fc9e7 100644 --- a/crates/router/src/connector/stax/transformers.rs +++ b/crates/router/src/connector/stax/transformers.rs @@ -118,7 +118,8 @@ impl TryFrom<&StaxRouterData<&types::PaymentsAuthorizeRouterData>> for StaxPayme | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: "SELECTED_PAYMENT_METHOD".to_string(), connector: "Stax", })?, @@ -268,7 +269,8 @@ impl TryFrom<&types::TokenizationRouterData> for StaxTokenRequest { | api::PaymentMethodData::Voucher(_) | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Upi(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Upi(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: "SELECTED_PAYMENT_METHOD".to_string(), connector: "Stax", })?, diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs index 56eebc2df3bd..ae7fe59be96c 100644 --- a/crates/router/src/connector/stripe/transformers.rs +++ b/crates/router/src/connector/stripe/transformers.rs @@ -1431,13 +1431,13 @@ fn create_stripe_payment_method( .into()), }, - payments::PaymentMethodData::Upi(_) | payments::PaymentMethodData::MandatePayment => { - Err(errors::ConnectorError::NotSupported { - message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), - connector: "stripe", - } - .into()) + payments::PaymentMethodData::Upi(_) + | payments::PaymentMethodData::MandatePayment + | payments::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { + message: connector_util::SELECTED_PAYMENT_METHOD.to_string(), + connector: "stripe", } + .into()), } } @@ -2995,6 +2995,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for StripeCreditTransferSo | Some(payments::PaymentMethodData::GiftCard(..)) | Some(payments::PaymentMethodData::CardRedirect(..)) | Some(payments::PaymentMethodData::Voucher(..)) + | Some(payments::PaymentMethodData::CardToken(..)) | None => Err(errors::ConnectorError::NotImplemented( connector_util::get_unimplemented_payment_method_error_message("stripe"), ) @@ -3416,7 +3417,8 @@ impl | api::PaymentMethodData::GiftCard(_) | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::CardRedirect(_) - | api::PaymentMethodData::Voucher(_) => Err(errors::ConnectorError::NotSupported { + | api::PaymentMethodData::Voucher(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotSupported { message: format!("{pm_type:?}"), connector: "Stripe", })?, diff --git a/crates/router/src/connector/trustpay/transformers.rs b/crates/router/src/connector/trustpay/transformers.rs index 0210d3ca2d92..e891501d6d0a 100644 --- a/crates/router/src/connector/trustpay/transformers.rs +++ b/crates/router/src/connector/trustpay/transformers.rs @@ -445,7 +445,8 @@ impl TryFrom<&TrustpayRouterData<&types::PaymentsAuthorizeRouterData>> for Trust | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("trustpay"), ) .into()), diff --git a/crates/router/src/connector/tsys/transformers.rs b/crates/router/src/connector/tsys/transformers.rs index c60aeb64898b..863b754fc89c 100644 --- a/crates/router/src/connector/tsys/transformers.rs +++ b/crates/router/src/connector/tsys/transformers.rs @@ -77,7 +77,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for TsysPaymentsRequest { | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("tsys"), ))?, } diff --git a/crates/router/src/connector/volt/transformers.rs b/crates/router/src/connector/volt/transformers.rs index e603ef2db06c..efed7c797c76 100644 --- a/crates/router/src/connector/volt/transformers.rs +++ b/crates/router/src/connector/volt/transformers.rs @@ -148,7 +148,8 @@ impl TryFrom<&VoltRouterData<&types::PaymentsAuthorizeRouterData>> for VoltPayme | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotSupported { message: utils::SELECTED_PAYMENT_METHOD.to_string(), connector: "Volt", diff --git a/crates/router/src/connector/worldline/transformers.rs b/crates/router/src/connector/worldline/transformers.rs index 049453e325ae..282e1b3a8adb 100644 --- a/crates/router/src/connector/worldline/transformers.rs +++ b/crates/router/src/connector/worldline/transformers.rs @@ -257,7 +257,8 @@ impl | api::PaymentMethodData::Reward | api::PaymentMethodData::Upi(_) | api::PaymentMethodData::Voucher(_) - | api::PaymentMethodData::GiftCard(_) => Err(errors::ConnectorError::NotImplemented( + | api::PaymentMethodData::GiftCard(_) + | api::PaymentMethodData::CardToken(_) => Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldline"), ))?, }; diff --git a/crates/router/src/connector/worldpay/transformers.rs b/crates/router/src/connector/worldpay/transformers.rs index d31f4d65e78c..e35a51552c03 100644 --- a/crates/router/src/connector/worldpay/transformers.rs +++ b/crates/router/src/connector/worldpay/transformers.rs @@ -120,7 +120,8 @@ fn fetch_payment_instrument( | api_models::payments::PaymentMethodData::Upi(_) | api_models::payments::PaymentMethodData::Voucher(_) | api_models::payments::PaymentMethodData::CardRedirect(_) - | api_models::payments::PaymentMethodData::GiftCard(_) => { + | api_models::payments::PaymentMethodData::GiftCard(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("worldpay"), ) diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index 689894176b26..64f6d5bf1a07 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -707,7 +707,8 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment api_models::payments::PaymentMethodData::Crypto(_) | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward - | api_models::payments::PaymentMethodData::Upi(_) => { + | api_models::payments::PaymentMethodData::Upi(_) + | api_models::payments::PaymentMethodData::CardToken(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ))? diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index c5490ee00e63..4f19562c83ce 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -27,7 +27,10 @@ pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; pub(crate) const UNSUPPORTED_ERROR_MESSAGE: &str = "Unsupported response type"; +pub(crate) const LOW_BALANCE_ERROR_MESSAGE: &str = "Insufficient balance in the payment method"; pub(crate) const CONNECTOR_UNAUTHORIZED_ERROR: &str = "Authentication Error from the connector"; +pub(crate) const CANNOT_CONTINUE_AUTH: &str = + "Cannot continue with Authorization due to failed Liability Shift."; // General purpose base64 engines pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose = diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index a429cab482b4..cff2dc8e58f1 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -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; diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs new file mode 100644 index 000000000000..1ea9454f00a0 --- /dev/null +++ b/crates/router/src/core/currency.rs @@ -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, 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, + 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)?, + )) +} diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 80cec01e9166..1049137a9470 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -3,6 +3,7 @@ pub mod surcharge_decision_configs; pub mod transformers; pub mod vault; +use api_models::payments::CardToken; pub use api_models::{ enums::{Connector, PayoutConnectors}, payouts as payout_types, @@ -42,6 +43,7 @@ pub trait PaymentMethodRetrieve { token: &storage::PaymentTokenData, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult>; } @@ -125,6 +127,7 @@ impl PaymentMethodRetrieve for Oss { token_data: &storage::PaymentTokenData, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult> { match token_data { storage::PaymentTokenData::TemporaryGeneric(generic_token) => { @@ -134,6 +137,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, card_cvc, merchant_key_store, + card_token_data, ) .await } @@ -145,6 +149,7 @@ impl PaymentMethodRetrieve for Oss { payment_intent, card_cvc, merchant_key_store, + card_token_data, ) .await } @@ -155,6 +160,7 @@ impl PaymentMethodRetrieve for Oss { &card_token.token, payment_intent, card_cvc, + card_token_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))) @@ -166,6 +172,7 @@ impl PaymentMethodRetrieve for Oss { &card_token.token, payment_intent, card_cvc, + card_token_data, ) .await .map(|card| Some((card, enums::PaymentMethod::Card))) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 1c40ef81f497..8cfded8463eb 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1408,6 +1408,17 @@ where (router_data, should_continue_payment) } } + Some(api_models::payments::PaymentMethodData::GiftCard(_)) => { + if connector.connector_name == router_types::Connector::Adyen { + router_data = router_data.preprocessing_steps(state, connector).await?; + + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } Some(api_models::payments::PaymentMethodData::BankDebit(_)) => { if connector.connector_name == router_types::Connector::Gocardless { router_data = router_data.preprocessing_steps(state, connector).await?; @@ -1418,7 +1429,21 @@ where (router_data, should_continue_payment) } } - _ => (router_data, should_continue_payment), + _ => { + // 3DS validation for paypal cards after verification (authorize call) + if connector.connector_name == router_types::Connector::Paypal + && payment_data.payment_attempt.payment_method + == Some(storage_enums::PaymentMethod::Card) + && matches!(format!("{operation:?}").as_str(), "CompleteAuthorize") + { + router_data = router_data.preprocessing_steps(state, connector).await?; + let is_error_in_response = router_data.response.is_err(); + // If is_error_in_response is true, should_continue_payment should be false, we should throw the error + (router_data, !is_error_in_response) + } else { + (router_data, should_continue_payment) + } + } }; Ok(router_data_and_should_continue_payment) diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 46eaca26f7cc..9be6f5905b8b 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -832,7 +832,6 @@ impl default_imp_for_pre_processing_steps!( connector::Aci, - connector::Adyen, connector::Airwallex, connector::Authorizedotnet, connector::Bambora, @@ -863,7 +862,6 @@ default_imp_for_pre_processing_steps!( connector::Opayo, connector::Opennode, connector::Payeezy, - connector::Paypal, connector::Payu, connector::Powertranz, connector::Prophetpay, diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 04bd7f0b4338..4ef23f481a2c 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -417,6 +417,30 @@ impl TryFrom for types::PaymentsPreProcessingData complete_authorize_url: data.complete_authorize_url, browser_info: data.browser_info, surcharge_details: data.surcharge_details, + connector_transaction_id: None, + }) + } +} + +impl TryFrom for types::PaymentsPreProcessingData { + type Error = error_stack::Report; + + fn try_from(data: types::CompleteAuthorizeData) -> Result { + Ok(Self { + payment_method_data: data.payment_method_data, + amount: Some(data.amount), + email: data.email, + currency: Some(data.currency), + payment_method_type: None, + setup_mandate_details: data.setup_mandate_details, + capture_method: data.capture_method, + order_details: None, + router_return_url: None, + webhook_url: None, + complete_authorize_url: None, + browser_info: data.browser_info, + surcharge_details: None, + connector_transaction_id: data.connector_transaction_id, }) } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 44d8728fd4d2..2d52a145feae 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -6,7 +6,7 @@ use crate::{ errors::{self, ConnectorErrorExt, RouterResult}, payments::{self, access_token, helpers, transformers, PaymentData}, }, - routes::AppState, + routes::{metrics, AppState}, services, types::{self, api, domain}, utils::OptionExt, @@ -144,6 +144,76 @@ impl Feature Ok((request, true)) } + + async fn preprocessing_steps<'a>( + self, + state: &AppState, + connector: &api::ConnectorData, + ) -> RouterResult { + complete_authorize_preprocessing_steps(state, &self, true, connector).await + } +} + +pub async fn complete_authorize_preprocessing_steps( + state: &AppState, + router_data: &types::RouterData, + confirm: bool, + connector: &api::ConnectorData, +) -> RouterResult> { + if confirm { + let connector_integration: services::BoxedConnectorIntegration< + '_, + api::PreProcessing, + types::PaymentsPreProcessingData, + types::PaymentsResponseData, + > = connector.connector.get_connector_integration(); + + let preprocessing_request_data = + types::PaymentsPreProcessingData::try_from(router_data.request.to_owned())?; + + let preprocessing_response_data: Result = + Err(types::ErrorResponse::default()); + + let preprocessing_router_data = + payments::helpers::router_data_type_conversion::<_, api::PreProcessing, _, _, _, _>( + router_data.clone(), + preprocessing_request_data, + preprocessing_response_data, + ); + + let resp = services::execute_connector_processing_step( + state, + connector_integration, + &preprocessing_router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + + metrics::PREPROCESSING_STEPS_COUNT.add( + &metrics::CONTEXT, + 1, + &[ + metrics::request::add_attributes("connector", connector.connector_name.to_string()), + metrics::request::add_attributes( + "payment_method", + router_data.payment_method.to_string(), + ), + ], + ); + + let authorize_router_data = + payments::helpers::router_data_type_conversion::<_, F, _, _, _, _>( + resp.clone(), + router_data.request.to_owned(), + resp.response, + ); + + Ok(authorize_router_data) + } else { + Ok(router_data.clone()) + } } impl TryFrom for types::PaymentMethodTokenizationData { diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index f57c0640f1a8..266792f98758 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use api_models::payments::GetPaymentMethodType; +use api_models::payments::{CardToken, GetPaymentMethodType}; use base64::Engine; use common_utils::{ ext_traits::{AsyncExt, ByteSliceExt, ValueExt}, @@ -1356,6 +1356,7 @@ pub async fn retrieve_payment_method_with_temporary_token( payment_intent: &PaymentIntent, card_cvc: Option>, merchant_key_store: &domain::MerchantKeyStore, + card_token_data: Option<&CardToken>, ) -> RouterResult> { let (pm, supplementary_data) = vault::Vault::get_payment_method_data_from_locker(state, token, merchant_key_store) @@ -1375,9 +1376,29 @@ pub async fn retrieve_payment_method_with_temporary_token( Ok::<_, error_stack::Report>(match pm { Some(api::PaymentMethodData::Card(card)) => { + let mut updated_card = card.clone(); + let mut is_card_updated = false; + + let name_on_card = if card.card_holder_name.clone().expose().is_empty() { + card_token_data + .and_then(|token_data| { + is_card_updated = true; + token_data.card_holder_name.clone() + }) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "card_holder_name", + })? + } else { + card.card_holder_name.clone() + }; + updated_card.card_holder_name = name_on_card; + if let Some(cvc) = card_cvc { - let mut updated_card = card; + is_card_updated = true; updated_card.card_cvc = cvc; + } + if is_card_updated { let updated_pm = api::PaymentMethodData::Card(updated_card); vault::Vault::store_payment_method_data_in_locker( state, @@ -1423,6 +1444,7 @@ pub async fn retrieve_card_with_permanent_token( token: &str, payment_intent: &PaymentIntent, card_cvc: Option>, + card_token_data: Option<&CardToken>, ) -> RouterResult { let customer_id = payment_intent .customer_id @@ -1437,13 +1459,23 @@ pub async fn retrieve_card_with_permanent_token( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("failed to fetch card information from the permanent locker")?; + let name_on_card = if let Some(name_on_card) = card.name_on_card.clone() { + if card.name_on_card.unwrap_or_default().expose().is_empty() { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + } else { + Some(name_on_card) + } + } else { + card_token_data + .and_then(|token_data| token_data.card_holder_name.clone()) + .filter(|name_on_card| !name_on_card.clone().expose().is_empty()) + }; + let api_card = api::Card { card_number: card.card_number, - card_holder_name: card - .name_on_card - .get_required_value("name_on_card") - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("card holder name was not saved in permanent locker")?, + card_holder_name: name_on_card.unwrap_or(masking::Secret::from("".to_string())), card_exp_month: card.card_exp_month, card_exp_year: card.card_exp_year, card_cvc: card_cvc.unwrap_or_default(), @@ -1529,6 +1561,11 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( let card_cvc = payment_data.card_cvc.clone(); + let card_token_data = request.as_ref().and_then(|pmd| match pmd { + api_models::payments::PaymentMethodData::CardToken(token_data) => Some(token_data), + _ => None, + }); + // TODO: Handle case where payment method and token both are present in request properly. let payment_method = match (request, hyperswitch_token) { (_, Some(hyperswitch_token)) => { @@ -1538,6 +1575,7 @@ pub async fn make_pm_data<'a, F: Clone, R, Ctx: PaymentMethodRetrieve>( &hyperswitch_token, &payment_data.payment_intent, card_cvc, + card_token_data, ) .await .attach_printable("in 'make_pm_data'")?; @@ -3316,6 +3354,9 @@ pub async fn get_additional_payment_data( api_models::payments::PaymentMethodData::GiftCard(_) => { api_models::payments::AdditionalPaymentData::GiftCard {} } + api_models::payments::PaymentMethodData::CardToken(_) => { + api_models::payments::AdditionalPaymentData::CardToken {} + } } } @@ -3615,6 +3656,12 @@ pub fn get_key_params_for_surcharge_details( gift_card.get_payment_method_type(), None, )), + api_models::payments::PaymentMethodData::CardToken(_) => { + Err(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payment_method_data", + } + .into()) + } } } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index f395c023128c..000bbb0fc00b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1428,6 +1428,7 @@ impl TryFrom> for types::PaymentsPreProce complete_authorize_url, browser_info, surcharge_details: payment_data.surcharge_details, + connector_transaction_id: payment_data.payment_attempt.connector_transaction_id, }) } } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 0bc8e492c40c..2b1f9c692d86 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -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())) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index d191890b8cdb..cfb0268a9f80 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -73,11 +73,11 @@ Never share your secret api keys. Keep them guarded and secure. // crate::routes::admin::retrieve_merchant_account, // crate::routes::admin::update_merchant_account, // crate::routes::admin::delete_merchant_account, - // crate::routes::admin::payment_connector_create, - // crate::routes::admin::payment_connector_retrieve, - // crate::routes::admin::payment_connector_list, - // crate::routes::admin::payment_connector_update, - // crate::routes::admin::payment_connector_delete, + crate::routes::admin::payment_connector_create, + crate::routes::admin::payment_connector_retrieve, + crate::routes::admin::payment_connector_list, + crate::routes::admin::payment_connector_update, + crate::routes::admin::payment_connector_delete, crate::routes::mandates::get_mandate, crate::routes::mandates::revoke_mandate, crate::routes::payments::payments_create, @@ -248,6 +248,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::OnlineMandate, api_models::payments::Card, api_models::payments::CardRedirectData, + api_models::payments::CardToken, api_models::payments::CustomerAcceptance, api_models::payments::PaymentsRequest, api_models::payments::PaymentsCreateRequest, diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 5166e326fb91..37cc1339e1a1 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -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")] diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 0586faabbf76..ce6a2a97e28d 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -190,7 +190,7 @@ pub async fn delete_merchant_account( ) .await } -/// PaymentsConnectors - Create +/// Merchant Connector - Create /// /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." #[utoipa::path( diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 84848e030120..ae0e0f04f598 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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; diff --git a/crates/router/src/routes/currency.rs b/crates/router/src/routes/currency.rs new file mode 100644 index 000000000000..1e1858517176 --- /dev/null +++ b/crates/router/src/routes/currency.rs @@ -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, 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, + req: HttpRequest, + params: web::Query, +) -> 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 +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 219948bdd4d2..5c2ad123749c 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -23,6 +23,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Forex, RustLockerMigration, Gsm, User, @@ -51,6 +52,8 @@ impl From for ApiIdentifier { | Flow::DecisionManagerRetrieveConfig | Flow::DecisionManagerUpsertConfig => Self::Routing, + Flow::RetrieveForexFlow => Self::Forex, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 8c9d030965c9..cd37fbb549d9 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -323,7 +323,7 @@ pub struct ApplePayCryptogramData { #[derive(Debug, Clone)] pub struct PaymentMethodBalance { pub amount: i64, - pub currency: String, + pub currency: storage_enums::Currency, } #[cfg(feature = "payouts")] @@ -442,6 +442,7 @@ pub struct PaymentsPreProcessingData { pub complete_authorize_url: Option, pub surcharge_details: Option, pub browser_info: Option, + pub connector_transaction_id: Option, } #[derive(Debug, Clone)] diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 45aad93371e2..5bd28db3c158 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -522,7 +522,8 @@ impl ForeignTryFrom for api_enums::Paym payment_method_data: api_models::payments::PaymentMethodData, ) -> Result { match payment_method_data { - api_models::payments::PaymentMethodData::Card(..) => Ok(Self::Card), + api_models::payments::PaymentMethodData::Card(..) + | api_models::payments::PaymentMethodData::CardToken(..) => Ok(Self::Card), api_models::payments::PaymentMethodData::Wallet(..) => Ok(Self::Wallet), api_models::payments::PaymentMethodData::PayLater(..) => Ok(Self::PayLater), api_models::payments::PaymentMethodData::BankRedirect(..) => Ok(Self::BankRedirect), diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 901e84997e67..c936ee858c17 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -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; diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs new file mode 100644 index 000000000000..118d9df28e22 --- /dev/null +++ b/crates/router/src/utils/currency.rs @@ -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, + timestamp: i64, +} + +static FX_EXCHANGE_RATES_CACHE: Lazy>> = + 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, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +struct FallbackForexResponse { + pub quotes: HashMap, +} + +#[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 { + 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 { + 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 for ExchangeRates { + type Error = error_stack::Report; + fn try_from(value: DefaultExchangeRates) -> Result { + let mut conversion_usable: HashMap = 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 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 { + 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 { + 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, + #[cfg(feature = "kms")] kms_config: &kms::KmsConfig, +) -> CustomResult { + 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 { + 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 { + 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 { + 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> { + #[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::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", forex_response); + + let mut conversions: HashMap = 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 { + #[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::() + .await + .into_report() + .change_context(ForexCacheError::ParsingError)?; + + logger::info!("{:?}", fallback_forex_response); + let mut conversions: HashMap = 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> { + 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 { + 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, 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> { + 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 { + 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(), + }) +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 7978e98e52c0..2a174f42eb63 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -163,6 +163,8 @@ pub enum Flow { RefundsUpdate, /// Refunds list flow. RefundsList, + // Retrieve forex flow. + RetrieveForexFlow, /// Routing create flow, RoutingCreateConfig, /// Routing link config diff --git a/crates/storage_impl/src/config.rs b/crates/storage_impl/src/config.rs index ceed3da81b39..f53507831b11 100644 --- a/crates/storage_impl/src/config.rs +++ b/crates/storage_impl/src/config.rs @@ -10,4 +10,6 @@ pub struct Database { pub pool_size: u32, pub connection_timeout: u64, pub queue_strategy: bb8::QueueStrategy, + pub min_idle: Option, + pub max_lifetime: Option, } diff --git a/crates/storage_impl/src/database/store.rs b/crates/storage_impl/src/database/store.rs index a09f1b752561..c36575e37c97 100644 --- a/crates/storage_impl/src/database/store.rs +++ b/crates/storage_impl/src/database/store.rs @@ -88,8 +88,10 @@ pub async fn diesel_make_pg_pool( let manager = async_bb8_diesel::ConnectionManager::::new(database_url); let mut pool = bb8::Pool::builder() .max_size(database.pool_size) + .min_idle(database.min_idle) .queue_strategy(database.queue_strategy) - .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)); + .connection_timeout(std::time::Duration::from_secs(database.connection_timeout)) + .max_lifetime(database.max_lifetime.map(std::time::Duration::from_secs)); if test_transaction { pool = pool.connection_customizer(Box::new(TestTransaction)); diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 2fb729fb7b90..bec1074b99d0 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -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 diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 88a0d115ff01..86dc053d2d77 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -129,6 +129,259 @@ ] } }, + "/accounts/{account_id}/connectors": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - List", + "description": "Merchant Connector - List\n\nList Merchant Connector Details for the merchant", + "operationId": "List all Merchant Connectors", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector list retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Create", + "description": "Merchant Connector - Create\n\nCreate a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc.\"", + "operationId": "Create a Merchant Connector", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "400": { + "description": "Missing Mandatory fields" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, + "/accounts/{account_id}/connectors/{connector_id}": { + "get": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Retrieve", + "description": "Merchant Connector - Retrieve\n\nRetrieve Merchant Connector Details", + "operationId": "Retrieve a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "post": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Update", + "description": "Merchant Connector - Update\n\nTo update an existing Merchant Connector. Helpful in enabling / disabling different payment methods and other settings for the connector etc.", + "operationId": "Update a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Merchant Connector Updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + }, + "delete": { + "tags": [ + "Merchant Connector Account" + ], + "summary": "Merchant Connector - Delete", + "description": "Merchant Connector - Delete\n\nDelete or Detach a Merchant Connector from Merchant Account", + "operationId": "Delete a Merchant Connector", + "parameters": [ + { + "name": "account_id", + "in": "path", + "description": "The unique identifier for the merchant account", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "connector_id", + "in": "path", + "description": "The unique identifier for the Merchant Connector", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "responses": { + "200": { + "description": "Merchant Connector Deleted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MerchantConnectorDeleteResponse" + } + } + } + }, + "401": { + "description": "Unauthorized request" + }, + "404": { + "description": "Merchant Connector does not exist in records" + } + }, + "security": [ + { + "admin_api_key": [] + } + ] + } + }, "/customers": { "post": { "tags": [ @@ -4053,6 +4306,19 @@ } ] }, + "CardToken": { + "type": "object", + "required": [ + "card_holder_name" + ], + "properties": { + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Test" + } + } + }, "CashappQr": { "type": "object" }, @@ -8657,6 +8923,17 @@ "$ref": "#/components/schemas/GiftCardData" } } + }, + { + "type": "object", + "required": [ + "card_token" + ], + "properties": { + "card_token": { + "$ref": "#/components/schemas/CardToken" + } + } } ] }, diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js new file mode 100644 index 000000000000..c48d8e2d054e --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json new file mode 100644 index 000000000000..0915e9894bb6 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 1100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 1100, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..0652a2d92fd4 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Succeeded" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'succeeded'", + function () { + pm.expect(jsonData.status).to.eql("succeeded"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Happy Cases/Scenario22-Create Gift Card payment/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json index 592cff807510..fe25f6f5e682 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payment Connector - Create/request.json @@ -190,6 +190,18 @@ } ] }, + { + "payment_method": "gift_card", + "payment_method_types": [ + { + "payment_method_type": "givex", + "minimum_amount": 1, + "maximum_amount": 68607706, + "recurring_enabled": true, + "installment_payment_enabled": true + } + ] + }, { "payment_method": "bank_redirect", "payment_method_types": [ diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json index 8ac3ed14b0a7..ed9dbeaa9c49 100644 --- a/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json +++ b/postman/collection-dir/adyen_uk/Flow Testcases/QuickStart/Payments - Create/request.json @@ -43,6 +43,10 @@ "card_cvc": "7373" } }, + "routing": { + "type": "single", + "data": "adyen" + }, "billing": { "address": { "line1": "1467", diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json new file mode 100644 index 000000000000..69b505c6d863 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/.meta.json @@ -0,0 +1,3 @@ +{ + "childrenOrder": ["Payments - Create", "Payments - Retrieve"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js new file mode 100644 index 000000000000..601f4f8fa7f5 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/event.test.js @@ -0,0 +1,81 @@ +// Validate status 2xx +pm.test("[POST]::/payments - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[POST]::/payments - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[POST]::/payments - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} + +// Response body should have error message as "Insufficient balance in the payment method" +if (jsonData?.error_message) { + pm.test( + "[POST]::/payments - Content check if value for 'error_message' matches 'Insufficient balance in the payment method'", + function () { + pm.expect(jsonData.error_message).to.eql("Insufficient balance in the payment method"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json new file mode 100644 index 000000000000..11437ff57659 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/request.json @@ -0,0 +1,88 @@ +{ + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "options": { + "raw": { + "language": "json" + } + }, + "raw_json_formatted": { + "amount": 14100, + "currency": "EUR", + "confirm": true, + "capture_method": "automatic", + "capture_on": "2022-09-10T10:11:12Z", + "amount_to_capture": 14100, + "customer_id": "StripeCustomer", + "email": "guest@example.com", + "name": "John Doe", + "phone": "999999999", + "phone_country_code": "+65", + "description": "Its my first payment request", + "authentication_type": "no_three_ds", + "return_url": "https://duck.com", + "payment_method": "gift_card", + "payment_method_type": "givex", + "payment_method_data": { + "gift_card": { + "givex": { + "number": "6364530000000000", + "cvc": "122222" + } + } + }, + "routing": { + "type": "single", + "data": "adyen" + }, + "billing": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "shipping": { + "address": { + "line1": "1467", + "line2": "Harrison Street", + "line3": "Harrison Street", + "city": "San Fransico", + "state": "California", + "zip": "94122", + "country": "US", + "first_name": "PiX" + } + }, + "statement_descriptor_name": "joseph", + "statement_descriptor_suffix": "JS", + "metadata": { + "udf1": "value1", + "new_customer": "true", + "login_date": "2019-09-10T10:11:12Z" + } + } + }, + "url": { + "raw": "{{baseUrl}}/payments", + "host": ["{{baseUrl}}"], + "path": ["payments"] + }, + "description": "To process a payment you will have to create a payment, attach a payment method and confirm. Depending on the user journey you wish to achieve, you may opt to all the steps in a single request or in a sequence of API request using following APIs: (i) Payments - Update, (ii) Payments - Confirm, and (iii) Payments - Capture" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Create/response.json @@ -0,0 +1 @@ +[] diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json new file mode 100644 index 000000000000..0731450e6b25 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/.event.meta.json @@ -0,0 +1,3 @@ +{ + "eventOrder": ["event.test.js"] +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js new file mode 100644 index 000000000000..ff2099305d7a --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/event.test.js @@ -0,0 +1,71 @@ +// Validate status 2xx +pm.test("[GET]::/payments/:id - Status code is 2xx", function () { + pm.response.to.be.success; +}); + +// Validate if response header has matching content-type +pm.test("[GET]::/payments/:id - Content-Type is application/json", function () { + pm.expect(pm.response.headers.get("Content-Type")).to.include( + "application/json", + ); +}); + +// Validate if response has JSON Body +pm.test("[GET]::/payments/:id - Response has JSON Body", function () { + pm.response.to.have.jsonBody(); +}); + +// Set response object as internal variable +let jsonData = {}; +try { + jsonData = pm.response.json(); +} catch (e) {} + +// pm.collectionVariables - Set payment_id as variable for jsonData.payment_id +if (jsonData?.payment_id) { + pm.collectionVariables.set("payment_id", jsonData.payment_id); + console.log( + "- use {{payment_id}} as collection variable for value", + jsonData.payment_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{payment_id}}, as jsonData.payment_id is undefined.", + ); +} + +// pm.collectionVariables - Set mandate_id as variable for jsonData.mandate_id +if (jsonData?.mandate_id) { + pm.collectionVariables.set("mandate_id", jsonData.mandate_id); + console.log( + "- use {{mandate_id}} as collection variable for value", + jsonData.mandate_id, + ); +} else { + console.log( + "INFO - Unable to assign variable {{mandate_id}}, as jsonData.mandate_id is undefined.", + ); +} + +// pm.collectionVariables - Set client_secret as variable for jsonData.client_secret +if (jsonData?.client_secret) { + pm.collectionVariables.set("client_secret", jsonData.client_secret); + console.log( + "- use {{client_secret}} as collection variable for value", + jsonData.client_secret, + ); +} else { + console.log( + "INFO - Unable to assign variable {{client_secret}}, as jsonData.client_secret is undefined.", + ); +} + +// Response body should have value "Failed" for "status" +if (jsonData?.status) { + pm.test( + "[POST]::/payments/:id - Content check if value for 'status' matches 'failed'", + function () { + pm.expect(jsonData.status).to.eql("failed"); + }, + ); +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json new file mode 100644 index 000000000000..6cd4b7d96c52 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/request.json @@ -0,0 +1,28 @@ +{ + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/payments/:id?force_sync=true", + "host": ["{{baseUrl}}"], + "path": ["payments", ":id"], + "query": [ + { + "key": "force_sync", + "value": "true" + } + ], + "variable": [ + { + "key": "id", + "value": "{{payment_id}}", + "description": "(Required) unique payment id" + } + ] + }, + "description": "To retrieve the properties of a Payment. This may be used to get the status of a previously initiated payment or next action for an ongoing payment" +} diff --git a/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json new file mode 100644 index 000000000000..fe51488c7066 --- /dev/null +++ b/postman/collection-dir/adyen_uk/Flow Testcases/Variation Cases/Scenario10-Create Gift Card payment where it fails due to insufficient balance/Payments - Retrieve/response.json @@ -0,0 +1 @@ +[]