diff --git a/.typos.toml b/.typos.toml index 0d6e6fd8e38c..1ac38a005c9e 100644 --- a/.typos.toml +++ b/.typos.toml @@ -40,4 +40,5 @@ afe = "afe" # Commit id extend-exclude = [ "config/redis.conf", # `typos` also checked "AKE" in the file, which is present as a quoted string "openapi/open_api_spec.yaml", # no longer updated + "crates/router/src/utils/user/blocker_emails.txt", # this file contains various email domains ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 412b42afc2eb..e5da650def02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,63 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.78.0 (2023-11-14) + +### Features + +- **router:** Add automatic retries and step up 3ds flow ([#2834](https://github.com/juspay/hyperswitch/pull/2834)) ([`d2968c9`](https://github.com/juspay/hyperswitch/commit/d2968c94978a57422fa46a8195d906736a95b864)) +- Payment link status page UI ([#2740](https://github.com/juspay/hyperswitch/pull/2740)) ([`856c7af`](https://github.com/juspay/hyperswitch/commit/856c7af77e17599ca0d4d119744ac582e9c3c971)) + +### Bug Fixes + +- Handle session and confirm flow discrepancy in surcharge details ([#2696](https://github.com/juspay/hyperswitch/pull/2696)) ([`cafea45`](https://github.com/juspay/hyperswitch/commit/cafea45982d7b520fe68fde967984ce88f68c6c0)) + +**Full Changelog:** [`v1.77.0...v1.78.0`](https://github.com/juspay/hyperswitch/compare/v1.77.0...v1.78.0) + +- - - + + +## 1.77.0 (2023-11-13) + +### Features + +- **apievent:** Added hs latency to api event ([#2734](https://github.com/juspay/hyperswitch/pull/2734)) ([`c124511`](https://github.com/juspay/hyperswitch/commit/c124511052ed8911a2ccfcf648c0793b5c1ca690)) +- **router:** + - Add new JWT authentication variants and use them ([#2835](https://github.com/juspay/hyperswitch/pull/2835)) ([`f88eee7`](https://github.com/juspay/hyperswitch/commit/f88eee7362be2cc3e8e8dc2bb7bfd263892ff01e)) + - Profile specific fallback derivation while routing payments ([#2806](https://github.com/juspay/hyperswitch/pull/2806)) ([`8e538db`](https://github.com/juspay/hyperswitch/commit/8e538dbd5c189047d0a0b24fa752b9a1c67554f5)) + +### Build System / Dependencies + +- **deps:** Remove unused dependencies and features ([#2854](https://github.com/juspay/hyperswitch/pull/2854)) ([`0553587`](https://github.com/juspay/hyperswitch/commit/05535871152f4a6ac24ce6b5b5390da13cc29b96)) + +**Full Changelog:** [`v1.76.0...v1.77.0`](https://github.com/juspay/hyperswitch/compare/v1.76.0...v1.77.0) + +- - - + + +## 1.76.0 (2023-11-12) + +### Features + +- **analytics:** Analytics APIs ([#2792](https://github.com/juspay/hyperswitch/pull/2792)) ([`f847802`](https://github.com/juspay/hyperswitch/commit/f847802339bfedb24cbaa47ad55e31d80cefddca)) +- **router:** Added Payment link new design ([#2731](https://github.com/juspay/hyperswitch/pull/2731)) ([`2a4f5d1`](https://github.com/juspay/hyperswitch/commit/2a4f5d13717a78dc2e2e4fc9a492a45b92151dbe)) +- **user:** Setup user tables ([#2803](https://github.com/juspay/hyperswitch/pull/2803)) ([`20c4226`](https://github.com/juspay/hyperswitch/commit/20c4226a36e4650a3ba8811b758ac5f7969bcfb3)) + +### Refactors + +- **connector:** [Zen] change error message from NotSupported to NotImplemented ([#2831](https://github.com/juspay/hyperswitch/pull/2831)) ([`b5ea8db`](https://github.com/juspay/hyperswitch/commit/b5ea8db2d2b7e7544931704a7191b42d3a8299be)) +- **core:** Remove connector response table and use payment_attempt instead ([#2644](https://github.com/juspay/hyperswitch/pull/2644)) ([`966369b`](https://github.com/juspay/hyperswitch/commit/966369b6f2c205b59524c23ad3b21ebab547631f)) +- **events:** Update api events to follow snake case naming ([#2828](https://github.com/juspay/hyperswitch/pull/2828)) ([`b3d5062`](https://github.com/juspay/hyperswitch/commit/b3d5062dc07676ec12e903b1999fdd9138c0891d)) + +### Documentation + +- **README:** Add bootstrap button for cloudformation deployment ([#2827](https://github.com/juspay/hyperswitch/pull/2827)) ([`e67e808`](https://github.com/juspay/hyperswitch/commit/e67e808d70d41c371fff168824e5a4dbb8b3a040)) + +**Full Changelog:** [`v1.75.0...v1.76.0`](https://github.com/juspay/hyperswitch/compare/v1.75.0...v1.76.0) + +- - - + + ## 1.75.0 (2023-11-09) ### Features diff --git a/Cargo.lock b/Cargo.lock index c96ce2c18258..1574933810b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,30 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "actix" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f728064aca1c318585bf4bb04ffcfac9e75e508ab4e8b1bd9ba5dfe04e2cbed5" -dependencies = [ - "actix-rt", - "actix_derive", - "bitflags 1.3.2", - "bytes", - "crossbeam-channel", - "futures-core", - "futures-sink", - "futures-task", - "futures-util", - "log", - "once_cell", - "parking_lot 0.12.1", - "pin-project-lite", - "smallvec", - "tokio", - "tokio-util", -] - [[package]] name = "actix-codec" version = "0.5.1" @@ -282,17 +258,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "actix_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d44b8fee1ced9671ba043476deddef739dd0959bf77030b26b738cc591737a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "addr2line" version = "0.21.0" @@ -416,9 +381,7 @@ dependencies = [ "router_derive", "serde", "serde_json", - "serde_with", "strum 0.24.1", - "thiserror", "time", "url", "utoipa", @@ -436,6 +399,18 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f907281554a3d0312bb7aab855a8e0ef6cbf1614d06de54105039ca8b34460e" +[[package]] +name = "argon2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.7" @@ -1145,6 +1120,12 @@ dependencies = [ "vsimd", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bb8" version = "0.8.1" @@ -1205,6 +1186,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "blake3" version = "1.4.0" @@ -1534,7 +1524,6 @@ dependencies = [ "serde", "serde_json", "strum 0.25.0", - "time", "utoipa", ] @@ -1669,9 +1658,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" [[package]] name = "crc16" @@ -1897,7 +1886,6 @@ dependencies = [ "masking", "serde", "serde_json", - "strum 0.25.0", "thiserror", "time", ] @@ -2008,13 +1996,10 @@ name = "diesel_models" version = "0.1.0" dependencies = [ "async-bb8-diesel", - "aws-config", - "aws-sdk-s3", "common_enums", "common_utils", "diesel", "error-stack", - "external_services", "frunk", "frunk_core", "masking", @@ -3244,12 +3229,6 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3852614a3bd9ca9804678ba6be5e3b8ce76dfc902cae004e3e0c44051b6e88db" -[[package]] -name = "literally" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d2be3f5a0d4d5c983d1f8ecc2a87676a0875a14feb9eebf0675f7c3e2f3c35" - [[package]] name = "local-channel" version = "0.1.4" @@ -3854,6 +3833,17 @@ dependencies = [ "regex", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -4555,13 +4545,13 @@ dependencies = [ name = "router" version = "0.2.0" dependencies = [ - "actix", "actix-cors", "actix-http", "actix-multipart", "actix-rt", "actix-web", "api_models", + "argon2", "async-bb8-diesel", "async-trait", "awc", @@ -4596,7 +4586,6 @@ dependencies = [ "josekit", "jsonwebtoken", "kgraph_utils", - "literally", "masking", "maud", "mimalloc", @@ -4625,22 +4614,20 @@ dependencies = [ "serde_with", "serial_test", "sha-1 0.9.8", - "signal-hook", - "signal-hook-tokio", "sqlx", "storage_impl", "strum 0.24.1", "tera", "test_utils", - "thirtyfour", "thiserror", "time", "tokio", - "toml 0.7.4", + "unicode-segmentation", "url", "utoipa", "utoipa-swagger-ui", "uuid", + "validator", "wiremock", "x509-parser", ] @@ -4921,7 +4908,6 @@ dependencies = [ "router_env", "serde", "serde_json", - "signal-hook-tokio", "storage_impl", "strum 0.24.1", "thiserror", @@ -5455,7 +5441,6 @@ dependencies = [ "diesel_models", "dyn-clone", "error-stack", - "external_services", "futures", "http", "masking", @@ -5689,27 +5674,20 @@ dependencies = [ name = "test_utils" version = "0.1.0" dependencies = [ - "actix-http", - "actix-web", - "api_models", "async-trait", - "awc", "base64 0.21.4", "clap", - "derive_deref", "masking", "rand 0.8.5", "reqwest", "serde", "serde_json", - "serde_path_to_error", "serde_urlencoded", "serial_test", "thirtyfour", "time", "tokio", "toml 0.7.4", - "uuid", ] [[package]] @@ -6376,6 +6354,21 @@ dependencies = [ "serde", ] +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index d15fdeabf387..ce882e913282 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -14,7 +14,7 @@ connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] backwards_compatibility = ["connector_choice_bcompat"] connector_choice_mca_id = ["euclid/connector_choice_mca_id"] -dummy_connector = ["common_enums/dummy_connector", "euclid/dummy_connector"] +dummy_connector = ["euclid/dummy_connector"] detailed_errors = [] payouts = [] @@ -25,12 +25,10 @@ mime = "0.3.17" reqwest = { version = "0.11.18", optional = true } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -serde_with = "3.0.0" strum = { version = "0.24.1", 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"] } -thiserror = "1.0.40" # First party crates cards = { version = "0.1.0", path = "../cards" } diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 23e7c9dc706a..ad07340615b4 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -5,6 +5,7 @@ pub mod payment; pub mod payouts; pub mod refund; pub mod routing; +pub mod user; use common_utils::{ events::{ApiEventMetric, ApiEventsType}, diff --git a/crates/api_models/src/events/routing.rs b/crates/api_models/src/events/routing.rs index 5eca01acc6fb..a09735bc5722 100644 --- a/crates/api_models/src/events/routing.rs +++ b/crates/api_models/src/events/routing.rs @@ -1,8 +1,9 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::routing::{ - LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, RoutingAlgorithmId, - RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, + LinkedRoutingConfigRetrieveResponse, MerchantRoutingAlgorithm, ProfileDefaultRoutingConfig, + RoutingAlgorithmId, RoutingConfigRequest, RoutingDictionaryRecord, RoutingKind, + RoutingPayloadWrapper, }; #[cfg(feature = "business_profile_routing")] use crate::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; @@ -37,6 +38,17 @@ impl ApiEventMetric for LinkedRoutingConfigRetrieveResponse { } } +impl ApiEventMetric for RoutingPayloadWrapper { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} +impl ApiEventMetric for ProfileDefaultRoutingConfig { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Routing) + } +} + #[cfg(feature = "business_profile_routing")] impl ApiEventMetric for RoutingRetrieveQuery { fn get_api_event_type(&self) -> Option { diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs new file mode 100644 index 000000000000..2a896cc38776 --- /dev/null +++ b/crates/api_models/src/events/user.rs @@ -0,0 +1,14 @@ +use common_utils::events::{ApiEventMetric, ApiEventsType}; + +use crate::user::{ConnectAccountRequest, ConnectAccountResponse}; + +impl ApiEventMetric for ConnectAccountResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::User { + merchant_id: self.merchant_id.clone(), + user_id: self.user_id.clone(), + }) + } +} + +impl ApiEventMetric for ConnectAccountRequest {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 75509ed7386d..bcc3913ea824 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -21,5 +21,6 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +pub mod user; pub mod verifications; pub mod webhooks; diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 289f652981eb..755acbf7f425 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -6,7 +6,6 @@ use common_utils::{ types::Percentage, }; use serde::de; -use serde_with::serde_as; use utoipa::ToSchema; #[cfg(feature = "payouts")] @@ -15,7 +14,7 @@ use crate::{ admin, customers::CustomerId, enums as api_enums, - payments::{self, BankCodeResponse}, + payments::{self, BankCodeResponse, RequestSurchargeDetails}, }; #[derive(Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -342,15 +341,85 @@ pub struct SurchargeDetailsResponse { pub final_amount: i64, } -#[serde_as] -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +impl SurchargeDetailsResponse { + pub fn is_request_surcharge_matching( + &self, + request_surcharge_details: RequestSurchargeDetails, + ) -> bool { + request_surcharge_details.surcharge_amount == self.surcharge_amount + && request_surcharge_details.tax_amount.unwrap_or(0) == self.tax_on_surcharge_amount + } +} + +#[derive(Clone, Debug)] pub struct SurchargeMetadata { - #[serde_as(as = "HashMap<_, _>")] - pub surcharge_results: HashMap, + surcharge_results: HashMap< + ( + common_enums::PaymentMethod, + common_enums::PaymentMethodType, + Option, + ), + SurchargeDetailsResponse, + >, + pub payment_attempt_id: String, } impl SurchargeMetadata { - pub fn get_key_for_surcharge_details_hash_map( + pub fn new(payment_attempt_id: String) -> Self { + Self { + surcharge_results: HashMap::new(), + payment_attempt_id, + } + } + pub fn is_empty_result(&self) -> bool { + self.surcharge_results.is_empty() + } + pub fn get_surcharge_results_size(&self) -> usize { + self.surcharge_results.len() + } + pub fn insert_surcharge_details( + &mut self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + surcharge_details: SurchargeDetailsResponse, + ) { + let key = ( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.insert(key, surcharge_details); + } + pub fn get_surcharge_details( + &self, + payment_method: &common_enums::PaymentMethod, + payment_method_type: &common_enums::PaymentMethodType, + card_network: Option<&common_enums::CardNetwork>, + ) -> Option<&SurchargeDetailsResponse> { + let key = &( + payment_method.to_owned(), + payment_method_type.to_owned(), + card_network.cloned(), + ); + self.surcharge_results.get(key) + } + pub fn get_surcharge_metadata_redis_key(payment_attempt_id: &str) -> String { + format!("surcharge_metadata_{}", payment_attempt_id) + } + pub fn get_individual_surcharge_key_value_pairs( + &self, + ) -> Vec<(String, SurchargeDetailsResponse)> { + self.surcharge_results + .iter() + .map(|((pm, pmt, card_network), surcharge_details)| { + let key = + Self::get_surcharge_details_redis_hashset_key(pm, pmt, card_network.as_ref()); + (key, surcharge_details.to_owned()) + }) + .collect() + } + pub fn get_surcharge_details_redis_hashset_key( payment_method: &common_enums::PaymentMethod, payment_method_type: &common_enums::PaymentMethodType, card_network: Option<&common_enums::CardNetwork>, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 22579ed6d6ea..cf0259f26951 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -16,6 +16,7 @@ use crate::{ admin, disputes, enums::{self as api_enums}, ephemeral_key::EphemeralKeyCreateResponse, + payment_methods::{Surcharge, SurchargeDetailsResponse}, refunds, }; @@ -319,6 +320,23 @@ pub struct RequestSurchargeDetails { pub tax_amount: Option, } +impl RequestSurchargeDetails { + pub fn is_surcharge_zero(&self) -> bool { + self.surcharge_amount == 0 && self.tax_amount.unwrap_or(0) == 0 + } + pub fn get_surcharge_details_object(&self, original_amount: i64) -> SurchargeDetailsResponse { + let surcharge_amount = self.surcharge_amount; + let tax_on_surcharge_amount = self.tax_amount.unwrap_or(0); + SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(self.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount: original_amount + surcharge_amount + tax_on_surcharge_amount, + } + } +} + #[derive(Default, Debug, Clone, Copy)] pub struct HeaderPayload { pub payment_confirm_source: Option, @@ -810,6 +828,36 @@ pub enum PaymentMethodData { GiftCard(Box), } +impl PaymentMethodData { + pub fn get_payment_method_type_if_session_token_type( + &self, + ) -> Option { + match self { + Self::Wallet(wallet) => match wallet { + WalletData::ApplePay(_) => Some(api_enums::PaymentMethodType::ApplePay), + WalletData::GooglePay(_) => Some(api_enums::PaymentMethodType::GooglePay), + WalletData::PaypalSdk(_) => Some(api_enums::PaymentMethodType::Paypal), + _ => None, + }, + Self::PayLater(pay_later) => match pay_later { + PayLaterData::KlarnaSdk { .. } => Some(api_enums::PaymentMethodType::Klarna), + _ => None, + }, + Self::Card(_) + | Self::CardRedirect(_) + | Self::BankRedirect(_) + | Self::BankDebit(_) + | Self::BankTransfer(_) + | Self::Crypto(_) + | Self::MandatePayment + | Self::Reward + | Self::Upi(_) + | Self::Voucher(_) + | Self::GiftCard(_) => None, + } + } +} + pub trait GetPaymentMethodType { fn get_payment_method_type(&self) -> api_enums::PaymentMethodType; } diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 425ca364191d..363df5389a79 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -40,6 +40,12 @@ pub struct RoutingConfigRequest { pub profile_id: Option, } +#[derive(Debug, serde::Serialize)] +pub struct ProfileDefaultRoutingConfig { + pub profile_id: String, + pub connectors: Vec, +} + #[cfg(feature = "business_profile_routing")] #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct RoutingRetrieveQuery { @@ -389,6 +395,13 @@ pub enum RoutingAlgorithmKind { Advanced, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] + +pub struct RoutingPayloadWrapper { + pub updated_config: Vec, + pub profile_id: String, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde( tag = "type", diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs new file mode 100644 index 000000000000..91f7702c654e --- /dev/null +++ b/crates/api_models/src/user.rs @@ -0,0 +1,21 @@ +use common_utils::pii; +use masking::Secret; + +#[derive(serde::Deserialize, Debug, Clone, serde::Serialize)] +pub struct ConnectAccountRequest { + pub email: pii::Email, + pub password: Secret, +} + +#[derive(serde::Serialize, Debug, Clone)] +pub struct ConnectAccountResponse { + pub token: Secret, + pub merchant_id: String, + pub name: Secret, + pub email: pii::Email, + pub verification_days_left: Option, + pub user_role: String, + //this field is added for audit/debug reasons + #[serde(skip_serializing)] + pub user_id: String, +} diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index db37d27ab0f1..88628825ca64 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -7,15 +7,11 @@ rust-version.workspace = true readme = "README.md" license.workspace = true -[features] -dummy_connector = [] - [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" strum = { version = "0.25", features = ["derive"] } -time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } utoipa = { version = "3.3.0", features = ["preserve_order"] } # First party crates diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 62bd747da1b0..3619c93d772c 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -23,6 +23,7 @@ http = "0.2.9" md5 = "0.7.0" nanoid = "0.4.0" once_cell = "1.18.0" +phonenumber = "0.3.3" quick-xml = { version = "0.28.2", features = ["serialize"] } rand = "0.8.5" regex = "1.8.4" @@ -37,12 +38,11 @@ strum = { version = "0.24.1", features = ["derive"] } thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"], optional = true } -phonenumber = "0.3.3" # First party crates +common_enums = { version = "0.1.0", path = "../common_enums" } masking = { version = "0.1.0", path = "../masking" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"], optional = true } -common_enums = { version = "0.1.0", path = "../common_enums" } [target.'cfg(not(target_os = "windows"))'.dependencies] signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"], optional = true } diff --git a/crates/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 254c194182f3..57ae1ec1ec87 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -8,16 +8,15 @@ readme = "README.md" license.workspace = true [features] -default = ["olap", "oltp"] -oltp = [] +default = ["olap"] olap = [] [dependencies] # First party deps api_models = { version = "0.1.0", path = "../api_models" } -masking = { version = "0.1.0", path = "../masking" } common_enums = { version = "0.1.0", path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } +masking = { version = "0.1.0", path = "../masking" } # Third party deps @@ -25,6 +24,5 @@ async-trait = "0.1.68" error-stack = "0.3.1" serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.25", features = [ "derive" ] } thiserror = "1.0.40" -time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } \ No newline at end of file +time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index cdd41ea9db2d..88fc7b3b524a 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -224,6 +224,8 @@ pub enum PaymentAttemptUpdate { business_sub_label: Option, amount_to_capture: Option, capture_method: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, UpdateTrackers { @@ -231,6 +233,8 @@ pub enum PaymentAttemptUpdate { connector: Option, straight_through_algorithm: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -255,8 +259,6 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -285,6 +287,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index 1a0bdfe5674e..9521c690366f 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -9,15 +9,10 @@ license.workspace = true [features] default = ["kv_store"] -email = ["external_services/email", "dep:aws-config"] -kms = ["external_services/kms", "dep:aws-config"] kv_store = [] -s3 = ["dep:aws-sdk-s3", "dep:aws-config"] [dependencies] async-bb8-diesel = "0.1.0" -aws-config = { version = "0.55.3", optional = true } -aws-sdk-s3 = { version = "0.28.0", optional = true } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64-column-tables"] } error-stack = "0.3.1" frunk = "0.4.1" @@ -31,7 +26,6 @@ time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } # First party crates common_enums = { path = "../common_enums" } common_utils = { version = "0.1.0", path = "../common_utils" } -external_services = { version = "0.1.0", path = "../external_services" } masking = { version = "0.1.0", path = "../masking" } router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index ce388fea10eb..cd976b9e19db 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -141,6 +141,8 @@ pub enum PaymentAttemptUpdate { business_sub_label: Option, amount_to_capture: Option, capture_method: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, }, UpdateTrackers { @@ -148,6 +150,8 @@ pub enum PaymentAttemptUpdate { connector: Option, straight_through_algorithm: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -172,8 +176,6 @@ pub enum PaymentAttemptUpdate { error_code: Option>, error_message: Option>, amount_capturable: Option, - surcharge_amount: Option, - tax_amount: Option, updated_by: String, merchant_connector_id: Option, }, @@ -202,6 +204,8 @@ pub enum PaymentAttemptUpdate { error_reason: Option>, connector_response_reference_id: Option, amount_capturable: Option, + surcharge_amount: Option, + tax_amount: Option, updated_by: String, authentication_data: Option, encoded_data: Option, @@ -370,6 +374,8 @@ impl From for PaymentAttemptUpdateInternal { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => Self { amount: Some(amount), @@ -386,6 +392,8 @@ impl From for PaymentAttemptUpdateInternal { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, ..Default::default() }, @@ -415,8 +423,6 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id, } => Self { @@ -437,8 +443,6 @@ impl From for PaymentAttemptUpdateInternal { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id, ..Default::default() @@ -479,6 +483,8 @@ impl From for PaymentAttemptUpdateInternal { error_reason, connector_response_reference_id, amount_capturable, + surcharge_amount, + tax_amount, updated_by, authentication_data, encoded_data, @@ -498,6 +504,8 @@ impl From for PaymentAttemptUpdateInternal { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, ..Default::default() @@ -531,6 +539,8 @@ impl From for PaymentAttemptUpdateInternal { connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, } => Self { @@ -538,6 +548,8 @@ impl From for PaymentAttemptUpdateInternal { connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, ..Default::default() diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 3bf056a69b38..56bebdce6b86 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" license.workspace = true [features] -release = ["kms","vergen"] +release = ["kms", "vergen"] kms = ["external_services/kms"] vergen = ["router_env/vergen"] @@ -28,11 +28,11 @@ tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } # First Party Crates common_utils = { version = "0.1.0", path = "../common_utils", features = ["signals"] } +diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } external_services = { version = "0.1.0", path = "../external_services" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } -diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } diff --git a/crates/euclid/Cargo.toml b/crates/euclid/Cargo.toml index f0e24b1ff63c..859795964145 100644 --- a/crates/euclid/Cargo.toml +++ b/crates/euclid/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true rust-version.workspace = true [dependencies] +erased-serde = "0.3.28" frunk = "0.4.1" frunk_core = "0.4.1" nom = { version = "7.1.3", features = ["alloc"], optional = true } @@ -13,7 +14,6 @@ once_cell = "1.18.0" rustc-hash = "1.1.0" serde = { version = "1.0.163", features = ["derive", "rc"] } serde_json = "1.0.96" -erased-serde = "0.3.28" strum = { version = "0.25", features = ["derive"] } thiserror = "1.0.43" @@ -24,10 +24,8 @@ euclid_macros = { version = "0.1.0", path = "../euclid_macros" } [features] ast_parser = ["dep:nom"] valued_jit = [] -connector_choice_bcompat = [] connector_choice_mca_id = [] dummy_connector = [] -backwards_compatibility = ["connector_choice_bcompat"] [dev-dependencies] criterion = "0.5" diff --git a/crates/euclid_wasm/Cargo.toml b/crates/euclid_wasm/Cargo.toml index 90489eb78bf6..4fc8cd970f40 100644 --- a/crates/euclid_wasm/Cargo.toml +++ b/crates/euclid_wasm/Cargo.toml @@ -10,28 +10,21 @@ rust-version.workspace = true crate-type = ["cdylib"] [features] -default = ["connector_choice_bcompat", "payouts"] -connector_choice_bcompat = [ - "euclid/connector_choice_bcompat", - "api_models/connector_choice_bcompat", - "kgraph_utils/backwards_compatibility" -] -connector_choice_mca_id = [ - "api_models/connector_choice_mca_id", - "euclid/connector_choice_mca_id", - "kgraph_utils/connector_choice_mca_id" -] +default = ["connector_choice_bcompat"] +connector_choice_bcompat = ["api_models/connector_choice_bcompat"] +connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] dummy_connector = ["kgraph_utils/dummy_connector"] -payouts = [] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } euclid = { path = "../euclid", features = [] } kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } + +# Third party crates getrandom = { version = "0.2.10", features = ["js"] } once_cell = "1.18.0" +ron-parser = "0.1.4" serde = { version = "1.0", features = [] } serde-wasm-bindgen = "0.5" strum = { version = "0.25", features = ["derive"] } wasm-bindgen = { version = "0.2.86" } -ron-parser = "0.1.4" diff --git a/crates/kgraph_utils/Cargo.toml b/crates/kgraph_utils/Cargo.toml index fa90b3974c20..cd0adf0bc8af 100644 --- a/crates/kgraph_utils/Cargo.toml +++ b/crates/kgraph_utils/Cargo.toml @@ -7,14 +7,14 @@ rust-version.workspace = true [features] dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector"] -backwards_compatibility = ["euclid/backwards_compatibility", "euclid/backwards_compatibility"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id"] [dependencies] api_models = { version = "0.1.0", path = "../api_models", package = "api_models" } euclid = { version = "0.1.0", path = "../euclid" } -masking = { version = "0.1.0", path = "../masking/"} +masking = { version = "0.1.0", path = "../masking/" } +# Third party crates serde = "1.0.163" serde_json = "1.0.96" thiserror = "1.0.43" diff --git a/crates/redis_interface/Cargo.toml b/crates/redis_interface/Cargo.toml index 8066787dcae2..9d3ae724d432 100644 --- a/crates/redis_interface/Cargo.toml +++ b/crates/redis_interface/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true [dependencies] error-stack = "0.3.1" -fred = { version = "6.3.0", features = ["metrics", "partial-tracing","subscriber-client"] } +fred = { version = "6.3.0", features = ["metrics", "partial-tracing", "subscriber-client"] } futures = "0.3" serde = { version = "1.0.163", features = ["derive"] } thiserror = "1.0.40" diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index d53fd1625fe4..ca85d19d38b0 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -248,7 +248,7 @@ impl super::RedisConnectionPool { &self, key: &str, values: V, - ttl: Option, + ttl: Option, ) -> CustomResult<(), errors::RedisError> where V: TryInto + Debug + Send + Sync, @@ -260,11 +260,10 @@ impl super::RedisConnectionPool { .await .into_report() .change_context(errors::RedisError::SetHashFailed); - // setting expiry for the key output .async_and_then(|_| { - self.set_expiry(key, ttl.unwrap_or(self.config.default_hash_ttl).into()) + self.set_expiry(key, ttl.unwrap_or(self.config.default_hash_ttl.into())) }) .await } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 7456944a8e4e..4d9c315a10b0 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,35 +9,35 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config"] basilisk = ["kms"] stripe = ["dep:serde_qs"] -release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "olap"] +release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] -oltp = ["data_models/oltp", "storage_impl/oltp"] +oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] accounts_cache = [] openapi = ["olap", "oltp", "payouts"] vergen = ["router_env/vergen"] -backwards_compatibility = ["api_models/backwards_compatibility", "euclid/backwards_compatibility", "kgraph_utils/backwards_compatibility"] -business_profile_routing=["api_models/business_profile_routing"] +backwards_compatibility = ["api_models/backwards_compatibility"] +business_profile_routing = ["api_models/business_profile_routing"] +profile_specific_fallback_routing = [] dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "kgraph_utils/dummy_connector"] connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] -api_locking = [] - +retry = [] [dependencies] -actix = "0.13.0" actix-cors = "0.6.4" actix-multipart = "0.6.0" actix-rt = "2.8.0" actix-web = "4.3.1" +argon2 = { version = "0.5.0", features = ["std"] } async-bb8-diesel = "0.1.0" async-trait = "0.1.68" aws-config = { version = "0.55.3", optional = true } @@ -50,6 +50,7 @@ bytes = "1.4.0" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } config = { version = "0.13.3", features = ["toml"] } diesel = { version = "2.1.0", features = ["postgres"] } +digest = "0.9" dyn-clone = "1.0.11" encoding_rs = "0.8.32" error-stack = "0.3.1" @@ -61,13 +62,13 @@ image = "0.23.14" infer = "0.13.0" josekit = "0.8.3" jsonwebtoken = "8.3.0" -literally = "0.1.3" maud = { version = "0.25", features = ["actix-web"] } mimalloc = { version = "0.1", optional = true } mime = "0.3.17" nanoid = "0.4.0" num_cpus = "1.15.0" once_cell = "1.18.0" +openssl = "0.10.55" qrcode = "0.12.0" rand = "0.8.5" rand_chacha = "0.3.1" @@ -82,42 +83,38 @@ serde_path_to_error = "0.1.11" serde_qs = { version = "0.12.0", optional = true } serde_urlencoded = "0.7.1" serde_with = "3.0.0" -signal-hook = "0.3.15" -strum = { version = "0.24.1", features = ["derive"] } +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"] } +tera = "1.19.1" thiserror = "1.0.40" time = { version = "0.3.21", features = ["serde", "serde-well-known", "std"] } tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] } -tera = "1.19.1" +unicode-segmentation = "1.10.1" url = { version = "2.4.0", features = ["serde"] } utoipa = { version = "3.3.0", features = ["preserve_order", "time"] } utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } -openssl = "0.10.55" +validator = "0.16.0" x509-parser = "0.15.0" -sha-1 = { version = "0.9"} -digest = "0.9" # First party crates 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"] } -common_enums = { version = "0.1.0", path = "../common_enums"} -external_services = { version = "0.1.0", path = "../external_services" } +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"] } +external_services = { version = "0.1.0", path = "../external_services" } +kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } router_derive = { version = "0.1.0", path = "../router_derive" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } -diesel_models = { version = "0.1.0", path = "../diesel_models", features = ["kv_store"] } -scheduler = { version = "0.1.0", path = "../scheduler", default-features = false} -data_models = { version = "0.1.0", path = "../data_models", default-features = false } -kgraph_utils = { version = "0.1.0", path = "../kgraph_utils" } +scheduler = { version = "0.1.0", path = "../scheduler", default-features = false } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } -[target.'cfg(not(target_os = "windows"))'.dependencies] -signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } - [build-dependencies] router_env = { version = "0.1.0", path = "../router_env", default-features = false } @@ -127,10 +124,8 @@ awc = { version = "3.1.1", features = ["rustls"] } derive_deref = "1.1.1" rand = "0.8.5" serial_test = "2.0.0" -thirtyfour = "0.31.0" time = { version = "0.3.21", features = ["macros"] } tokio = "1.28.2" -toml = "0.7.4" wiremock = "0.5" # First party dev-dependencies diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 7b20c3865d15..410e3c1113b1 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -1,3 +1,6 @@ +#[cfg(feature = "olap")] +pub mod user; + // ID generation pub(crate) const ID_LENGTH: usize = 20; pub(crate) const MAX_ID_LENGTH: usize = 64; @@ -52,3 +55,6 @@ pub const ROUTING_CONFIG_ID_LENGTH: usize = 10; pub const LOCKER_REDIS_PREFIX: &str = "LOCKER_PM_TOKEN"; pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes + +#[cfg(any(feature = "olap", feature = "oltp"))] +pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days diff --git a/crates/router/src/consts/user.rs b/crates/router/src/consts/user.rs new file mode 100644 index 000000000000..3a71fed01a12 --- /dev/null +++ b/crates/router/src/consts/user.rs @@ -0,0 +1,8 @@ +#[cfg(feature = "olap")] +pub const MAX_NAME_LENGTH: usize = 70; +#[cfg(feature = "olap")] +pub const MAX_COMPANY_NAME_LENGTH: usize = 70; + +// USER ROLES +#[cfg(any(feature = "olap", feature = "oltp"))] +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 817fafdae520..b7023fe5ae46 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -18,6 +18,8 @@ pub mod payments; pub mod payouts; pub mod refunds; pub mod routing; +#[cfg(feature = "olap")] +pub mod user; pub mod utils; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index e1e5ea744e2f..5ccd9e964866 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -916,13 +916,16 @@ pub async fn create_payment_connector( let mut default_routing_config = routing_helpers::get_merchant_default_config(&*state.store, merchant_id).await?; + let mut default_routing_config_for_profile = + routing_helpers::get_merchant_default_config(&*state.clone().store, &profile_id).await?; + let mca = state .store .insert_merchant_connector_account(merchant_connector_account, &key_store) .await .to_duplicate_response( errors::ApiErrorResponse::DuplicateMerchantConnectorAccount { - profile_id, + profile_id: profile_id.clone(), connector_name: req.connector_name.to_string(), }, )?; @@ -939,7 +942,7 @@ pub async fn create_payment_connector( }; if !default_routing_config.contains(&choice) { - default_routing_config.push(choice); + default_routing_config.push(choice.clone()); routing_helpers::update_merchant_default_config( &*state.store, merchant_id, @@ -947,6 +950,15 @@ pub async fn create_payment_connector( ) .await?; } + if !default_routing_config_for_profile.contains(&choice.clone()) { + default_routing_config_for_profile.push(choice); + routing_helpers::update_merchant_default_config( + &*state.store, + &profile_id.clone(), + default_routing_config_for_profile, + ) + .await?; + } } metrics::MCA_CREATE.add( diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index dc1d56721e88..810c079987eb 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -2,6 +2,8 @@ pub mod api_error_response; pub mod customers_error_response; pub mod error_handlers; pub mod transformers; +#[cfg(feature = "olap")] +pub mod user; pub mod utils; use std::fmt::Display; @@ -13,6 +15,8 @@ use diesel_models::errors as storage_errors; pub use redis_interface::errors::RedisError; use scheduler::errors as sch_errors; use storage_impl::errors as storage_impl_errors; +#[cfg(feature = "olap")] +pub use user::*; pub use self::{ api_error_response::ApiErrorResponse, diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs new file mode 100644 index 000000000000..b4d48365dc84 --- /dev/null +++ b/crates/router/src/core/errors/user.rs @@ -0,0 +1,78 @@ +use common_utils::errors::CustomResult; + +use crate::services::ApplicationResponse; + +pub type UserResult = CustomResult; +pub type UserResponse = CustomResult, UserErrors>; + +#[derive(Debug, thiserror::Error)] +pub enum UserErrors { + #[error("User InternalServerError")] + InternalServerError, + #[error("InvalidCredentials")] + InvalidCredentials, + #[error("UserExists")] + UserExists, + #[error("EmailParsingError")] + EmailParsingError, + #[error("NameParsingError")] + NameParsingError, + #[error("PasswordParsingError")] + PasswordParsingError, + #[error("CompanyNameParsingError")] + CompanyNameParsingError, + #[error("MerchantAccountCreationError: {0}")] + MerchantAccountCreationError(String), + #[error("InvalidEmailError")] + InvalidEmailError, + #[error("DuplicateOrganizationId")] + DuplicateOrganizationId, +} + +impl common_utils::errors::ErrorSwitch for UserErrors { + fn switch(&self) -> api_models::errors::types::ApiErrorResponse { + use api_models::errors::types::{ApiError, ApiErrorResponse as AER}; + let sub_code = "UR"; + match self { + Self::InternalServerError => { + AER::InternalServerError(ApiError::new("HE", 0, "Something Went Wrong", None)) + } + Self::InvalidCredentials => AER::Unauthorized(ApiError::new( + sub_code, + 1, + "Incorrect email or password", + None, + )), + Self::UserExists => AER::BadRequest(ApiError::new( + sub_code, + 3, + "An account already exists with this email", + None, + )), + Self::EmailParsingError => { + AER::BadRequest(ApiError::new(sub_code, 7, "Invalid Email", None)) + } + Self::NameParsingError => { + AER::BadRequest(ApiError::new(sub_code, 8, "Invalid Name", None)) + } + Self::PasswordParsingError => { + AER::BadRequest(ApiError::new(sub_code, 9, "Invalid Password", None)) + } + Self::CompanyNameParsingError => { + AER::BadRequest(ApiError::new(sub_code, 14, "Invalid Company Name", None)) + } + Self::MerchantAccountCreationError(error_message) => { + AER::InternalServerError(ApiError::new(sub_code, 15, error_message, None)) + } + Self::InvalidEmailError => { + AER::BadRequest(ApiError::new(sub_code, 16, "Invalid Email", None)) + } + Self::DuplicateOrganizationId => AER::InternalServerError(ApiError::new( + sub_code, + 21, + "An Organization with the id already exists", + None, + )), + } + } +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 67410cac8418..e02bc16e7197 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -1,6 +1,8 @@ + + {{ hyperloader_sdk_link }} @@ -545,22 +599,144 @@ rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
-
+ - -
+ @@ -817,15 +870,22 @@ }; var widgets = null; + var unifiedCheckout = null; const pub_key = window.__PAYMENT_DETAILS.pub_key; const hyper = Hyper(pub_key); + function mountUnifiedCheckout(id) { + if (unifiedCheckout !== null) { + unifiedCheckout.mount(id); + } + } + async function initialize() { const paymentDetails = window.__PAYMENT_DETAILS; var client_secret = paymentDetails.client_secret; const appearance = { variables: { - colorPrimary: paymentDetails.sdk_theme, + colorPrimary: paymentDetails.sdk_theme || "rgb(0, 109, 249)", fontFamily: "Work Sans, sans-serif", fontSizeBase: "16px", colorText: "rgb(51, 65, 85)", @@ -856,11 +916,8 @@ }, }; - const unifiedCheckout = widgets.create( - "payment", - unifiedCheckoutOptions - ); - unifiedCheckout.mount("#unified-checkout"); + unifiedCheckout = widgets.create("payment", unifiedCheckoutOptions); + mountUnifiedCheckout("#unified-checkout"); // Handle button press callback var paymentElement = widgets.getElement("payment"); @@ -890,6 +947,9 @@ } else { showMessage("An unexpected error occurred."); } + + // Re-initialize SDK + mountUnifiedCheckout("#unified-checkout"); } else { const { paymentIntent } = await hyper.retrievePaymentIntent( paymentDetails.client_secret @@ -906,13 +966,19 @@ // Fetches the payment status after payment submission async function checkStatus() { - const clientSecret = new URLSearchParams(window.location.search).get( - "payment_intent_client_secret" - ); + const paymentDetails = window.__PAYMENT_DETAILS; const res = { showSdk: true, }; + let clientSecret = new URLSearchParams(window.location.search).get( + "payment_intent_client_secret" + ); + + if (!clientSecret) { + clientSecret = paymentDetails.client_secret; + } + if (!clientSecret) { return res; } @@ -921,7 +987,10 @@ clientSecret ); - if (!paymentIntent || !paymentIntent.status) { + if ( + !paymentIntent || + paymentIntent.status === "requires_confirmation" + ) { return res; } @@ -950,101 +1019,68 @@ show("#payment-message"); addText("#payment-message", msg); } + function showStatus(paymentDetails) { const status = paymentDetails.status; let statusDetails = { imageSource: "", - message: "", + message: null, status: status, amountText: "", items: [], }; + // Payment details + var paymentId = createItem("Ref Id", paymentDetails.payment_id); + // @ts-ignore + statusDetails.items.push(paymentId); + + // Status specific information switch (status) { case "succeeded": - statusDetails.imageSource = - "http://www.clipartbest.com/cliparts/4ib/oRa/4iboRa7RT.png"; - statusDetails.message = "Payment successful"; - statusDetails.status = "Succeeded"; + statusDetails.imageSource = "https://i.imgur.com/5BOmYVl.img"; + statusDetails.message = + "We have successfully received your payment"; + statusDetails.status = "Paid successfully"; statusDetails.amountText = new Date( paymentDetails.created ).toTimeString(); - - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); break; case "processing": - statusDetails.imageSource = - "http://www.clipartbest.com/cliparts/4ib/oRa/4iboRa7RT.png"; - statusDetails.message = "Payment in progress"; - statusDetails.status = "Processing"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.message = + "Sorry! Your payment is taking longer than expected. Please check back again in sometime."; + statusDetails.status = "Payment Pending"; break; case "failed": - statusDetails.imageSource = ""; - statusDetails.message = "Payment failed"; - statusDetails.status = "Failed"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount + statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; + statusDetails.status = "Payment Failed!"; + var errorCodeNode = createItem( + "Error code", + paymentDetails.error_code + ); + var errorMessageNode = createItem( + "Error message", + paymentDetails.error_message ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.items.push(errorMessageNode, errorCodeNode); break; case "cancelled": - statusDetails.imageSource = ""; - statusDetails.message = "Payment cancelled"; - statusDetails.status = "Cancelled"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; + statusDetails.status = "Payment Cancelled"; break; case "requires_merchant_action": - statusDetails.imageSource = ""; - statusDetails.message = "Payment under review"; - statusDetails.status = "Under review"; - // Payment details - var amountNode = createItem( - "AMOUNT PAID", - paymentDetails.currency + " " + paymentDetails.amount - ); - var paymentId = createItem("PAYMENT ID", paymentDetails.payment_id); - var paymentId = createItem( - "MESSAGE", - "Your payment is under review by the merchant." - ); - // @ts-ignore - statusDetails.items.push(amountNode, paymentId); + statusDetails.imageSource = "https://i.imgur.com/Yb79Qt4.png"; + statusDetails.status = "Payment under review"; break; default: - statusDetails.imageSource = - "http://www.clipartbest.com/cliparts/4ib/oRa/4iboRa7RT.png"; - statusDetails.message = "Something went wrong"; + statusDetails.imageSource = "https://i.imgur.com/UD8CEuY.png"; statusDetails.status = "Something went wrong"; // Error details if (typeof paymentDetails.error === "object") { @@ -1062,36 +1098,52 @@ break; } - // Append status - var statusTextNode = document.getElementById("status-text"); - if (statusTextNode !== null) { - statusTextNode.innerText = statusDetails.message; - } - - // Append image - var statusImageNode = document.getElementById("status-img"); - if (statusImageNode !== null) { - statusImageNode.src = statusDetails.imageSource; - } - - // Append status details - var statusDateNode = document.getElementById("status-date"); - if (statusDateNode !== null) { - statusDateNode.innerText = statusDetails.amountText; - } + // Form header items + var amountNode = document.createElement("div"); + amountNode.className = "hyper-checkout-status-amount"; + amountNode.innerText = + paymentDetails.currency + " " + paymentDetails.amount; + var merchantLogoNode = document.createElement("img"); + merchantLogoNode.className = "hyper-checkout-status-merchant-logo"; + merchantLogoNode.src = ""; + merchantLogoNode.alt = ""; + + // Form content items + var statusImageNode = document.createElement("img"); + statusImageNode.className = "hyper-checkout-status-image"; + statusImageNode.src = statusDetails.imageSource; + var statusTextNode = document.createElement("div"); + statusTextNode.className = "hyper-checkout-status-text"; + statusTextNode.innerText = statusDetails.status; + var statusMessageNode = document.createElement("div"); + statusMessageNode.className = "hyper-checkout-status-message"; + statusMessageNode.innerText = statusDetails.message; + var statusDetailsNode = document.createElement("div"); + statusDetailsNode.className = "hyper-checkout-status-details"; // Append items - var statusItemNode = document.getElementById( - "hyper-checkout-status-items" + statusDetails.items.map((item) => statusDetailsNode?.append(item)); + const statusHeaderNode = document.getElementById( + "hyper-checkout-status-header" ); - if (statusItemNode !== null) { - statusDetails.items.map((item) => statusItemNode?.append(item)); + if (statusHeaderNode !== null) { + statusHeaderNode.append(amountNode, merchantLogoNode); + } + const statusContentNode = document.getElementById( + "hyper-checkout-status-content" + ); + if (statusContentNode !== null) { + statusContentNode.append(statusImageNode, statusTextNode); + if (statusDetails.message !== null) { + statusContentNode.append(statusMessageNode); + } + statusContentNode.append(statusDetailsNode); } } function createItem(heading, value) { var itemNode = document.createElement("div"); - itemNode.className = "hyper-checkout-item"; + itemNode.className = "hyper-checkout-status-item"; var headerNode = document.createElement("div"); headerNode.className = "hyper-checkout-item-header"; headerNode.innerText = heading; @@ -1238,8 +1290,7 @@ // Product price var priceNode = document.createElement("div"); priceNode.className = "hyper-checkout-card-item-price"; - priceNode.innerText = - paymentDetails.currency + " " + item.amount; + priceNode.innerText = paymentDetails.currency + " " + item.amount; // Append items nameAndQuantityWrapperNode.append(productNameNode, quantityNode); itemWrapperNode.append( @@ -1336,16 +1387,6 @@ show("#hyper-checkout-cart"); } - function hideCartInMobileView() { - window.history.back(); - hide("#hyper-checkout-cart"); - } - - function viewCartInMobileView() { - show("#hyper-checkout-cart"); - window.history.pushState("view-cart", ""); - } - function renderSDKHeader() { const paymentDetails = window.__PAYMENT_DETAILS; @@ -1389,6 +1430,8 @@ show("#hyper-checkout-sdk"); show("#hyper-checkout-details"); } else { + hide("#hyper-checkout-sdk"); + hide("#hyper-checkout-details"); show("#hyper-checkout-status"); show("#hyper-footer"); } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 234323f0179a..6b3cf11f5891 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1052,6 +1052,8 @@ pub async fn list_payment_methods( amount_capturable: None, updated_by: merchant_account.storage_scheme.to_string(), merchant_connector_id: None, + surcharge_amount: None, + tax_amount: None, }; state diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index a114b20380bf..e7408cecf163 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -3,6 +3,8 @@ pub mod customers; pub mod flows; pub mod helpers; pub mod operations; +#[cfg(feature = "retry")] +pub mod retry; pub mod routing; pub mod tokenization; pub mod transformers; @@ -12,7 +14,7 @@ use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant, vec::IntoI use api_models::{ enums, - payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, + payment_methods::{Surcharge, SurchargeDetailsResponse}, payments::HeaderPayload, }; use common_utils::{ext_traits::AsyncExt, pii}; @@ -231,7 +233,7 @@ where state, &merchant_account, &key_store, - connector_data, + connector_data.clone(), &operation, &mut payment_data, &customer, @@ -242,6 +244,33 @@ where ) .await?; + #[cfg(feature = "retry")] + let mut router_data = router_data; + #[cfg(feature = "retry")] + { + use crate::core::payments::retry::{self, GsmValidation}; + let config_bool = + retry::config_should_call_gsm(&*state.store, &merchant_account.merchant_id) + .await; + + if config_bool && router_data.should_call_gsm() { + router_data = retry::do_gsm_actions( + state, + &mut payment_data, + connectors, + connector_data, + router_data, + &merchant_account, + &key_store, + &operation, + &customer, + &validate_result, + schedule_time, + ) + .await?; + }; + } + let operation = Box::new(PaymentResponse); let db = &*state.store; connector_http_status_code = router_data.connector_http_status_code; @@ -261,6 +290,8 @@ where } api::ConnectorCallType::SessionMultiple(connectors) => { + let session_surcharge_data = + get_session_surcharge_data(&payment_data.payment_attempt); call_multiple_connectors_service( state, &merchant_account, @@ -269,7 +300,7 @@ where &operation, payment_data, &customer, - None, + session_surcharge_data, ) .await? } @@ -324,6 +355,21 @@ pub fn get_connector_data( .attach_printable("Connector not found in connectors iterator") } +pub fn get_session_surcharge_data( + payment_attempt: &data_models::payments::payment_attempt::PaymentAttempt, +) -> Option { + payment_attempt.surcharge_amount.map(|surcharge_amount| { + let tax_on_surcharge_amount = payment_attempt.tax_amount.unwrap_or(0); + let final_amount = payment_attempt.amount + surcharge_amount + tax_on_surcharge_amount; + api::SessionSurchargeDetails::PreDetermined(SurchargeDetailsResponse { + surcharge: Surcharge::Fixed(surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount, + final_amount, + }) + }) +} #[allow(clippy::too_many_arguments)] pub async fn payments_core( state: AppState, @@ -891,7 +937,7 @@ pub async fn call_multiple_connectors_service( _operation: &Op, mut payment_data: PaymentData, customer: &Option, - session_surcharge_metadata: Option, + session_surcharge_details: Option, ) -> RouterResult> where Op: Debug, @@ -928,18 +974,16 @@ where ) .await?; - payment_data.surcharge_details = session_surcharge_metadata - .as_ref() - .and_then(|surcharge_metadata| { - surcharge_metadata.surcharge_results.get( - &SurchargeMetadata::get_key_for_surcharge_details_hash_map( + payment_data.surcharge_details = + session_surcharge_details + .as_ref() + .and_then(|session_surcharge_details| { + session_surcharge_details.fetch_surcharge_details( &session_connector_data.payment_method_type.into(), &session_connector_data.payment_method_type, None, - ), - ) - }) - .cloned(); + ) + }); let router_data = payment_data .construct_router_data( diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 21f7db3d0b41..96cd4f5c622f 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,10 +1,14 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payment_methods}; +use api_models::{ + enums::FrmSuggestion, + payment_methods::{self, SurchargeDetailsResponse}, +}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::ResultExt; use futures::FutureExt; +use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; @@ -14,6 +18,7 @@ use crate::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + utils::get_individual_surcharge_detail_from_redis, }, db::StorageInterface, routes::AppState, @@ -305,19 +310,17 @@ impl sm.mandate_type = payment_attempt.mandate_details.clone().or(sm.mandate_type); sm }); + Self::validate_request_surcharge_details_with_session_surcharge_details( + state, + &payment_attempt, + request, + ) + .await?; - // populate payment_data.surcharge_details from request - let surcharge_details = request.surcharge_details.map(|surcharge_details| { - payment_methods::SurchargeDetailsResponse { - surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), - tax_on_surcharge: None, - surcharge_amount: surcharge_details.surcharge_amount, - tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), - final_amount: payment_attempt.amount - + surcharge_details.surcharge_amount - + surcharge_details.tax_amount.unwrap_or(0), - } - }); + let surcharge_details = Self::get_surcharge_details_from_payment_request_or_payment_attempt( + request, + &payment_attempt, + ); Ok(( Box::new(self), @@ -529,14 +532,6 @@ impl .take(); let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); - let surcharge_amount = payment_data - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.surcharge_amount); - let tax_amount = payment_data - .surcharge_details - .as_ref() - .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); let authorized_amount = payment_data .surcharge_details .as_ref() @@ -562,8 +557,6 @@ impl error_code, error_message, amount_capturable: Some(authorized_amount), - surcharge_amount, - tax_amount, updated_by: storage_scheme.to_string(), merchant_connector_id, }, @@ -672,3 +665,92 @@ impl ValidateRequest RouterResult<()> { + match ( + request.surcharge_details, + request.payment_method_data.as_ref(), + ) { + (Some(request_surcharge_details), Some(payment_method_data)) => { + if let Some(payment_method_type) = + payment_method_data.get_payment_method_type_if_session_token_type() + { + let invalid_surcharge_details_error = Err(errors::ApiErrorResponse::InvalidRequestData { + message: "surcharge_details sent in session token flow doesn't match with the one sent in confirm request".into(), + }.into()); + if let Some(attempt_surcharge_amount) = payment_attempt.surcharge_amount { + // payment_attempt.surcharge_amount will be Some if some surcharge was sent in payment create + // if surcharge was sent in payment create call, the same would have been sent to the connector during session call + // So verify the same + if request_surcharge_details.surcharge_amount != attempt_surcharge_amount + || request_surcharge_details.tax_amount != payment_attempt.tax_amount + { + return invalid_surcharge_details_error; + } + } else { + // if not sent in payment create + // verify that any calculated surcharge sent in session flow is same as the one sent in confirm + return match get_individual_surcharge_detail_from_redis( + state, + &payment_method_type.into(), + &payment_method_type, + None, + &payment_attempt.attempt_id, + ) + .await + { + Ok(surcharge_details) => utils::when( + !surcharge_details + .is_request_surcharge_matching(request_surcharge_details), + || invalid_surcharge_details_error, + ), + Err(err) if err.current_context() == &RedisError::NotFound => { + utils::when(!request_surcharge_details.is_surcharge_zero(), || { + invalid_surcharge_details_error + }) + } + Err(err) => Err(err) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch redis value"), + }; + } + } + Ok(()) + } + (Some(_request_surcharge_details), None) => { + Err(errors::ApiErrorResponse::MissingRequiredField { + field_name: "payment_method_data", + } + .into()) + } + _ => Ok(()), + } + } + + fn get_surcharge_details_from_payment_request_or_payment_attempt( + payment_request: &api::PaymentsRequest, + payment_attempt: &storage::PaymentAttempt, + ) -> Option { + payment_request + .surcharge_details + .map(|surcharge_details| { + surcharge_details.get_surcharge_details_object(payment_attempt.amount) + }) // if not passed in confirm request, look inside payment_attempt + .or(payment_attempt + .surcharge_amount + .map(|surcharge_amount| SurchargeDetailsResponse { + surcharge: payment_methods::Surcharge::Fixed(surcharge_amount), + tax_on_surcharge: None, + surcharge_amount, + tax_on_surcharge_amount: payment_attempt.tax_amount.unwrap_or(0), + final_amount: payment_attempt.amount + + surcharge_amount + + payment_attempt.tax_amount.unwrap_or(0), + })) + } +} diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 97bb84371306..fad7212c61d3 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::FrmSuggestion; +use api_models::{enums::FrmSuggestion, payment_methods}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use data_models::{mandates::MandateData, payments::payment_attempt::PaymentAttempt}; @@ -267,6 +267,19 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate: Option = setup_mandate.map(Into::into); + // populate payment_data.surcharge_details from request + let surcharge_details = request.surcharge_details.map(|surcharge_details| { + payment_methods::SurchargeDetailsResponse { + surcharge: payment_methods::Surcharge::Fixed(surcharge_details.surcharge_amount), + tax_on_surcharge: None, + surcharge_amount: surcharge_details.surcharge_amount, + tax_on_surcharge_amount: surcharge_details.tax_amount.unwrap_or(0), + final_amount: payment_attempt.amount + + surcharge_details.surcharge_amount + + surcharge_details.tax_amount.unwrap_or(0), + } + }); + Ok(( operation, PaymentData { @@ -299,7 +312,7 @@ impl ephemeral_key, multiple_capture_data: None, redirect_response: None, - surcharge_details: None, + surcharge_details, frm_message: None, payment_link_data, }, @@ -421,6 +434,15 @@ impl let authorized_amount = payment_data.payment_attempt.amount; let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); + payment_data.payment_attempt = db .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, @@ -432,6 +454,8 @@ impl true => Some(authorized_amount), false => None, }, + surcharge_amount, + tax_amount, updated_by: storage_scheme.to_string(), merchant_connector_id, }, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 77c344949660..d6346a512ef1 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -466,6 +466,8 @@ async fn payment_response_update_tracker( } else { None }, + surcharge_amount: router_data.request.get_surcharge_amount(), + tax_amount: router_data.request.get_tax_on_surcharge_amount(), updated_by: storage_scheme.to_string(), authentication_data, encoded_data, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 0a49c830b732..26bda6d6bee6 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -304,6 +304,10 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); + let surcharge_details = request.surcharge_details.map(|request_surcharge_details| { + request_surcharge_details.get_surcharge_details_object(payment_attempt.amount) + }); + Ok(( next_operation, PaymentData { @@ -336,7 +340,7 @@ impl ephemeral_key: None, multiple_capture_data: None, redirect_response: None, - surcharge_details: None, + surcharge_details, frm_message: None, payment_link_data: None, }, @@ -467,6 +471,14 @@ impl let payment_experience = payment_data.payment_attempt.payment_experience; let amount_to_capture = payment_data.payment_attempt.amount_to_capture; let capture_method = payment_data.payment_attempt.capture_method; + let surcharge_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount); + let tax_amount = payment_data + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); payment_data.payment_attempt = db .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, @@ -483,6 +495,8 @@ impl business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by: storage_scheme.to_string(), }, storage_scheme, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs new file mode 100644 index 000000000000..376b9048c856 --- /dev/null +++ b/crates/router/src/core/payments/retry.rs @@ -0,0 +1,581 @@ +use std::{str::FromStr, vec::IntoIter}; + +use diesel_models::enums as storage_enums; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; + +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payment_methods::PaymentMethodRetrieve, + payments::{ + self, + flows::{ConstructFlowSpecificData, Feature}, + operations, + }, + }, + db::StorageInterface, + routes, + routes::{app, metrics}, + services::{self, RedirectForm}, + types, + types::{api, domain, storage}, + utils, +}; + +#[instrument(skip_all)] +#[allow(clippy::too_many_arguments)] +pub async fn do_gsm_actions( + state: &app::AppState, + payment_data: &mut payments::PaymentData, + mut connectors: IntoIter, + original_connector_data: api::ConnectorData, + mut router_data: types::RouterData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + operation: &operations::BoxedOperation<'_, F, ApiRequest, Ctx>, + customer: &Option, + validate_result: &operations::ValidateResult<'_>, + schedule_time: Option, +) -> RouterResult> +where + F: Clone + Send + Sync, + FData: Send + Sync, + payments::PaymentResponse: operations::Operation, + + payments::PaymentData: ConstructFlowSpecificData, + types::RouterData: Feature, + dyn api::Connector: services::api::ConnectorIntegration, + Ctx: PaymentMethodRetrieve, +{ + let mut retries = None; + + metrics::AUTO_RETRY_ELIGIBLE_REQUEST_COUNT.add(&metrics::CONTEXT, 1, &[]); + + let mut initial_gsm = get_gsm(state, &router_data).await; + + //Check if step-up to threeDS is possible and merchant has enabled + let step_up_possible = initial_gsm + .clone() + .map(|gsm| gsm.step_up_possible) + .unwrap_or(false); + let is_no_three_ds_payment = matches!( + payment_data.payment_attempt.authentication_type, + Some(storage_enums::AuthenticationType::NoThreeDs) + ); + let should_step_up = if step_up_possible && is_no_three_ds_payment { + is_step_up_enabled_for_merchant_connector( + state, + &merchant_account.merchant_id, + original_connector_data.connector_name, + ) + .await + } else { + false + }; + + if should_step_up { + router_data = do_retry( + &state.clone(), + original_connector_data, + operation, + customer, + merchant_account, + key_store, + payment_data, + router_data, + validate_result, + schedule_time, + true, + ) + .await?; + } + // Step up is not applicable so proceed with auto retries flow + else { + loop { + // Use initial_gsm for first time alone + let gsm = match initial_gsm.as_ref() { + Some(gsm) => Some(gsm.clone()), + None => get_gsm(state, &router_data).await, + }; + + match get_gsm_decision(gsm) { + api_models::gsm::GsmDecision::Retry => { + retries = get_retries(state, retries, &merchant_account.merchant_id).await; + + if retries.is_none() || retries == Some(0) { + metrics::AUTO_RETRY_EXHAUSTED_COUNT.add(&metrics::CONTEXT, 1, &[]); + logger::info!("retries exhausted for auto_retry payment"); + break; + } + + if connectors.len() == 0 { + logger::info!("connectors exhausted for auto_retry payment"); + metrics::AUTO_RETRY_EXHAUSTED_COUNT.add(&metrics::CONTEXT, 1, &[]); + break; + } + + let connector = super::get_connector_data(&mut connectors)?; + + router_data = do_retry( + &state.clone(), + connector, + operation, + customer, + merchant_account, + key_store, + payment_data, + router_data, + validate_result, + schedule_time, + //this is an auto retry payment, but not step-up + false, + ) + .await?; + + retries = retries.map(|i| i - 1); + } + api_models::gsm::GsmDecision::Requeue => { + Err(errors::ApiErrorResponse::NotImplemented { + message: errors::api_error_response::NotImplementedMessage::Reason( + "Requeue not implemented".to_string(), + ), + }) + .into_report()? + } + api_models::gsm::GsmDecision::DoDefault => break, + } + initial_gsm = None; + } + } + Ok(router_data) +} + +#[instrument(skip_all)] +pub async fn is_step_up_enabled_for_merchant_connector( + state: &app::AppState, + merchant_id: &str, + connector_name: types::Connector, +) -> bool { + let key = format!("step_up_enabled_{merchant_id}"); + let db = &*state.store; + db.find_config_by_key_unwrap_or(key.as_str(), Some("[]".to_string())) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .and_then(|step_up_config| { + serde_json::from_str::>(&step_up_config.config) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Step-up config parsing failed") + }) + .map_err(|err| { + logger::error!(step_up_config_error=?err); + }) + .ok() + .map(|connectors_enabled| connectors_enabled.contains(&connector_name)) + .unwrap_or(false) +} + +#[instrument(skip_all)] +pub async fn get_retries( + state: &app::AppState, + retries: Option, + merchant_id: &str, +) -> Option { + match retries { + Some(retries) => Some(retries), + None => { + let key = format!("max_auto_retries_enabled_{merchant_id}"); + let db = &*state.store; + db.find_config_by_key(key.as_str()) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .and_then(|retries_config| { + retries_config + .config + .parse::() + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Retries config parsing failed") + }) + .map_err(|err| { + logger::error!(retries_error=?err); + None:: + }) + .ok() + } + } +} + +#[instrument(skip_all)] +pub async fn get_gsm( + state: &app::AppState, + router_data: &types::RouterData, +) -> Option { + let error_response = router_data.response.as_ref().err(); + let error_code = error_response.map(|err| err.code.to_owned()); + let error_message = error_response.map(|err| err.message.to_owned()); + let get_gsm = || async { + let connector_name = router_data.connector.to_string(); + let flow = get_flow_name::()?; + state.store.find_gsm_rule( + connector_name.clone(), + flow.clone(), + "sub_flow".to_string(), + error_code.clone().unwrap_or_default(), // TODO: make changes in connector to get a mandatory code in case of success or error response + error_message.clone().unwrap_or_default(), + ) + .await + .map_err(|err| { + if err.current_context().is_db_not_found() { + logger::warn!( + "GSM miss for connector - {}, flow - {}, error_code - {:?}, error_message - {:?}", + connector_name, + flow, + error_code, + error_message + ); + metrics::AUTO_RETRY_GSM_MISS_COUNT.add(&metrics::CONTEXT, 1, &[]); + } else { + metrics::AUTO_RETRY_GSM_FETCH_FAILURE_COUNT.add(&metrics::CONTEXT, 1, &[]); + }; + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to fetch decision from gsm") + }) + }; + get_gsm() + .await + .map_err(|err| { + // warn log should suffice here because we are not propagating this error + logger::warn!(get_gsm_decision_fetch_error=?err, "error fetching gsm decision"); + err + }) + .ok() +} + +#[instrument(skip_all)] +pub fn get_gsm_decision( + option_gsm: Option, +) -> api_models::gsm::GsmDecision { + let option_gsm_decision = option_gsm + .and_then(|gsm| { + api_models::gsm::GsmDecision::from_str(gsm.decision.as_str()) + .into_report() + .map_err(|err| { + let api_error = err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("gsm decision parsing failed"); + logger::warn!(get_gsm_decision_parse_error=?api_error, "error fetching gsm decision"); + api_error + }) + .ok() + }); + + if option_gsm_decision.is_some() { + metrics::AUTO_RETRY_GSM_MATCH_COUNT.add(&metrics::CONTEXT, 1, &[]); + } + option_gsm_decision.unwrap_or_default() +} + +#[inline] +fn get_flow_name() -> RouterResult { + Ok(std::any::type_name::() + .to_string() + .rsplit("::") + .next() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Flow stringify failed")? + .to_string()) +} + +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn do_retry( + state: &routes::AppState, + connector: api::ConnectorData, + operation: &operations::BoxedOperation<'_, F, ApiRequest, Ctx>, + customer: &Option, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + payment_data: &mut payments::PaymentData, + router_data: types::RouterData, + validate_result: &operations::ValidateResult<'_>, + schedule_time: Option, + is_step_up: bool, +) -> RouterResult> +where + F: Clone + Send + Sync, + FData: Send + Sync, + payments::PaymentResponse: operations::Operation, + + payments::PaymentData: ConstructFlowSpecificData, + types::RouterData: Feature, + dyn api::Connector: services::api::ConnectorIntegration, + Ctx: PaymentMethodRetrieve, +{ + metrics::AUTO_RETRY_PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); + + modify_trackers( + state, + connector.connector_name.to_string(), + payment_data, + merchant_account.storage_scheme, + router_data, + is_step_up, + ) + .await?; + + payments::call_connector_service( + state, + merchant_account, + key_store, + connector, + operation, + payment_data, + customer, + payments::CallConnectorAction::Trigger, + validate_result, + schedule_time, + api::HeaderPayload::default(), + ) + .await +} + +#[instrument(skip_all)] +pub async fn modify_trackers( + state: &routes::AppState, + connector: String, + payment_data: &mut payments::PaymentData, + storage_scheme: storage_enums::MerchantStorageScheme, + router_data: types::RouterData, + is_step_up: bool, +) -> RouterResult<()> +where + F: Clone + Send, + FData: Send, +{ + let new_attempt_count = payment_data.payment_intent.attempt_count + 1; + let new_payment_attempt = make_new_payment_attempt( + connector, + payment_data.payment_attempt.clone(), + new_attempt_count, + is_step_up, + ); + + let db = &*state.store; + + match router_data.response { + Ok(types::PaymentsResponseData::TransactionResponse { + resource_id, + connector_metadata, + redirection_data, + .. + }) => { + let encoded_data = payment_data.payment_attempt.encoded_data.clone(); + + let authentication_data = redirection_data + .map(|data| utils::Encode::::encode_to_value(&data)) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not parse the connector response")?; + + db.update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + storage::PaymentAttemptUpdate::ResponseUpdate { + status: router_data.status, + connector: None, + connector_transaction_id: match resource_id { + types::ResponseId::NoResponseId => None, + types::ResponseId::ConnectorTransactionId(id) + | types::ResponseId::EncodedData(id) => Some(id), + }, + connector_response_reference_id: payment_data + .payment_attempt + .connector_response_reference_id + .clone(), + authentication_type: None, + payment_method_id: Some(router_data.payment_method_id), + mandate_id: payment_data + .mandate_id + .clone() + .map(|mandate| mandate.mandate_id), + connector_metadata, + payment_token: None, + error_code: None, + error_message: None, + error_reason: None, + amount_capturable: if router_data.status.is_terminal_status() { + Some(0) + } else { + None + }, + surcharge_amount: None, + tax_amount: None, + updated_by: storage_scheme.to_string(), + authentication_data, + encoded_data, + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + Ok(_) => { + logger::error!("unexpected response: this response was not expected in Retry flow"); + return Ok(()); + } + Err(error_response) => { + db.update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + storage::PaymentAttemptUpdate::ErrorUpdate { + connector: None, + error_code: Some(Some(error_response.code)), + error_message: Some(Some(error_response.message)), + status: storage_enums::AttemptStatus::Failure, + error_reason: Some(error_response.reason), + amount_capturable: Some(0), + updated_by: storage_scheme.to_string(), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + } + + let payment_attempt = db + .insert_payment_attempt(new_payment_attempt, storage_scheme) + .await + .to_duplicate_response(errors::ApiErrorResponse::DuplicatePayment { + payment_id: payment_data.payment_intent.payment_id.clone(), + })?; + + // update payment_attempt, connector_response and payment_intent in payment_data + payment_data.payment_attempt = payment_attempt; + + payment_data.payment_intent = db + .update_payment_intent( + payment_data.payment_intent.clone(), + storage::PaymentIntentUpdate::PaymentAttemptAndAttemptCountUpdate { + active_attempt_id: payment_data.payment_attempt.attempt_id.clone(), + attempt_count: new_attempt_count, + updated_by: storage_scheme.to_string(), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + Ok(()) +} + +#[instrument(skip_all)] +pub fn make_new_payment_attempt( + connector: String, + old_payment_attempt: storage::PaymentAttempt, + new_attempt_count: i16, + is_step_up: bool, +) -> storage::PaymentAttemptNew { + let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); + storage::PaymentAttemptNew { + connector: Some(connector), + attempt_id: utils::get_payment_attempt_id( + &old_payment_attempt.payment_id, + new_attempt_count, + ), + payment_id: old_payment_attempt.payment_id, + merchant_id: old_payment_attempt.merchant_id, + status: old_payment_attempt.status, + amount: old_payment_attempt.amount, + currency: old_payment_attempt.currency, + save_to_locker: old_payment_attempt.save_to_locker, + + offer_amount: old_payment_attempt.offer_amount, + surcharge_amount: old_payment_attempt.surcharge_amount, + tax_amount: old_payment_attempt.tax_amount, + payment_method_id: old_payment_attempt.payment_method_id, + payment_method: old_payment_attempt.payment_method, + payment_method_type: old_payment_attempt.payment_method_type, + capture_method: old_payment_attempt.capture_method, + capture_on: old_payment_attempt.capture_on, + confirm: old_payment_attempt.confirm, + authentication_type: if is_step_up { + Some(storage_enums::AuthenticationType::ThreeDs) + } else { + old_payment_attempt.authentication_type + }, + + amount_to_capture: old_payment_attempt.amount_to_capture, + mandate_id: old_payment_attempt.mandate_id, + browser_info: old_payment_attempt.browser_info, + payment_token: old_payment_attempt.payment_token, + + created_at, + modified_at, + last_synced, + ..storage::PaymentAttemptNew::default() + } +} + +pub async fn config_should_call_gsm(db: &dyn StorageInterface, merchant_id: &String) -> bool { + let config = db + .find_config_by_key_unwrap_or( + format!("should_call_gsm_{}", merchant_id).as_str(), + Some("false".to_string()), + ) + .await; + match config { + Ok(conf) => conf.config == "true", + Err(err) => { + logger::error!("{err}"); + false + } + } +} + +pub trait GsmValidation { + // TODO : move this function to appropriate place later. + fn should_call_gsm(&self) -> bool; +} + +impl + GsmValidation + for types::RouterData +{ + #[inline(always)] + fn should_call_gsm(&self) -> bool { + if self.response.is_err() { + true + } else { + match self.status { + storage_enums::AttemptStatus::Started + | storage_enums::AttemptStatus::AuthenticationPending + | storage_enums::AttemptStatus::AuthenticationSuccessful + | storage_enums::AttemptStatus::Authorized + | storage_enums::AttemptStatus::Charged + | storage_enums::AttemptStatus::Authorizing + | storage_enums::AttemptStatus::CodInitiated + | storage_enums::AttemptStatus::Voided + | storage_enums::AttemptStatus::VoidInitiated + | storage_enums::AttemptStatus::CaptureInitiated + | storage_enums::AttemptStatus::RouterDeclined + | storage_enums::AttemptStatus::VoidFailed + | storage_enums::AttemptStatus::AutoRefunded + | storage_enums::AttemptStatus::CaptureFailed + | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::Pending + | storage_enums::AttemptStatus::PaymentMethodAwaited + | storage_enums::AttemptStatus::ConfirmationAwaited + | storage_enums::AttemptStatus::Unresolved + | storage_enums::AttemptStatus::DeviceDataCollectionPending => false, + + storage_enums::AttemptStatus::AuthenticationFailed + | storage_enums::AttemptStatus::AuthorizationFailed + | storage_enums::AttemptStatus::Failure => true, + } + } + } +} diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 4134ddf65ea0..3b89d4e38e4e 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -71,7 +71,10 @@ pub struct SessionRoutingPmTypeInput<'a> { routing_algorithm: &'a MerchantAccountRoutingAlgorithm, backend_input: dsl_inputs::BackendInput, allowed_connectors: FxHashMap, - #[cfg(feature = "business_profile_routing")] + #[cfg(any( + feature = "business_profile_routing", + feature = "profile_specific_fallback_routing" + ))] profile_id: Option, } static ROUTING_CACHE: StaticCache = StaticCache::new(); @@ -207,10 +210,22 @@ pub async fn perform_static_routing_v1( let algorithm_id = if let Some(id) = algorithm_ref.algorithm_id { id } else { - let fallback_config = - routing_helpers::get_merchant_default_config(&*state.clone().store, merchant_id) - .await - .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + let fallback_config = routing_helpers::get_merchant_default_config( + &*state.clone().store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] + merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; return Ok(fallback_config); }; @@ -616,10 +631,22 @@ pub async fn perform_fallback_routing( eligible_connectors: Option<&Vec>, #[cfg(feature = "business_profile_routing")] profile_id: Option, ) -> RoutingResult> { - let fallback_config = - routing_helpers::get_merchant_default_config(&*state.store, &key_store.merchant_id) - .await - .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; + let fallback_config = routing_helpers::get_merchant_default_config( + &*state.store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] + &key_store.merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, + ) + .await + .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; let backend_input = make_dsl_input(payment_data)?; perform_kgraph_filtering( @@ -819,8 +846,11 @@ pub async fn perform_session_flow_routing( routing_algorithm: &routing_algorithm, backend_input: backend_input.clone(), allowed_connectors, - #[cfg(feature = "business_profile_routing")] - profile_id: session_input.payment_intent.clone().profile_id, + #[cfg(any( + feature = "business_profile_routing", + feature = "profile_specific_fallback_routing" + ))] + profile_id: session_input.payment_intent.profile_id.clone(), }; let maybe_choice = perform_session_routing_for_pm_type(session_pm_input).await?; @@ -880,7 +910,16 @@ async fn perform_session_routing_for_pm_type( } else { routing_helpers::get_merchant_default_config( &*session_pm_input.state.clone().store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + session_pm_input + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)? @@ -903,7 +942,16 @@ async fn perform_session_routing_for_pm_type( if final_selection.is_empty() { let fallback = routing_helpers::get_merchant_default_config( &*session_pm_input.state.clone().store, + #[cfg(not(feature = "profile_specific_fallback_routing"))] merchant_id, + #[cfg(feature = "profile_specific_fallback_routing")] + { + session_pm_input + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)? + }, ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 723611ed5009..4171c3385637 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -13,13 +13,14 @@ use diesel_models::routing_algorithm::RoutingAlgorithm; use error_stack::{IntoReport, ResultExt}; use rustc_hash::FxHashSet; -#[cfg(feature = "business_profile_routing")] -use crate::core::utils::validate_and_get_business_profile; #[cfg(feature = "business_profile_routing")] use crate::types::transformers::{ForeignInto, ForeignTryInto}; use crate::{ consts, - core::errors::{RouterResponse, StorageErrorExt}, + core::{ + errors::{RouterResponse, StorageErrorExt}, + utils as core_utils, + }, routes::AppState, types::domain, utils::{self, OptionExt, ValueExt}, @@ -111,8 +112,12 @@ pub async fn create_routing_config( }) .attach_printable("Profile_id not provided")?; - validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) - .await?; + core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await?; helpers::validate_connectors_in_routing_config( db, @@ -229,7 +234,7 @@ pub async fn link_routing_config( .await .change_context(errors::ApiErrorResponse::ResourceIdNotFound)?; - let business_profile = validate_and_get_business_profile( + let business_profile = core_utils::validate_and_get_business_profile( db, Some(&routing_algorithm.profile_id), &merchant_account.merchant_id, @@ -332,7 +337,7 @@ pub async fn retrieve_routing_config( .await .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?; - validate_and_get_business_profile( + core_utils::validate_and_get_business_profile( db, Some(&routing_algorithm.profile_id), &merchant_account.merchant_id, @@ -401,9 +406,12 @@ pub async fn unlink_routing_config( field_name: "profile_id", }) .attach_printable("Profile_id not provided")?; - let business_profile = - validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) - .await?; + let business_profile = core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await?; match business_profile { Some(business_profile) => { let routing_algo_ref: routing_types::RoutingAlgorithmRef = business_profile @@ -622,13 +630,15 @@ pub async fn retrieve_linked_routing_config( #[cfg(feature = "business_profile_routing")] { let business_profiles = if let Some(profile_id) = query_params.profile_id { - validate_and_get_business_profile(db, Some(&profile_id), &merchant_account.merchant_id) - .await? - .map(|profile| vec![profile]) - .get_required_value("BusinessProfile") - .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { - id: profile_id, - })? + core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await? + .map(|profile| vec![profile]) + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { id: profile_id })? } else { db.list_business_profile_by_merchant_id(&merchant_account.merchant_id) .await @@ -711,3 +721,118 @@ pub async fn retrieve_linked_routing_config( Ok(service_api::ApplicationResponse::Json(response)) } } + +pub async fn retrieve_default_routing_config_for_profiles( + state: AppState, + merchant_account: domain::MerchantAccount, +) -> RouterResponse> { + let db = state.store.as_ref(); + + let all_profiles = db + .list_business_profile_by_merchant_id(&merchant_account.merchant_id) + .await + .to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound) + .attach_printable("error retrieving all business profiles for merchant")?; + + let retrieve_config_futures = all_profiles + .iter() + .map(|prof| helpers::get_merchant_default_config(db, &prof.profile_id)) + .collect::>(); + + let configs = futures::future::join_all(retrieve_config_futures) + .await + .into_iter() + .collect::, _>>()?; + + let default_configs = configs + .into_iter() + .zip(all_profiles.iter().map(|prof| prof.profile_id.clone())) + .map( + |(config, profile_id)| routing_types::ProfileDefaultRoutingConfig { + profile_id, + connectors: config, + }, + ) + .collect::>(); + + Ok(service_api::ApplicationResponse::Json(default_configs)) +} + +pub async fn update_default_routing_config_for_profile( + state: AppState, + merchant_account: domain::MerchantAccount, + updated_config: Vec, + profile_id: String, +) -> RouterResponse { + let db = state.store.as_ref(); + + let business_profile = core_utils::validate_and_get_business_profile( + db, + Some(&profile_id), + &merchant_account.merchant_id, + ) + .await? + .get_required_value("BusinessProfile") + .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { id: profile_id })?; + let default_config = + helpers::get_merchant_default_config(db, &business_profile.profile_id).await?; + + utils::when(default_config.len() != updated_config.len(), || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: "current config and updated config have different lengths".to_string(), + }) + .into_report() + })?; + + let existing_set = FxHashSet::from_iter(default_config.iter().map(|c| { + ( + c.connector.to_string(), + #[cfg(feature = "connector_choice_mca_id")] + c.merchant_connector_id.as_ref(), + #[cfg(not(feature = "connector_choice_mca_id"))] + c.sub_label.as_ref(), + ) + })); + + let updated_set = FxHashSet::from_iter(updated_config.iter().map(|c| { + ( + c.connector.to_string(), + #[cfg(feature = "connector_choice_mca_id")] + c.merchant_connector_id.as_ref(), + #[cfg(not(feature = "connector_choice_mca_id"))] + c.sub_label.as_ref(), + ) + })); + + let symmetric_diff = existing_set + .symmetric_difference(&updated_set) + .cloned() + .collect::>(); + + utils::when(!symmetric_diff.is_empty(), || { + let error_str = symmetric_diff + .into_iter() + .map(|(connector, ident)| format!("'{connector}:{ident:?}'")) + .collect::>() + .join(", "); + + Err(errors::ApiErrorResponse::InvalidRequestData { + message: format!("connector mismatch between old and new configs ({error_str})"), + }) + .into_report() + })?; + + helpers::update_merchant_default_config( + db, + &business_profile.profile_id, + updated_config.clone(), + ) + .await?; + + Ok(service_api::ApplicationResponse::Json( + routing_types::ProfileDefaultRoutingConfig { + profile_id: business_profile.profile_id, + connectors: updated_config, + }, + )) +} diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs new file mode 100644 index 000000000000..710dc9281bfa --- /dev/null +++ b/crates/router/src/core/user.rs @@ -0,0 +1,81 @@ +use api_models::user as api; +use diesel_models::enums::UserStatus; +use error_stack::IntoReport; +use masking::{ExposeInterface, Secret}; +use router_env::env; + +use super::errors::{UserErrors, UserResponse}; +use crate::{ + consts::user as consts, routes::AppState, services::ApplicationResponse, types::domain, +}; + +pub async fn connect_account( + state: AppState, + request: api::ConnectAccountRequest, +) -> UserResponse { + let find_user = state + .store + .find_user_by_email(request.email.clone().expose().expose().as_str()) + .await; + + if let Ok(found_user) = find_user { + let user_from_db: domain::UserFromStorage = found_user.into(); + + user_from_db.compare_password(request.password)?; + + let user_role = user_from_db.get_role_from_db(state.clone()).await?; + let jwt_token = user_from_db + .get_jwt_auth_token(state.clone(), user_role.org_id) + .await?; + + return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + })); + } else if find_user + .map_err(|e| e.current_context().is_db_not_found()) + .err() + .unwrap_or(false) + { + if matches!(env::which(), env::Env::Production) { + return Err(UserErrors::InvalidCredentials).into_report(); + } + + let new_user = domain::NewUser::try_from(request)?; + let _ = new_user + .get_new_merchant() + .get_new_organization() + .insert_org_in_db(state.clone()) + .await?; + let user_from_db = new_user + .insert_user_and_merchant_in_db(state.clone()) + .await?; + let user_role = new_user + .insert_user_role_in_db( + state.clone(), + consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + UserStatus::Active, + ) + .await?; + let jwt_token = user_from_db + .get_jwt_auth_token(state.clone(), user_role.org_id) + .await?; + + return Ok(ApplicationResponse::Json(api::ConnectAccountResponse { + token: Secret::new(jwt_token), + merchant_id: user_role.merchant_id, + name: user_from_db.get_name(), + email: user_from_db.get_email(), + verification_days_left: None, + user_role: user_role.role_id, + user_id: user_from_db.get_user_id().to_string(), + })); + } else { + Err(UserErrors::InternalServerError.into()) + } +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 1eb9029ae398..fb3dc3e7d281 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,10 +1,18 @@ use std::{marker::PhantomData, str::FromStr}; -use api_models::enums::{DisputeStage, DisputeStatus}; +use api_models::{ + enums::{DisputeStage, DisputeStatus}, + payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}, +}; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; -use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; +use common_utils::{ + errors::CustomResult, + ext_traits::{AsyncExt, Encode}, +}; use error_stack::{report, IntoReport, ResultExt}; +use euclid::enums as euclid_enums; +use redis_interface::errors::RedisError; use router_env::{instrument, tracing}; use uuid::Uuid; @@ -1073,3 +1081,65 @@ pub fn get_flow_name() -> RouterResult { .attach_printable("Flow stringify failed")? .to_string()) } + +pub async fn persist_individual_surcharge_details_in_redis( + state: &AppState, + merchant_account: &domain::MerchantAccount, + surcharge_metadata: &SurchargeMetadata, +) -> RouterResult<()> { + if !surcharge_metadata.is_empty_result() { + let redis_conn = state + .store + .get_redis_conn() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get redis connection")?; + let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key( + &surcharge_metadata.payment_attempt_id, + ); + + let mut value_list = Vec::with_capacity(surcharge_metadata.get_surcharge_results_size()); + for (key, value) in surcharge_metadata + .get_individual_surcharge_key_value_pairs() + .into_iter() + { + value_list.push(( + key, + Encode::::encode_to_string_of_json(&value) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to encode to string of json")?, + )); + } + let intent_fulfillment_time = merchant_account + .intent_fulfillment_time + .unwrap_or(consts::DEFAULT_FULFILLMENT_TIME); + redis_conn + .set_hash_fields(&redis_key, value_list, Some(intent_fulfillment_time)) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to write to redis")?; + } + Ok(()) +} + +pub async fn get_individual_surcharge_detail_from_redis( + state: &AppState, + payment_method: &euclid_enums::PaymentMethod, + payment_method_type: &euclid_enums::PaymentMethodType, + card_network: Option, + payment_attempt_id: &str, +) -> CustomResult { + let redis_conn = state + .store + .get_redis_conn() + .attach_printable("Failed to get redis connection")?; + let redis_key = SurchargeMetadata::get_surcharge_metadata_redis_key(payment_attempt_id); + let value_key = SurchargeMetadata::get_surcharge_details_redis_hashset_key( + payment_method, + payment_method_type, + card_network.as_ref(), + ); + + redis_conn + .get_hash_field_and_deserialize(&redis_key, &value_key, "SurchargeDetailsResponse") + .await +} diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 873102e81ec2..27a90028ba6a 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -38,6 +38,7 @@ pub struct ApiEvent { response: Option, #[serde(flatten)] event_type: ApiEventsType, + hs_latency: Option, } impl ApiEvent { @@ -49,6 +50,7 @@ impl ApiEvent { status_code: i64, request: serde_json::Value, response: Option, + hs_latency: Option, auth_type: AuthenticationType, event_type: ApiEventsType, http_req: &HttpRequest, @@ -72,6 +74,7 @@ impl ApiEvent { .and_then(|user_agent_value| user_agent_value.to_str().ok().map(ToOwned::to_owned)), url_path: http_req.path().to_string(), event_type, + hs_latency, } } } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 5cd0b6cbea5f..e106eb06a766 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -146,6 +146,7 @@ pub fn mk_app( .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) .service(routes::Gsm::server(state.clone())) + .service(routes::User::server(state.clone())) } #[cfg(all(feature = "olap", feature = "kms"))] diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index ac5c14200600..745433c2074b 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -23,6 +23,8 @@ pub mod payouts; pub mod refunds; #[cfg(feature = "olap")] pub mod routing; +#[cfg(feature = "olap")] +pub mod user; #[cfg(all(feature = "olap", feature = "kms"))] pub mod verification; pub mod webhooks; @@ -38,7 +40,7 @@ pub use self::app::Verify; pub use self::app::{ ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey, Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, - PaymentMethods, Payments, Refunds, Webhooks, + PaymentMethods, Payments, Refunds, User, Webhooks, }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 9153e9e747f6..a8eda22402c3 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -64,7 +64,10 @@ pub async fn retrieve_merchant_account( ) -> HttpResponse { let flow = Flow::MerchantsAccountRetrieve; let merchant_id = mid.into_inner(); - let payload = web::Json(admin::MerchantId { merchant_id }).into_inner(); + let payload = web::Json(admin::MerchantId { + merchant_id: merchant_id.to_owned(), + }) + .into_inner(); api::server_wrap( flow, @@ -72,7 +75,11 @@ pub async fn retrieve_merchant_account( &req, payload, |state, _, req| get_merchant_account(state, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -130,7 +137,13 @@ pub async fn update_merchant_account( &req, json_payload.into_inner(), |state, _, req| merchant_account_update(state, &merchant_id, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -203,7 +216,13 @@ pub async fn payment_connector_create( &req, json_payload.into_inner(), |state, _, req| create_payment_connector(state, req, &merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -236,7 +255,7 @@ pub async fn payment_connector_retrieve( let flow = Flow::MerchantConnectorsRetrieve; let (merchant_id, merchant_connector_id) = path.into_inner(); let payload = web::Json(admin::MerchantConnectorId { - merchant_id, + merchant_id: merchant_id.clone(), merchant_connector_id, }) .into_inner(); @@ -249,7 +268,11 @@ pub async fn payment_connector_retrieve( |state, _, req| { retrieve_payment_connector(state, req.merchant_id, req.merchant_connector_id) }, - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -285,9 +308,13 @@ pub async fn payment_connector_list( flow, state, &req, - merchant_id, + merchant_id.to_owned(), |state, _, merchant_id| list_payment_connectors(state, merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -328,7 +355,13 @@ pub async fn payment_connector_update( &req, json_payload.into_inner(), |state, _, req| update_payment_connector(state, &merchant_id, &merchant_connector_id, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -362,7 +395,7 @@ pub async fn payment_connector_delete( let (merchant_id, merchant_connector_id) = path.into_inner(); let payload = web::Json(admin::MerchantConnectorId { - merchant_id, + merchant_id: merchant_id.clone(), merchant_connector_id, }) .into_inner(); @@ -372,7 +405,11 @@ pub async fn payment_connector_delete( &req, payload, |state, _, req| delete_payment_connector(state, req.merchant_id, req.merchant_connector_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -419,7 +456,13 @@ pub async fn business_profile_create( &req, payload, |state, _, req| create_business_profile(state, req, &merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -431,7 +474,7 @@ pub async fn business_profile_retrieve( path: web::Path<(String, String)>, ) -> HttpResponse { let flow = Flow::BusinessProfileRetrieve; - let (_, profile_id) = path.into_inner(); + let (merchant_id, profile_id) = path.into_inner(); api::server_wrap( flow, @@ -439,7 +482,11 @@ pub async fn business_profile_retrieve( &req, profile_id, |state, _, profile_id| retrieve_business_profile(state, profile_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -460,7 +507,13 @@ pub async fn business_profile_update( &req, json_payload.into_inner(), |state, _, req| update_business_profile(state, &profile_id, &merchant_id, req), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -498,9 +551,13 @@ pub async fn business_profiles_list( flow, state, &req, - merchant_id, + merchant_id.clone(), |state, _, merchant_id| list_business_profile(state, merchant_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index c2e289cd0f7e..1f71f1dc2800 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -53,7 +53,13 @@ pub async fn api_key_create( ) .await }, - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -91,7 +97,13 @@ pub async fn api_key_retrieve( &req, (&merchant_id, &key_id), |state, _, (merchant_id, key_id)| api_keys::retrieve_api_key(state, merchant_id, key_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -173,7 +185,13 @@ pub async fn api_key_revoke( &req, (&merchant_id, &key_id), |state, _, (merchant_id, key_id)| api_keys::revoke_api_key(state, merchant_id, key_id), - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { + merchant_id: merchant_id.clone(), + }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await @@ -213,11 +231,15 @@ pub async fn api_key_list( flow, state, &req, - (limit, offset, merchant_id), + (limit, offset, merchant_id.clone()), |state, _, (limit, offset, merchant_id)| async move { api_keys::list_api_keys(state, merchant_id, limit, offset).await }, - &auth::AdminApiAuth, + auth::auth_type( + &auth::AdminApiAuth, + &auth::JWTAuthMerchantFromRoute { merchant_id }, + req.headers(), + ), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 67662961ed44..7f5c720be607 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -19,7 +19,7 @@ use super::routing as cloud_routing; #[cfg(all(feature = "olap", feature = "kms"))] use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains}; #[cfg(feature = "olap")] -use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*}; +use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, user::*}; use super::{cache::*, health::*, payment_link::*}; #[cfg(any(feature = "olap", feature = "oltp"))] use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; @@ -324,6 +324,16 @@ impl Routing { web::resource("/{algorithm_id}/activate") .route(web::post().to(cloud_routing::routing_link_config)), ) + .service( + web::resource("/default/profile/{profile_id}").route( + web::post().to(cloud_routing::routing_update_default_config_for_profile), + ), + ) + .service( + web::resource("/default/profile").route( + web::get().to(cloud_routing::routing_retrieve_default_config_for_profiles), + ), + ) } } @@ -710,3 +720,17 @@ impl Verify { ) } } + +pub struct User; + +#[cfg(feature = "olap")] +impl User { + pub fn server(state: AppState) -> Scope { + web::scope("/user") + .app_data(web::Data::new(state)) + .service(web::resource("/signin").route(web::post().to(user_connect_account))) + .service(web::resource("/signup").route(web::post().to(user_connect_account))) + .service(web::resource("/v2/signin").route(web::post().to(user_connect_account))) + .service(web::resource("/v2/signup").route(web::post().to(user_connect_account))) + } +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 4e6fc1870f56..ae573e871627 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -24,6 +24,7 @@ pub enum ApiIdentifier { PaymentLink, Routing, Gsm, + User, } impl From for ApiIdentifier { @@ -134,6 +135,8 @@ impl From for ApiIdentifier { | Flow::GsmRuleRetrieve | Flow::GsmRuleUpdate | Flow::GsmRuleDelete => Self::Gsm, + + Flow::UserConnectAccount => Self::User, } } } diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index 34d818eaa392..a8e6f9d2a892 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -102,5 +102,13 @@ counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_SUCCESSFUL_PAYMENT, GLOBAL_METER); counter_metric!(APPLE_PAY_MANUAL_FLOW_FAILED_PAYMENT, GLOBAL_METER); counter_metric!(APPLE_PAY_SIMPLIFIED_FLOW_FAILED_PAYMENT, GLOBAL_METER); +// Metrics for Auto Retries +counter_metric!(AUTO_RETRY_ELIGIBLE_REQUEST_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_GSM_MISS_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_GSM_FETCH_FAILURE_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_GSM_MATCH_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_EXHAUSTED_COUNT, GLOBAL_METER); +counter_metric!(AUTO_RETRY_PAYMENT_COUNT, GLOBAL_METER); + pub mod request; pub mod utils; diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 5ed73df1c175..ed36721da445 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -4,7 +4,7 @@ pub mod helpers; use actix_web::{web, Responder}; use api_models::payments::HeaderPayload; use error_stack::report; -use router_env::{instrument, tracing, types, Flow}; +use router_env::{env, instrument, tracing, types, Flow}; use crate::{ self as app, @@ -118,7 +118,10 @@ pub async fn payments_create( api::AuthFlow::Merchant, ) }, - &auth::ApiKeyAuth, + match env::which() { + env::Env::Production => &auth::ApiKeyAuth, + _ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + }, locking_action, ) .await @@ -249,7 +252,11 @@ pub async fn payments_retrieve( HeaderPayload::default(), ) }, - &*auth_type, + auth::auth_type( + &*auth_type, + &auth::JWTAuth, + req.headers(), + ), locking_action, ) .await @@ -828,7 +835,7 @@ pub async fn payments_list( &req, payload, |state, auth, req| payments::list_payments(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -848,7 +855,7 @@ pub async fn payments_list_by_filter( &req, payload, |state, auth, req| payments::apply_filters_on_payments(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -868,7 +875,7 @@ pub async fn get_filters_for_payments( &req, payload, |state, auth, req| payments::get_filters_for_payments(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index c20f3fbf975d..d1f5cb56fe23 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -37,7 +37,7 @@ pub async fn refunds_create( &req, json_payload.into_inner(), |state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -88,7 +88,7 @@ pub async fn refunds_retrieve( refund_retrieve_core, ) }, - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -202,7 +202,7 @@ pub async fn refunds_list( &req, payload.into_inner(), |state, auth, req| refund_list(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await @@ -235,7 +235,7 @@ pub async fn refunds_filter_list( &req, payload.into_inner(), |state, auth, req| refund_filter_list(state, auth.merchant_account, req), - &auth::ApiKeyAuth, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, ) .await diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 9252c360a9ce..606111a88818 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -14,7 +14,7 @@ use router_env::{ use crate::{ core::{api_locking, routing}, routes::AppState, - services::{api as oss_api, authentication as oss_auth, authentication as auth}, + services::{api as oss_api, authentication as auth}, }; #[cfg(feature = "olap")] @@ -30,11 +30,11 @@ pub async fn routing_create_config( state, &req, json_payload.into_inner(), - |state, auth: oss_auth::AuthenticationData, payload| { + |state, auth: auth::AuthenticationData, payload| { routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -55,7 +55,7 @@ pub async fn routing_link_config( state, &req, path.into_inner(), - |state, auth: oss_auth::AuthenticationData, algorithm_id| { + |state, auth: auth::AuthenticationData, algorithm_id| { routing::link_routing_config( state, auth.merchant_account, @@ -65,7 +65,7 @@ pub async fn routing_link_config( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -87,11 +87,11 @@ pub async fn routing_retrieve_config( state, &req, algorithm_id, - |state, auth: oss_auth::AuthenticationData, algorithm_id| { + |state, auth: auth::AuthenticationData, algorithm_id| { routing::retrieve_routing_config(state, auth.merchant_account, algorithm_id) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -114,7 +114,7 @@ pub async fn routing_retrieve_dictionary( state, &req, query.into_inner(), - |state, auth: oss_auth::AuthenticationData, query_params| { + |state, auth: auth::AuthenticationData, query_params| { routing::retrieve_merchant_routing_dictionary( state, auth.merchant_account, @@ -122,7 +122,7 @@ pub async fn routing_retrieve_dictionary( ) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -138,11 +138,11 @@ pub async fn routing_retrieve_dictionary( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::retrieve_merchant_routing_dictionary(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -168,11 +168,11 @@ pub async fn routing_unlink_config( state, &req, payload.into_inner(), - |state, auth: oss_auth::AuthenticationData, payload_req| { + |state, auth: auth::AuthenticationData, payload_req| { routing::unlink_routing_config(state, auth.merchant_account, payload_req) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -188,11 +188,11 @@ pub async fn routing_unlink_config( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -213,11 +213,11 @@ pub async fn routing_update_default_config( state, &req, json_payload.into_inner(), - |state, auth: oss_auth::AuthenticationData, updated_config| { + |state, auth: auth::AuthenticationData, updated_config| { routing::update_default_routing_config(state, auth.merchant_account, updated_config) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -236,11 +236,11 @@ pub async fn routing_retrieve_default_config( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::retrieve_default_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -268,7 +268,7 @@ pub async fn routing_retrieve_linked_config( routing::retrieve_linked_routing_config(state, auth.merchant_account, query_params) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -284,11 +284,11 @@ pub async fn routing_retrieve_linked_config( state, &req, (), - |state, auth: oss_auth::AuthenticationData, _| { + |state, auth: auth::AuthenticationData, _| { routing::retrieve_linked_routing_config(state, auth.merchant_account) }, #[cfg(not(feature = "release"))] - auth::auth_type(&oss_auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), #[cfg(feature = "release")] &auth::JWTAuth, api_locking::LockAction::NotApplicable, @@ -296,3 +296,60 @@ pub async fn routing_retrieve_linked_config( .await } } + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_retrieve_default_config_for_profiles( + state: web::Data, + req: HttpRequest, +) -> impl Responder { + oss_api::server_wrap( + Flow::RoutingRetrieveDefaultConfig, + state, + &req, + (), + |state, auth: auth::AuthenticationData, _| { + routing::retrieve_default_routing_config_for_profiles(state, auth.merchant_account) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +#[cfg(feature = "olap")] +#[instrument(skip_all)] +pub async fn routing_update_default_config_for_profile( + state: web::Data, + req: HttpRequest, + path: web::Path, + json_payload: web::Json>, +) -> impl Responder { + let routing_payload_wrapper = routing_types::RoutingPayloadWrapper { + updated_config: json_payload.into_inner(), + profile_id: path.into_inner(), + }; + oss_api::server_wrap( + Flow::RoutingUpdateDefaultConfig, + state, + &req, + routing_payload_wrapper, + |state, auth: auth::AuthenticationData, wrapper| { + routing::update_default_routing_config_for_profile( + state, + auth.merchant_account, + wrapper.updated_config, + wrapper.profile_id, + ) + }, + #[cfg(not(feature = "release"))] + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + #[cfg(feature = "release")] + &auth::JWTAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs new file mode 100644 index 000000000000..0ff11ce087b5 --- /dev/null +++ b/crates/router/src/routes/user.rs @@ -0,0 +1,31 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::user as user_api; +use router_env::Flow; + +use super::AppState; +use crate::{ + core::{api_locking, user}, + services::{ + api, + authentication::{self as auth}, + }, +}; + +pub async fn user_connect_account( + state: web::Data, + http_req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::UserConnectAccount; + let req_payload = json_payload.into_inner(); + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + req_payload.clone(), + |state, _, req_body| user::connect_account(state, req_body), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 631e9a5c189d..21f33f0fa0b8 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -1,6 +1,8 @@ pub mod api; pub mod authentication; pub mod encryption; +#[cfg(feature = "olap")] +pub mod jwt; pub mod logger; #[cfg(feature = "kms")] diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index bb0e70b4b27b..0a8b84ffd11c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -98,11 +98,7 @@ pub trait ConnectorValidation: ConnectorCommon { } fn validate_if_surcharge_implemented(&self) -> CustomResult<(), errors::ConnectorError> { - Err(errors::ConnectorError::NotImplemented(format!( - "Surcharge not implemented for {}", - self.id() - )) - .into()) + Err(errors::ConnectorError::NotImplemented(format!("Surcharge for {}", self.id())).into()) } } @@ -830,6 +826,7 @@ where .as_millis(); let mut serialized_response = None; + let mut overhead_latency = None; let status_code = match output.as_ref() { Ok(res) => { if let ApplicationResponse::Json(data) = res { @@ -839,6 +836,19 @@ where .attach_printable("Failed to serialize json response") .change_context(errors::ApiErrorResponse::InternalServerError.switch())?, ); + } else if let ApplicationResponse::JsonWithHeaders((data, headers)) = res { + serialized_response.replace( + masking::masked_serialize(&data) + .into_report() + .attach_printable("Failed to serialize json response") + .change_context(errors::ApiErrorResponse::InternalServerError.switch())?, + ); + + if let Some((_, value)) = headers.iter().find(|(key, _)| key == X_HS_LATENCY) { + if let Ok(external_latency) = value.parse::() { + overhead_latency.replace(external_latency); + } + } } event_type = res.get_api_event_type().or(event_type); @@ -854,6 +864,7 @@ where status_code, serialized_request, serialized_response, + overhead_latency, auth_type, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 0a7f5189b904..da4dec2eec8a 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -9,6 +9,10 @@ use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; use masking::{PeekInterface, StrongSecret}; use serde::Serialize; +#[cfg(feature = "olap")] +use super::jwt; +#[cfg(feature = "olap")] +use crate::consts; use crate::{ configs::settings, core::{ @@ -71,6 +75,37 @@ impl AuthenticationType { } } +#[derive(serde::Serialize, serde::Deserialize)] +pub struct AuthToken { + pub user_id: String, + pub merchant_id: String, + pub role_id: String, + pub exp: u64, + pub org_id: String, +} + +#[cfg(feature = "olap")] +impl AuthToken { + pub async fn new_token( + user_id: String, + merchant_id: String, + role_id: String, + settings: &settings::Settings, + org_id: String, + ) -> errors::UserResult { + let exp_duration = std::time::Duration::from_secs(consts::JWT_TOKEN_TIME_IN_SECS); + let exp = jwt::generate_exp(exp_duration)?.as_secs(); + let token_payload = Self { + user_id, + merchant_id, + role_id, + exp, + org_id, + }; + jwt::generate_jwt(&token_payload, settings).await + } +} + pub trait AuthInfo { fn get_merchant_id(&self) -> Option<&str>; } @@ -366,14 +401,58 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<((), AuthenticationType)> { - let mut token = get_jwt(request_headers)?; - token = strip_jwt_token(token)?; - decode_jwt::(token, state) - .await - .map(|_| ((), AuthenticationType::NoAuth)) + let payload = parse_jwt_payload::(request_headers, state).await?; + Ok(( + (), + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) + } +} + +pub struct JWTAuthMerchantFromRoute { + pub merchant_id: String, +} + +#[async_trait] +impl AuthenticateAndFetch<(), A> for JWTAuthMerchantFromRoute +where + A: AppStateInfo + Sync, +{ + async fn authenticate_and_fetch( + &self, + request_headers: &HeaderMap, + state: &A, + ) -> RouterResult<((), AuthenticationType)> { + let payload = parse_jwt_payload::(request_headers, state).await?; + + // Check if token has access to merchantID that has been requested through query param + if payload.merchant_id != self.merchant_id { + return Err(report!(errors::ApiErrorResponse::InvalidJwtToken)); + } + Ok(( + (), + AuthenticationType::MerchantJWT { + merchant_id: payload.merchant_id, + user_id: Some(payload.user_id), + }, + )) } } +pub async fn parse_jwt_payload(headers: &HeaderMap, state: &A) -> RouterResult +where + T: serde::de::DeserializeOwned, + A: AppStateInfo + Sync, +{ + let token = get_jwt_from_authorization_header(headers)?; + let payload = decode_jwt(token, state).await?; + + Ok(payload) +} + #[derive(serde::Deserialize)] struct JwtAuthPayloadFetchMerchantAccount { merchant_id: String, @@ -389,9 +468,9 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { - let mut token = get_jwt(request_headers)?; - token = strip_jwt_token(token)?; - let payload = decode_jwt::(token, state).await?; + let payload = + parse_jwt_payload::(request_headers, state) + .await?; let key_store = state .store() .get_merchant_key_store_by_merchant_id( @@ -595,14 +674,16 @@ pub fn get_header_value_by_key(key: String, headers: &HeaderMap) -> RouterResult .transpose() } -pub fn get_jwt(headers: &HeaderMap) -> RouterResult<&str> { +pub fn get_jwt_from_authorization_header(headers: &HeaderMap) -> RouterResult<&str> { headers .get(crate::headers::AUTHORIZATION) .get_required_value(crate::headers::AUTHORIZATION)? .to_str() .into_report() .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to convert JWT token to string") + .attach_printable("Failed to convert JWT token to string")? + .strip_prefix("Bearer ") + .ok_or(errors::ApiErrorResponse::InvalidJwtToken.into()) } pub fn strip_jwt_token(token: &str) -> RouterResult<&str> { diff --git a/crates/router/src/services/jwt.rs b/crates/router/src/services/jwt.rs new file mode 100644 index 000000000000..b69a21583919 --- /dev/null +++ b/crates/router/src/services/jwt.rs @@ -0,0 +1,42 @@ +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use jsonwebtoken::{encode, EncodingKey, Header}; +use masking::PeekInterface; + +use super::authentication; +use crate::{configs::settings::Settings, core::errors::UserErrors}; + +pub fn generate_exp( + exp_duration: std::time::Duration, +) -> CustomResult { + std::time::SystemTime::now() + .checked_add(exp_duration) + .ok_or(UserErrors::InternalServerError)? + .duration_since(std::time::UNIX_EPOCH) + .into_report() + .change_context(UserErrors::InternalServerError) +} + +pub async fn generate_jwt( + claims_data: &T, + settings: &Settings, +) -> CustomResult +where + T: serde::ser::Serialize, +{ + let jwt_secret = authentication::get_jwt_secret( + &settings.secrets, + #[cfg(feature = "kms")] + external_services::kms::get_kms_client(&settings.kms).await, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to obtain JWT secret")?; + encode( + &Header::default(), + claims_data, + &EncodingKey::from_secret(jwt_secret.peek().as_bytes()), + ) + .into_report() + .change_context(UserErrors::InternalServerError) +} diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index f2e86a4bf335..7e9725d1a3b7 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -547,11 +547,31 @@ pub trait Capturable { fn get_capture_amount(&self) -> Option { Some(0) } + fn get_surcharge_amount(&self) -> Option { + None + } + fn get_tax_on_surcharge_amount(&self) -> Option { + None + } } impl Capturable for PaymentsAuthorizeData { fn get_capture_amount(&self) -> Option { - Some(self.amount) + let final_amount = self + .surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.final_amount); + final_amount.or(Some(self.amount)) + } + fn get_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.surcharge_amount) + } + fn get_tax_on_surcharge_amount(&self) -> Option { + self.surcharge_details + .as_ref() + .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount) } } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index e815740cac48..67d2d37f4fea 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -16,6 +16,7 @@ pub mod webhooks; use std::{fmt::Debug, str::FromStr}; +use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ @@ -214,6 +215,30 @@ pub struct SessionConnectorData { pub business_sub_label: Option, } +/// Session Surcharge type +pub enum SessionSurchargeDetails { + /// Surcharge is calculated by hyperswitch + Calculated(SurchargeMetadata), + /// Surcharge is sent by merchant + PreDetermined(SurchargeDetailsResponse), +} + +impl SessionSurchargeDetails { + pub fn fetch_surcharge_details( + &self, + payment_method: &enums::PaymentMethod, + payment_method_type: &enums::PaymentMethodType, + card_network: Option<&enums::CardNetwork>, + ) -> Option { + match self { + Self::Calculated(surcharge_metadata) => surcharge_metadata + .get_surcharge_details(payment_method, payment_method_type, card_network) + .cloned(), + Self::PreDetermined(surcharge_details) => Some(surcharge_details.clone()), + } + } +} + pub enum ConnectorChoice { SessionMultiple(Vec), StraightThrough(serde_json::Value), diff --git a/crates/router/src/types/domain.rs b/crates/router/src/types/domain.rs index 44123850d468..c93f96eaf09e 100644 --- a/crates/router/src/types/domain.rs +++ b/crates/router/src/types/domain.rs @@ -5,9 +5,13 @@ mod merchant_account; mod merchant_connector_account; mod merchant_key_store; pub mod types; +#[cfg(feature = "olap")] +pub mod user; pub use address::*; pub use customer::*; pub use merchant_account::*; pub use merchant_connector_account::*; pub use merchant_key_store::*; +#[cfg(feature = "olap")] +pub use user::*; diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs new file mode 100644 index 000000000000..c053b0f15448 --- /dev/null +++ b/crates/router/src/types/domain/user.rs @@ -0,0 +1,483 @@ +use std::{collections::HashSet, ops, str::FromStr}; + +use api_models::{admin as admin_api, organization as api_org, user as user_api}; +use common_utils::pii; +use diesel_models::{ + enums::UserStatus, + organization as diesel_org, + organization::Organization, + user as storage_user, + user_role::{UserRole, UserRoleNew}, +}; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface, Secret}; +use once_cell::sync::Lazy; +use unicode_segmentation::UnicodeSegmentation; + +use crate::{ + consts::user as consts, + core::{ + admin, + errors::{UserErrors, UserResult}, + }, + db::StorageInterface, + routes::AppState, + services::authentication::AuthToken, + types::transformers::ForeignFrom, + utils::user::password, +}; + +#[derive(Clone)] +pub struct UserName(Secret); + +impl UserName { + pub fn new(name: Secret) -> UserResult { + let name = name.expose(); + let is_empty_or_whitespace = name.trim().is_empty(); + let is_too_long = name.graphemes(true).count() > consts::MAX_NAME_LENGTH; + + let forbidden_characters = ['/', '(', ')', '"', '<', '>', '\\', '{', '}']; + let contains_forbidden_characters = name.chars().any(|g| forbidden_characters.contains(&g)); + + if is_empty_or_whitespace || is_too_long || contains_forbidden_characters { + Err(UserErrors::NameParsingError.into()) + } else { + Ok(Self(name.into())) + } + } + + pub fn get_secret(self) -> Secret { + self.0 + } +} + +impl TryFrom for UserName { + type Error = error_stack::Report; + + fn try_from(value: pii::Email) -> UserResult { + Self::new(Secret::new( + value + .peek() + .split_once('@') + .ok_or(UserErrors::InvalidEmailError)? + .0 + .to_string(), + )) + } +} + +#[derive(Clone, Debug)] +pub struct UserEmail(pii::Email); + +static BLOCKED_EMAIL: Lazy> = Lazy::new(|| { + let blocked_emails_content = include_str!("../../utils/user/blocker_emails.txt"); + let blocked_emails: HashSet = blocked_emails_content + .lines() + .map(|s| s.trim().to_owned()) + .collect(); + blocked_emails +}); + +impl UserEmail { + pub fn new(email: Secret) -> UserResult { + let email_string = email.expose(); + let email = + pii::Email::from_str(&email_string).change_context(UserErrors::EmailParsingError)?; + + if validator::validate_email(&email_string) { + let (_username, domain) = match email_string.as_str().split_once('@') { + Some((u, d)) => (u, d), + None => return Err(UserErrors::EmailParsingError.into()), + }; + + if BLOCKED_EMAIL.contains(domain) { + return Err(UserErrors::InvalidEmailError.into()); + } + Ok(Self(email)) + } else { + Err(UserErrors::EmailParsingError.into()) + } + } + + pub fn from_pii_email(email: pii::Email) -> UserResult { + let email_string = email.peek(); + if validator::validate_email(email_string) { + let (_username, domain) = match email_string.split_once('@') { + Some((u, d)) => (u, d), + None => return Err(UserErrors::EmailParsingError.into()), + }; + if BLOCKED_EMAIL.contains(domain) { + return Err(UserErrors::InvalidEmailError.into()); + } + Ok(Self(email)) + } else { + Err(UserErrors::EmailParsingError.into()) + } + } + + pub fn into_inner(self) -> pii::Email { + self.0 + } + + pub fn get_secret(self) -> Secret { + (*self.0).clone() + } +} + +impl TryFrom for UserEmail { + type Error = error_stack::Report; + + fn try_from(value: pii::Email) -> Result { + Self::from_pii_email(value) + } +} + +impl ops::Deref for UserEmail { + type Target = Secret; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone)] +pub struct UserPassword(Secret); + +impl UserPassword { + pub fn new(password: Secret) -> UserResult { + let password = password.expose(); + if password.is_empty() { + Err(UserErrors::PasswordParsingError.into()) + } else { + Ok(Self(password.into())) + } + } + + pub fn get_secret(&self) -> Secret { + self.0.clone() + } +} + +#[derive(Clone)] +pub struct UserCompanyName(String); + +impl UserCompanyName { + pub fn new(company_name: String) -> UserResult { + let company_name = company_name.trim(); + let is_empty_or_whitespace = company_name.is_empty(); + let is_too_long = company_name.graphemes(true).count() > consts::MAX_COMPANY_NAME_LENGTH; + + let is_all_valid_characters = company_name + .chars() + .all(|x| x.is_alphanumeric() || x.is_ascii_whitespace() || x == '_'); + if is_empty_or_whitespace || is_too_long || !is_all_valid_characters { + Err(UserErrors::CompanyNameParsingError.into()) + } else { + Ok(Self(company_name.to_string())) + } + } + + pub fn get_secret(self) -> String { + self.0 + } +} + +#[derive(Clone)] +pub struct NewUserOrganization(diesel_org::OrganizationNew); + +impl NewUserOrganization { + pub async fn insert_org_in_db(self, state: AppState) -> UserResult { + state + .store + .insert_organization(self.0) + .await + .map_err(|e| { + if e.current_context().is_db_unique_violation() { + e.change_context(UserErrors::DuplicateOrganizationId) + } else { + e.change_context(UserErrors::InternalServerError) + } + }) + .attach_printable("Error while inserting organization") + } + + pub fn get_organization_id(&self) -> String { + self.0.org_id.clone() + } +} + +impl From for NewUserOrganization { + fn from(_value: user_api::ConnectAccountRequest) -> Self { + let new_organization = api_org::OrganizationNew::new(None); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + +#[derive(Clone)] +pub struct NewUserMerchant { + merchant_id: String, + company_name: Option, + new_organization: NewUserOrganization, +} + +impl NewUserMerchant { + pub fn get_company_name(&self) -> Option { + self.company_name.clone().map(UserCompanyName::get_secret) + } + + pub fn get_merchant_id(&self) -> String { + self.merchant_id.clone() + } + + pub fn get_new_organization(&self) -> NewUserOrganization { + self.new_organization.clone() + } + + pub async fn check_if_already_exists_in_db(&self, state: AppState) -> UserResult<()> { + if state + .store + .get_merchant_key_store_by_merchant_id( + self.get_merchant_id().as_str(), + &state.store.get_master_key().to_vec().into(), + ) + .await + .is_ok() + { + return Err(UserErrors::MerchantAccountCreationError(format!( + "Merchant with {} already exists", + self.get_merchant_id() + ))) + .into_report(); + } + Ok(()) + } + + pub async fn create_new_merchant_and_insert_in_db(&self, state: AppState) -> UserResult<()> { + self.check_if_already_exists_in_db(state.clone()).await?; + Box::pin(admin::create_merchant_account( + state.clone(), + admin_api::MerchantAccountCreate { + merchant_id: self.get_merchant_id(), + metadata: None, + locker_id: None, + return_url: None, + merchant_name: self.get_company_name().map(Secret::new), + webhook_details: None, + publishable_key: None, + organization_id: Some(self.new_organization.get_organization_id()), + merchant_details: None, + routing_algorithm: None, + parent_merchant_id: None, + payment_link_config: None, + sub_merchants_enabled: None, + frm_routing_algorithm: None, + intent_fulfillment_time: None, + payout_routing_algorithm: None, + primary_business_details: None, + payment_response_hash_key: None, + enable_payment_response_hash: None, + redirect_to_merchant_with_http_post: None, + }, + )) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error while creating a merchant")?; + Ok(()) + } +} + +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::ConnectAccountRequest) -> UserResult { + let merchant_id = format!("merchant_{}", common_utils::date_time::now_unix_timestamp()); + let new_organization = NewUserOrganization::from(value); + + Ok(Self { + company_name: None, + merchant_id, + new_organization, + }) + } +} + +#[derive(Clone)] +pub struct NewUser { + user_id: String, + name: UserName, + email: UserEmail, + password: UserPassword, + new_merchant: NewUserMerchant, +} + +impl NewUser { + pub fn get_user_id(&self) -> String { + self.user_id.clone() + } + + pub fn get_email(&self) -> UserEmail { + self.email.clone() + } + + pub fn get_name(&self) -> Secret { + self.name.clone().get_secret() + } + + pub fn get_new_merchant(&self) -> NewUserMerchant { + self.new_merchant.clone() + } + + pub async fn insert_user_in_db( + &self, + db: &dyn StorageInterface, + ) -> UserResult { + match db.insert_user(self.clone().try_into()?).await { + Ok(user) => Ok(user.into()), + Err(e) => { + if e.current_context().is_db_unique_violation() { + return Err(e.change_context(UserErrors::UserExists)); + } else { + return Err(e.change_context(UserErrors::InternalServerError)); + } + } + } + .attach_printable("Error while inserting user") + } + + pub async fn insert_user_and_merchant_in_db( + &self, + state: AppState, + ) -> UserResult { + let db = state.store.as_ref(); + let merchant_id = self.get_new_merchant().get_merchant_id(); + self.new_merchant + .create_new_merchant_and_insert_in_db(state.clone()) + .await?; + let created_user = self.insert_user_in_db(db).await; + if created_user.is_err() { + let _ = admin::merchant_account_delete(state, merchant_id).await; + }; + created_user + } + + pub async fn insert_user_role_in_db( + self, + state: AppState, + role_id: String, + user_status: UserStatus, + ) -> UserResult { + let now = common_utils::date_time::now(); + let user_id = self.get_user_id(); + + state + .store + .insert_user_role(UserRoleNew { + merchant_id: self.get_new_merchant().get_merchant_id(), + status: user_status, + created_by: user_id.clone(), + last_modified_by: user_id.clone(), + user_id, + role_id, + created_at: now, + last_modified_at: now, + org_id: self + .get_new_merchant() + .get_new_organization() + .get_organization_id(), + }) + .await + .change_context(UserErrors::InternalServerError) + } +} + +impl TryFrom for storage_user::UserNew { + type Error = error_stack::Report; + + fn try_from(value: NewUser) -> UserResult { + let hashed_password = password::generate_password_hash(value.password.get_secret())?; + Ok(Self { + user_id: value.get_user_id(), + name: value.get_name(), + email: value.get_email().into_inner(), + password: hashed_password, + ..Default::default() + }) + } +} + +impl TryFrom for NewUser { + type Error = error_stack::Report; + + fn try_from(value: user_api::ConnectAccountRequest) -> UserResult { + let user_id = uuid::Uuid::new_v4().to_string(); + let email = value.email.clone().try_into()?; + let name = UserName::try_from(value.email.clone())?; + let password = UserPassword::new(value.password.clone())?; + let new_merchant = NewUserMerchant::try_from(value)?; + + Ok(Self { + user_id, + name, + email, + password, + new_merchant, + }) + } +} + +pub struct UserFromStorage(pub storage_user::User); + +impl From for UserFromStorage { + fn from(value: storage_user::User) -> Self { + Self(value) + } +} + +impl UserFromStorage { + pub fn get_user_id(&self) -> &str { + self.0.user_id.as_str() + } + + pub fn compare_password(&self, candidate: Secret) -> UserResult<()> { + match password::is_correct_password(candidate, self.0.password.clone()) { + Ok(true) => Ok(()), + Ok(false) => Err(UserErrors::InvalidCredentials.into()), + Err(e) => Err(e), + } + } + + pub fn get_name(&self) -> Secret { + self.0.name.clone() + } + + pub fn get_email(&self) -> pii::Email { + self.0.email.clone() + } + + pub async fn get_jwt_auth_token(&self, state: AppState, org_id: String) -> UserResult { + let role_id = self.get_role_from_db(state.clone()).await?.role_id; + let merchant_id = state + .store + .find_user_role_by_user_id(self.get_user_id()) + .await + .change_context(UserErrors::InternalServerError)? + .merchant_id; + AuthToken::new_token( + self.0.user_id.clone(), + merchant_id, + role_id, + &state.conf, + org_id, + ) + .await + } + + pub async fn get_role_from_db(&self, state: AppState) -> UserResult { + state + .store + .find_user_role_by_user_id(self.get_user_id()) + .await + .change_context(UserErrors::InternalServerError) + } +} diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 558044028f7a..aadb714e8ce2 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -1,6 +1,8 @@ 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; diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs new file mode 100644 index 000000000000..c72e4b9feb3c --- /dev/null +++ b/crates/router/src/utils/user.rs @@ -0,0 +1 @@ +pub mod password; diff --git a/crates/router/src/utils/user/blocker_emails.txt b/crates/router/src/utils/user/blocker_emails.txt new file mode 100644 index 000000000000..e29e1b2d86f4 --- /dev/null +++ b/crates/router/src/utils/user/blocker_emails.txt @@ -0,0 +1,2349 @@ +020.co.uk +123.com +123box.net +123india.com +123mail.cl +123mail.org +123qwe.co.uk +138mail.com +141.ro +150mail.com +150ml.com +16mail.com +1963chevrolet.com +1963pontiac.com +1netdrive.com +1st-website.com +1stpd.net +2-mail.com +20after4.com +21cn.com +24h.co.jp +24horas.com +271soundview.com +2die4.com +2mydns.com +2net.us +3000.it +3ammagazine.com +3email.com +3xl.net +444.net +4email.com +4email.net +4newyork.com +50mail.com +55mail.cc +5fm.za.com +6210.hu +6sens.com +702mail.co.za +7110.hu +8848.net +8m.com +8m.net +8x.com.br +8u8.com +8u8.hk +8u8.tw +a-topmail.at +about.com +abv.bg +acceso.or.cr +access4less.net +accessgcc.com +acmemail.net +adiga.com +adinet.com.uy +adres.nl +advalvas.be +aeiou.pt +aeneasmail.com +afrik.com +afropoets.com +aggies.com +ahaa.dk +aichi.com +aim.com +airpost.net +aiutamici.com +aklan.com +aknet.kg +alabama.usa.com +alaska.usa.com +alavatotal.com +albafind.com +albawaba.com +alburaq.net +aldeax.com +aldeax.com.ar +alex4all.com +aliyun.com +alexandria.cc +algeria.com +alice.it +allmail.net +alskens.dk +altavista.se +altbox.org +alternativagratis.com +alum.com +alunos.unipar.br +alvilag.hu +amenworld.com +america.hm +americamail.com +amnetsal.com +amorous.com +ananzi.co.za +anet.ne.jp +anfmail.com +angelfire.com +animail.net +aniverse.com +anjungcafe.com +another.com +antedoonsub.com +antwerpen.com +anunciador.net +anytimenow.com +aon.at +apexmail.com +apollo.lv +approvers.net +aprava.com +apropo.ro +arcor.de +argentina.com +arizona.usa.com +arkansas.usa.com +armmail.com +army.com +arnet.com.ar +aroma.com +arrl.net +aruba.it +asheville.com +asia-links.com +asiamail.com +assala.com +assamesemail.com +asurfer.com +atl.lv +atlas.cz +atlas.sk +atozasia.com +atreillou.com +att.net +au.ru +aubenin.com +aus-city.com +aussiemail.com.au +avasmail.com.mv +axarnet.com +ayna.com +azet.sk +babbalu.com +badgers.com +bakpaka.com +bakpaka.net +balochistan.org +baluch.com +bama-fan.com +bancora.net +bankersmail.com +barlick.net +beeebank.com +beehive.org +been-there.com +beirut.com +belizehome.com +belizemail.net +belizeweb.com +bellsouth.net +berlin.de +bestmail.us +bflomail.com +bgnmail.com +bharatmail.com +big-orange.com +bigboss.cz +bigfoot.com +bigger.com +bigmailbox.com +bigmir.net +bigstring.com +bip.net +bigpond.com +bitwiser.com +biz.by +bizhosting.com +black-sea.ro +blackburnmail.com +blackglobalnetwork.net +blink182.net +blue.devils.com +bluebottle.com +bluemail.ch +blumail.org +blvds.com +bol.com.br +bolando.com +bollywood2000.com +bollywoodz.com +bombka.dyn.pl +bonbon.net +boom.com +bootmail.com +bostonoffice.com +box.az +boxbg.com +boxemail.com +brain.com.pk +brasilia.net +bravanese.com +brazilmail.com.br +breathe.com +brestonline.com +brfree.com.br +brujula.net +btcc.org +buffaloes.com +bulgaria.com +bulldogs.com +bumerang.ro +burntmail.com +butch-femme.net +buzy.com +buzzjakkerz.com +c-box.cz +c3.hu +c4.com +cadinfo.net +calcfacil.com.br +calcware.org +california.usa.com +callnetuk.com +camaroclubsweden.com +canada-11.com +canada.com +canal21.com +canoemail.com +caramail.com +cardblvd.com +care-mail.com +care2.com +caress.com +carioca.net +cashette.com +casino.com +casinomail.com +cataloniamail.com +catalunyamail.com +cataz.com +catcha.com +catholic.org +caths.co.uk +caxess.net +cbrmail.com +cc.lv +cemelli.com +centoper.it +centralpets.com +centrum.cz +centrum.sk +centurylink.net +cercaziende.it +cgac.es +chaiyo.com +chaiyomail.com +chance2mail.com +channelonetv.com +charter.net +chattown.com +checkitmail.at +chelny.com +cheshiremail.com +chil-e.com +chillimail.com +china.com +christianmail.org +ciaoweb.it +cine.com +ciphercom.net +circlemail.com +cititrustbank1.cjb.net +citromail.hu +citynetusa.com +ciudad.com.ar +claramail.com +classicmail.co.za +cliffhanger.com +clix.pt +close2you.net +cluemail.com +clujnapoca.ro +collegeclub.com +colombia.com +colorado.usa.com +comcast.net +comfortable.com +compaqnet.fr +compuserve.com +computer.net +computermail.net +computhouse.com +conevyt.org.mx +connect4free.net +connecticut.usa.com +coolgoose.com +coolkiwi.com +coollist.com +coxinet.net +coolmail.com +coolmail.net +coolsend.com +cooltoad.com +cooperation.net +copacabana.com +copticmail.com +corporateattorneys.com +corporation.net +correios.net.br +correomagico.com +cosmo.com +cosmosurf.net +cougars.com +count.com +countrybass.com +couple.com +criticalpath.net +critterpost.com +crosspaths.net +crosswinds.net +cryingmail.com +cs.com +csucsposta.hu +cumbriamail.com +curio-city.com +custmail.com +cwazy.co.uk +cwazy.net +cww.de +cyberaccess.com.pk +cybergirls.dk +cyberguys.dk +cybernet.it +cymail.net +dabsol.net +dada.net +dadanet.it +dailypioneer.com +damuc.org.br +dansegulvet.com +darkhorsefan.net +data54.com +davegracey.com +dayzers.com +daum.net +dbmail.com +dcemail.com +dcsi.net +deacons.com +deadlymob.org +deal-maker.com +dearriba.com +degoo.com +delajaonline.org +delaware.usa.com +delfi.lv +delhimail.com +demon.deacons.com +desertonline.com +desidrivers.com +deskpilot.com +despammed.com +detik.com +devils.com +dexara.net +dhmail.net +di-ve.com +didamail.com +digitaltrue.com +direccion.com +director-general.com +diri.com +discardmail.com +discoverymail.net +disinfo.net +djmillenium.com +dmailman.com +dnsmadeeasy.com +do.net.ar +dodgeit.com +dogmail.co.uk +doityourself.com +domaindiscover.com +domainmanager.com +doneasy.com +dontexist.org +dores.com +dostmail.com +dot5hosting.com +dotcom.fr +dotnow.com +dott.it +doubt.com +dplanet.ch +dragoncon.net +dragonfans.com +dropzone.com +dserver.org +dubaiwebcity.com +dublin.ie +dustdevil.com +dynamitemail.com +dyndns.org +e-apollo.lv +e-hkma.com +e-mail.cz +e-mail.ph +e-mailanywhere.com +e-milio.com +e-tapaal.com +e-webtec.com +earthalliance.com +earthling.net +eastmail.com +eastrolog.com +easy-pages.com +easy.com +easyinfomail.co.za +easypeasy.com +echina.com +ecn.org +ecplaza.net +eircom.net +edsamail.com.ph +educacao.te.pt +edumail.co.za +eeism.com +ego.co.th +ekolay.net +elforotv.com.ar +elitemail.org +elsitio.com +eltimon.com +elvis.com +email.com.br +email.cz +email.bg +email.it +email.lu +email.lviv.ua +email.nu +email.ro +email.si +email2me.com +emailacc.com +emailaccount.com +emailaddresses.com +emailchoice.com +emailcorner.net +emailn.de +emailengine.net +emailengine.org +emailgaul.com +emailgroups.net +emailhut.net +emailpinoy.com +emailplanet.com +emailplus.org +emailuser.net +ematic.com +embarqmail.com +embroideryforums.com +eml.cc +emoka.ro +emptymail.com +enel.net +enelpunto.net +england.com +enterate.com.ar +entryweb.it +entusiastisk.com +enusmail.com +epatra.com +epix.net +epomail.com +epost.de +eprompter.com +eqqu.com +eramail.co.za +eresmas.com +eriga.lv +ertelecom.ru +esde-s.org +esfera.cl +estadao.com.br +etllao.com +euromail.net +euroseek.com +euskalmail.com +evafan.com +everyday.com.kh +everymail.net +everyone.net +execs2k.com +executivemail.co.za +expn.com +ezilon.com +ezrs.com +f-m.fm +facilmail.com +fadrasha.net +fadrasha.org +faithhighway.com +faithmail.com +familymailbox.com +familyroll.com +familysafeweb.net +fan.com +fan.net +faroweb.com +fast-email.com +fast-mail.org +fastem.com +fastemail.us +fastemailer.com +fastermail.com +fastest.cc +fastimap.com +fastmailbox.net +fastmessaging.com +fastwebmail.it +fawz.net +fea.st +federalcontractors.com +fedxmail.com +feelings.com +female.ru +fepg.net +ffanet.com +fiberia.com +filipinolinks.com +financesource.com +findmail.com +fiscal.net +flashmail.com +flipcode.com +florida.usa.com +floridagators.com +fmail.co.uk +fmailbox.com +fmgirl.com +fmguy.com +fnmail.com +footballer.com +foxmail.com +forfree.at +forsythmissouri.org +fortuncity.com +forum.dk +free.com.pe +free.fr +free.net.nz +freeaccess.nl +freegates.be +freeghana.com +freehosting.nl +freei.co.th +freeler.nl +freemail.globalsite.com.br +freemuslim.net +freenet.de +freenet.kg +freeola.net +freepgs.com +freesbee.fr +freeservers.com +freestart.hu +freesurf.ch +freesurf.fr +freesurf.nl +freeuk.com +freeuk.net +freeweb.it +freewebemail.com +freeyellow.com +frisurf.no +frontiernet.net +fsmail.net +fsnet.co.uk +ftml.net +fuelie.org +fun-greetings-jokes.com +fun.21cn.com +fusemail.com +fut.es +gala.net +galmail.co.za +gamebox.net +gamecocks.com +gawab.com +gay.com +gaymailbox.com +gaza.net +gazeta.pl +gci.net +gdi.net +geeklife.com +gemari.or.id +genxemail.com +geopia.com +georgia.usa.com +getmail.no +ggaweb.ch +giga4u.de +gjk.dk +glay.org +glendale.net +globalfree.it +globomail.com +globalpinoy.com +globalsite.com.br +globalum.com +globetrotter.net +go-bama.com +go-cavs.com +go-chargers.com +go-dawgs.com +go-gators.com +go-hogs.com +go-irish.com +go-spartans.com +go-tigers.com +go.aggies.com +go.air-force.com +go.badgers.com +go.big-orange.com +go.blue.devils.com +go.buffaloes.com +go.bulldogs.com +go.com +go.cougars.com +go.dores.com +go.gamecocks.com +go.huskies.com +go.longhorns.com +go.mustangs.com +go.rebels.com +go.ro +go.ru +go.terrapins.com +go.wildcats.com +go.wolverines.com +go.yellow-jackets.com +go2net.com +go4.it +gofree.co.uk +golfemail.com +goliadtexas.com +gomail.com.ua +gonowmail.com +gonuts4free.com +googlemail.com +goplay.com +gorontalo.net +gotmail.com +gotomy.com +govzone.com +grad.com +graffiti.net +gratisweb.com +gtechnics.com +guate.net +guessmail.com +gwalla.com +h-mail.us +haberx.com +hailmail.net +halejob.com +hamptonroads.com +handbag.com +hanmail.net +happemail.com +happycounsel.com +hawaii.com +hawaii.usa.com +hayahaya.tg +hedgeai.com +heesun.net +heremail.com +hetnet.nl +highveldmail.co.za +hildebrands.de +hingis.org +hispavista.com +hitmanrecords.com +hockeyghiaccio.com +hockeymail.com +holapuravida.com +home.no.net +home.ro +home.se +homelocator.com +homemail.co.za +homenetmail.com +homestead.com +homosexual.net +hongkong.com +hong-kong-1.com +hopthu.com +hosanna.net +hot.ee +hotbot.com +hotbox.ru +hotcoolmail.com +hotdak.com +hotfire.net +hotinbox.com +hotpop.com +hotvoice.com +hour.com +howling.com +huhmail.com +humour.com +hurra.de +hush.ai +hush.com +hushmail.com +huskies.com +hutchcity.com +i-france.com +i-p.com +i12.com +i2828.com +ibatam.com +ibest.com.br +ibizdns.com +icafe.com +ice.is +icestorm.com +icq.com +icqmail.com +icrazy.com +id.ru +idaho.usa.com +idirect.com +idncafe.com +ieg.com.br +iespalomeras.net +iespana.es +ifrance.com +ig.com.br +ignazio.it +illinois.usa.com +ilse.net +ilse.nl +imail.ru +imailbox.com +imap-mail.com +imap.cc +imapmail.org +imel.org +in-box.net +inbox.com +inbox.ge +inbox.lv +inbox.net +inbox.ru +in.com +incamail.com +indexa.fr +india.com +indiamail.com +indiana.usa.com +indiatimes.com +induquimica.org +inet.com.ua +infinito.it +infoapex.com +infohq.com +infomail.es +infomart.or.jp +infosat.net +infovia.com.ar +inicia.es +inmail.sk +inmail24.com +inoutbox.com +intelnet.net.gt +intelnett.com +interblod.com +interfree.it +interia.pl +interlap.com.ar +intermail.hu +internet-e-mail.com +internet-mail.org +internet.lu +internetegypt.com +internetemails.net +internetkeno.com +internetmailing.net +inwind.it +iobox.com +iobox.fi +iol.it +iol.pt +iowa.usa.com +ip3.com +ipermitmail.com +iqemail.com +iquebec.com +iran.com +irangate.net +iscool.net +islandmama.com +ismart.net +isonews2.com +isonfire.com +isp9.net +ispey.com +itelgua.com +itloox.com +itmom.com +ivenus.com +iwan-fals.com +iwon.com +ixp.net +japan.com +jaydemail.com +jedrzejow.pl +jetemail.net +jingjo.net +jippii.fi +jmail.co.za +jojomail.com +jovem.te.pt +joymail.com +jubii.dk +jubiipost.dk +jumpy.it +juno.com +justemail.net +justmailz.com +k.ro +kaazoo.com +kabissa.org +kaixo.com +kalluritimes.com +kalpoint.com +kansas.usa.com +katamail.com +kataweb.it +kayafmmail.co.za +keko.com.ar +kentucky.usa.com +keptprivate.com +kimo.com +kiwitown.com +klik.it +klikni.cz +kmtn.ru +koko.com +kolozsvar.ro +kombud.com +koreanmail.com +kotaksuratku.info +krunis.com +kukamail.com +kuronowish.com +kyokodate.com +kyokofukada.net +ladymail.cz +lagoon.nc +lahaonline.com +lamalla.net +lancsmail.com +land.ru +laposte.net +latinmail.com +lawyer.com +lawyersmail.com +lawyerzone.com +lebanonatlas.com +leehom.net +leonardo.it +leonlai.net +letsjam.com +letterbox.org +letterboxes.org +levele.com +lexpress.net +libero.it +liberomail.com +libertysurf.net +libre.net +lightwines.org +linkmaster.com +linuxfreemail.com +lionsfan.com.au +livedoor.com +llandudno.com +llangollen.com +lmxmail.sk +loggain.net +loggain.nu +lolnetwork.net +london.com +longhorns.com +look.com +looksmart.co.uk +looksmart.com +looksmart.com.au +loteria.net +lotonazo.com +louisiana.usa.com +louiskoo.com +loveable.com +lovemail.com +lovingjesus.com +lpemail.com +luckymail.com +luso.pt +lusoweb.pt +luukku.com +lycosmail.com +mac.com +machinecandy.com +macmail.com +mad.scientist.com +madcrazy.com +madonno.com +madrid.com +mag2.com +magicmail.co.za +magik-net.com +mail-atlas.net +mail-awu.de +mail-box.cz +mail.by +mail-center.com +mail-central.com +mail-jp.org +mail-online.dk +mail-page.com +mail-x-change.com +mail.austria.com +mail.az +mail.de +mail.be +mail.bg +mail.bulgaria.com +mail.co.za +mail.dk +mail.ee +mail.goo.ne.jp +mail.gr +mail.lawguru.com +mail.md +mail.mn +mail.org +mail.pf +mail.pt +mail.ru +mail.yahoo.co.jp +mail15.com +mail3000.com +mail333.com +mail8.com +mailandftp.com +mailandnews.com +mailas.com +mailasia.com +mailbg.com +mailblocks.com +mailbolt.com +mailbox.as +mailbox.co.za +mailbox.gr +mailbox.hu +mailbox.sk +mailc.net +mailcan.com +mailcircuit.com +mailclub.fr +mailclub.net +maildozy.com +mailfly.com +mailforce.net +mailftp.com +mailglobal.net +mailhaven.com +mailinator.com +mailingaddress.org +mailingweb.com +mailisent.com +mailite.com +mailme.dk +mailmight.com +mailmij.nl +mailnew.com +mailops.com +mailpanda.com +mailpersonal.com +mailroom.com +mailru.com +mails.de +mailsent.net +mailserver.dk +mailservice.ms +mailsnare.net +mailsurf.com +mailup.net +mailvault.com +mailworks.org +maine.usa.com +majorana.martina-franca.ta.it +maktoob.com +malayalamtelevision.net +malayalapathram.com +male.ru +manager.de +manlymail.net +mantrafreenet.com +mantramail.com +mantraonline.com +marihuana.ro +marijuana.nl +marketweighton.com +maryland.usa.com +masrawy.com +massachusetts.usa.com +mauimail.com +mbox.com.au +mcrmail.com +me.by +me.com +medicinatv.com +meetingmall.com +megamail.pt +menara.ma +merseymail.com +mesra.net +messagez.com +metacrawler.com +mexico.com +miaoweb.net +michigan.usa.com +micro2media.com +miesto.sk +mighty.co.za +milacamn.net +milmail.com +mindless.com +mindviz.com +minnesota.usa.com +mississippi.usa.com +missouri.usa.com +mixmail.com +ml1.net +ml2clan.com +mlanime.com +mm.st +mmail.com +mobimail.mn +mobsters.com +mobstop.com +modemnet.net +modomail.com +moldova.com +moldovacc.com +monarchy.com +montana.usa.com +montevideo.com.uy +moomia.com +moose-mail.com +mosaicfx.com +motormania.com +movemail.com +mr.outblaze.com +mrspender.com +mscold.com +msnzone.cn +mundo-r.com +muslimsonline.com +mustangs.com +mxs.de +myblue.cc +mycabin.com +mycity.com +mycommail.com +mycool.com +mydomain.com +myeweb.com +myfastmail.com +myfunnymail.com +mygrande.net +mykolab.com +mygamingconsoles.com +myiris.com +myjazzmail.com +mymacmail.com +mymail.dk +mymail.ph.inter.net +mymail.ro +mynet.com +mynet.com.tr +myotw.net +myopera.com +myownemail.com +mypersonalemail.com +myplace.com +myrealbox.com +myspace.com +myt.mu +myway.com +mzgchaos.de +n2.com +n2business.com +n2mail.com +n2software.com +nabble.com +name.com +nameplanet.com +nanamail.co.il +nanaseaikawa.com +nandomail.com +naseej.com +nastything.com +national-champs.com +nativeweb.net +narod.ru +nate.com +naveganas.com +naver.com +nebraska.usa.com +nemra1.com +nenter.com +nerdshack.com +nervhq.org +net.hr +net4b.pt +net4jesus.com +net4you.at +netbounce.com +netcabo.pt +netcape.net +netcourrier.com +netexecutive.com +netfirms.com +netkushi.com +netmongol.com +netpiper.com +netposta.net +netscape.com +netscape.net +netscapeonline.co.uk +netsquare.com +nettaxi.com +netti.fi +networld.com +netzero.com +netzero.net +neustreet.com +nevada.usa.com +newhampshire.usa.com +newjersey.usa.com +newmail.com +newmail.net +newmail.ok.com +newmail.ru +newmexico.usa.com +newspaperemail.com +newyork.com +newyork.usa.com +newyorkcity.com +nfmail.com +nicegal.com +nightimeuk.com +nightly.com +nightmail.com +nightmail.ru +noavar.com +noemail.com +nonomail.com +nokiamail.com +noolhar.com +northcarolina.usa.com +northdakota.usa.com +nospammail.net +nowzer.com +ny.com +nyc.com +nz11.com +nzoomail.com +o2.pl +oceanfree.net +ocsnet.net +oddpost.com +odeon.pl +odmail.com +offshorewebmail.com +ofir.dk +ohio.usa.com +oicexchange.com +ok.ru +oklahoma.usa.com +ole.com +oleco.net +olympist.net +omaninfo.com +onatoo.com +ondikoi.com +onebox.com +onenet.com.ar +onet.pl +ongc.net +oninet.pt +online.ie +online.ru +onlinewiz.com +onobox.com +open.by +openbg.com +openforyou.com +opentransfer.com +operamail.com +oplusnet.com +orange.fr +orangehome.co.uk +orange.es +orange.jo +orange.pl +orbitel.bg +orcon.net.nz +oregon.usa.com +oreka.com +organizer.net +orgio.net +orthodox.com +osite.com.br +oso.com +ourbrisbane.com +ournet.md +ourprofile.net +ourwest.com +outgun.com +ownmail.net +oxfoot.com +ozu.es +pacer.com +paginasamarillas.com +pakistanmail.com +pandawa.com +pando.com +pandora.be +paris.com +parsimail.com +parspage.com +patmail.com +pattayacitythailand.com +pc4me.us +pcpostal.com +penguinmaster.com +pennsylvania.usa.com +peoplepc.com +peopleweb.com +personal.ro +personales.com +peru.com +petml.com +phreaker.net +pigeonportal.com +pilu.com +pimagop.com +pinoymail.com +pipni.cz +pisem.net +planet-school.de +planetaccess.com +planetout.com +plasa.com +playersodds.com +playful.com +pluno.com +plusmail.com.br +pmail.net +pnetmail.co.za +pobox.ru +pobox.sk +pochtamt.ru +pochta.ru +poczta.fm +poetic.com +pogowave.com +polbox.com +pop3.ru +pop.co.th +popmail.com +poppymail.com +popsmail.com +popstar.com +portafree.com +portaldosalunos.com +portugalmail.com +portugalmail.pt +post.cz +post.expart.ne.jp +post.pl +post.sk +posta.ge +postaccesslite.com +postiloota.net +postinbox.com +postino.ch +postino.it +postmaster.co.uk +postpro.net +praize.com +press.co.jp +primposta.com +printesamargareta.ro +private.21cn.com +probemail.com +profesional.com +profession.freemail.com.br +proinbox.com +promessage.com +prontomail.com +provincial.net +publicaccounting.com +punkass.com +puppy.com.my +q.com +qatar.io +qlmail.com +qq.com +qrio.com +qsl.net +qudsmail.com +queerplaces.com +quepasa.com +quick.cz +quickwebmail.com +r-o-o-t.com +r320.hu +raakim.com +rbcmail.ru +racingseat.com +radicalz.com +radiojobbank.com +ragingbull.com +raisingadaughter.com +rallye-webmail.com +rambler.ru +ranmamail.com +ravearena.com +ravemail.co.za +razormail.com +real.ro +realemail.net +reallyfast.biz +reallyfast.info +rebels.com +recife.net +recme.net +rediffmailpro.com +redseven.de +redwhitearmy.com +relia.com +revenue.com +rexian.com +rhodeisland.usa.com +ritmes.net +rn.com +roanokemail.com +rochester-mail.com +rock.com +rocketmail.com +rockfan.com +rockinghamgateway.com +rojname.com +rol.ro +rollin.com +rome.com +romymichele.com +royal.net +rpharmacist.com +rt.nl +ru.ru +rushpost.com +russiamail.com +rxpost.net +s-mail.com +saabnet.com +sacbeemail.com +sacmail.com +safe-mail.net +safe-mailbox.com +saigonnet.vn +saint-mike.org +samilan.net +sandiego.com +sanook.com +sanriotown.com +sapibon.com +sapo.pt +saturnfans.com +sayhi.net +sbcglobal.com +scfn.net +schweiz.org +sci.fi +sciaga.pl +scrapbookscrapbook.com +seapole.com +search417.com +seark.com +sebil.com +secretservices.net +secure-jlnet.com +seductive.com +sendmail.ru +sendme.cz +sent.as +sent.at +sent.com +serga.com.ar +sermix.com +server4free.de +serverwench.com +sesmail.com +sexmagnet.com +seznam.cz +shadango.com +she.com +shuf.com +siamlocalhost.com +siamnow.net +sify.com +sinamail.com +singapore.com +singmail.com +singnet.com.sg +siraj.org +sirindia.com +sirunet.com +sister.com +sina.com +sina.cn +sinanail.com +sistersbrothers.com +sizzling.com +slamdunkfan.com +slickriffs.co.uk +slingshot.com +slo.net +slomusic.net +smartemail.co.uk +smtp.ru +snail-mail.net +sndt.net +sneakemail.com +snoopymail.com +snowboarding.com +so-simple.org +socamail.com +softhome.net +sohu.com +sol.dk +solidmail.com +soon.com +sos.lv +soundvillage.org +southcarolina.usa.com +southdakota.usa.com +space.com +spacetowns.com +spamex.com +spartapiet.com +speed-racer.com +speedpost.net +speedymail.org +spils.com +spinfinder.com +sportemail.com +spray.net +spray.no +spray.se +spymac.com +srbbs.com +srilankan.net +ssan.com +ssl-mail.com +stade.fr +stalag13.com +stampmail.com +starbuzz.com +starline.ee +starmail.com +starmail.org +starmedia.com +starspath.com +start.com.au +start.no +stribmail.com +student.com +student.ednet.ns.ca +studmail.com +sudanmail.net +suisse.org +sunbella.net +sunmail1.com +sunpoint.net +sunrise.ch +sunumail.sn +sunuweb.net +suomi24.fi +superdada.it +supereva.com +supereva.it +supermailbox.com +superposta.com +surf3.net +surfassistant.com +surfsupnet.net +surfy.net +surimail.com +surnet.cl +sverige.nu +svizzera.org +sweb.cz +swift-mail.com +swissinfo.org +swissmail.net +switzerland.org +syom.com +syriamail.com +t-mail.com +t-net.net.ve +t2mail.com +tabasheer.com +talk21.com +talkcity.com +tangmonkey.com +tatanova.com +taxcutadvice.com +techemail.com +technisamail.co.za +teenmail.co.uk +teenmail.co.za +tejary.com +telebot.com +telefonica.net +telegraf.by +teleline.es +telinco.net +telkom.net +telpage.net +telstra.com +telenet.be +telusplanet.net +tempting.com +tenchiclub.com +tennessee.usa.com +terrapins.com +texas.usa.com +texascrossroads.com +tfz.net +thai.com +thaimail.com +thaimail.net +the-fastest.net +the-quickest.com +thegame.com +theinternetemail.com +theoffice.net +thepostmaster.net +theracetrack.com +theserverbiz.com +thewatercooler.com +thewebpros.co.uk +thinkpost.net +thirdage.com +thundermail.com +tim.it +timemail.com +tin.it +tinati.net +tiscalinet.it +tjohoo.se +tkcity.com +tlcfan.com +tlen.pl +tmicha.net +todito.com +todoperros.com +tokyo.com +topchat.com +topmail.com.ar +topmail.dk +topmail.co.ie +topmail.co.in +topmail.co.nz +topmail.co.uk +topmail.co.za +topsurf.com +toquedequeda.com +torba.com +torchmail.com +totalmail.com +totalsurf.com +totonline.net +tough.com +toughguy.net +trav.se +trevas.net +tripod-mail.com +triton.net +trmailbox.com +tsamail.co.za +turbonett.com +turkey.com +tvnet.lv +twc.com +typemail.com +u2club.com +uae.ac +ubbi.com +ubbi.com.br +uboot.com +ugeek.com +uk2.net +uk2net.com +ukr.net +ukrpost.net +ukrpost.ua +uku.co.uk +ulimit.com +ummah.org +unbounded.com +unicum.de +unimail.mn +unitedemailsystems.com +universal.pt +universia.cl +universia.edu.ve +universia.es +universia.net.co +universia.net.mx +universia.pr +universia.pt +universiabrasil.net +unofree.it +uol.com.ar +uol.com.br +uole.com +uolmail.com +uomail.com +uraniomail.com +urbi.com.br +ureach.com +usanetmail.com +userbeam.com +utah.usa.com +uyuyuy.com +v-sexi.com +v3mail.com +valanides.com +vegetarisme.be +velnet.com +velocall.com +vercorreo.com +verizonmail.com +vermont.usa.com +verticalheaven.com +veryfast.biz +veryspeedy.net +vfemail.net +vietmedia.com +vip.gr +virgilio.it +virgin.net +virginia.usa.com +virtual-mail.com +visitmail.com +visto.com +vivelared.com +vjtimail.com +vnn.vn +vsnl.com +vsnl.net +vodamail.co.za +voila.fr +volkermord.com +vosforums.com +w.cn +walla.com +walla.co.il +wallet.com +wam.co.za +wanex.ge +wap.hu +wapda.com +wapicode.com +wappi.com +warpmail.net +washington.usa.com +wassup.com +waterloo.com +waumail.com +wazmail.com +wearab.net +web-mail.com.ar +web.de +web.nl +web2mail.com +webaddressbook.com +webbworks.com +webcity.ca +webdream.com +webemaillist.com +webindia123.com +webinfo.fi +webjump.com +webl-3.br.inter.net +webmail.co.yu +webmail.co.za +webmails.com +webmailv.com +webpim.cc +webspawner.com +webstation.com +websurfer.co.za +webtopmail.com +webtribe.net +webtv.net +weedmail.com +weekonline.com +weirdness.com +westvirginia.usa.com +whale-mail.com +whipmail.com +who.net +whoever.com +wildcats.com +wildmail.com +williams.net.ar +winning.com +winningteam.com +winwinhosting.com +wisconsin.usa.com +witelcom.com +witty.com +wolverines.com +wooow.it +workmail.co.za +worldcrossing.com +worldemail.com +worldmedic.com +worldonline.de +wowmail.com +wp.pl +wprost.pl +wrongmail.com +wtonetwork.com +wurtele.net +www.com +www.consulcredit.it +wyoming.usa.com +x-mail.net +xasa.com +xfreehosting.com +xmail.net +xmsg.com +xnmsn.cn +xoom.com +xtra.co.nz +xuite.net +xpectmore.com +xrea.com +xsmail.com +xzapmail.com +y7mail.com +yahala.co.il +yaho.com +yalla.com.lb +ya.com +yeah.net +ya.ru +yahoomail.com +yam.com +yamal.info +yapost.com +yawmail.com +yebox.com +yehey.com +yellow-jackets.com +yellowstone.net +yenimail.com +yepmail.net +yifan.net +yopmail.com +your-mail.com +yours.com +yourwap.com +yyhmail.com +z11.com +z6.com +zednet.co.uk +zeeman.nl +ziplip.com +zipmail.com.br +zipmax.com +zmail.pt +zmail.ru +zona-andina.net +zonai.com +zoneview.net +zonnet.nl +zoho.com +zoomshare.com +zoznam.sk +zubee.com +zuvio.com +zwallet.com +zworg.com +zybermail.com +zzn.com +126.com +139.com +163.com +188.com +189.cn +263.net +9.cn +vip.126.com +vip.163.com +vip.188.com +vip.sina.com +vip.sohu.com +vip.sohu.net +vip.tom.com +vip.qq.com +vipsohu.net +clovermail.net +mail-on.us +chewiemail.com +offcolormail.com +powdermail.com +tightmail.com +toothandmail.com +tushmail.com +openmail.cc +expressmail.dk +4xn.de +5x2.de +5x2.me +aufdrogen.de +auf-steroide.de +besser-als-du.de +brainsurfer.de +chillaxer.de +cyberkriminell.de +danneben.so +freemailen.de +freemailn.de +ist-der-mann.de +ist-der-wahnsinn.de +ist-echt.so +istecht.so +ist-genialer.de +ist-schlauer.de +ist-supersexy.de +kann.so +mag-spam.net +mega-schlau.de +muss.so +nerd4life.de +ohne-drogen-gehts.net +on-steroids.de +scheint.so +staatsterrorist.de +super-gerissen.de +unendlich-schlau.de +vip-client.de +will-keinen-spam.de +zu-geil.de +rbox.me +rbox.co +tunome.com +acatperson.com +adogperson.com +all4theskins.com +allsportsrock.com +alwaysgrilling.com +alwaysinthekitchen.com +alwayswatchingmovies.com +alwayswatchingtv.com +asylum.com +basketball-email.com +beabookworm.com +beagolfer.com +beahealthnut.com +believeinliberty.com +bestcoolcars.com +bestjobcandidate.com +besure2vote.com +bigtimecatperson.com +bigtimedogperson.com +bigtimereader.com +bigtimesportsfan.com +blackvoices.com +capsfanatic.com +capshockeyfan.com +capsred.com +car-nut.net +cat-person.com +catpeoplerule.com +chat-with-me.com +cheatasrule.com +crazy4baseball.com +crazy4homeimprovement.com +crazy4mail.com +crazyaboutfilms.net +crazycarfan.com +crazyforemail.com +crazymoviefan.com +descriptivemail.com +differentmail.com +dog-person.com +dogpeoplerule.com +easydoesit.com +expertrenovator.com +expressivemail.com +fanaticos.com +fanofbooks.com +fanofcomputers.com +fanofcooking.com +fanoftheweb.com +fieldmail.com +fleetmail.com +focusedonprofits.com +focusedonreturns.com +futboladdict.com +games.com +getintobooks.com +hail2theskins.com +hitthepuck.com +i-dig-movies.com +i-love-restaurants.com +idigcomputers.com +idigelectronics.com +idigvideos.com +ilike2helpothers.com +ilike2invest.com +ilike2workout.com +ilikeelectronics.com +ilikeworkingout.com +ilovehomeprojects.com +iloveourteam.com +iloveworkingout.com +in2autos.net +interestedinthejob.com +intomotors.com +iwatchrealitytv.com +lemondrop.com +love2exercise.com +love2workout.com +lovefantasysports.com +lovetoexercise.com +luvfishing.com +luvgolfing.com +luvsoccer.com +mail4me.com +majorgolfer.com +majorshopaholic.com +majortechie.com +mcom.com +motor-nut.com +moviefan.com +mycapitalsmail.com +mycatiscool.com +myfantasyteamrules.com +myteamisbest.com +netbusiness.com +news-fanatic.com +newspaperfan.com +onlinevideosrock.com +realbookfan.com +realhealthnut.com +realitytvaddict.net +realitytvnut.com +reallyintomusic.com +realtravelfan.com +redskinscheer.com +redskinsfamily.com +redskinsfancentral.com +redskinshog.com +redskinsrule.com +redskinsspecialteams.com +redskinsultimatefan.com +scoutmail.com +skins4life.com +stargate2.com +stargateatlantis.com +stargatefanclub.com +stargatesg1.com +stargateu.com +switched.com +t-online.de +thegamefanatic.com +total-techie.com +totalfoodnut.com +totally-into-cooking.com +totallyintobaseball.com +totallyintobasketball.com +totallyintocooking.com +totallyintofootball.com +totallyintogolf.com +totallyintohockey.com +totallyintomusic.com +totallyintoreading.com +totallyintosports.com +totallyintotravel.com +totalmoviefan.com +travel2newplaces.com +tvchannelsurfer.com +ultimateredskinsfan.com +videogamesrock.com +volunteeringisawesome.com +wayintocomputers.com +whatmail.com +when.com +wild4music.com +wildaboutelectronics.com +workingaroundthehouse.com +workingonthehouse.com +writesoon.com +xmasmail.com +arab.ir +denmark.ir +egypt.ir +icq.ir +ir.ae +iraq.ir +ire.ir +ireland.ir +irr.ir +jpg.ir +ksa.ir +kuwait.ir +london.ir +paltalk.ir +spain.ir +sweden.ir +tokyo.ir +111mail.com +123iran.com +37.com +420email.com +4degreez.com +4-music-today.com +actingbiz.com +allhiphop.com +anatomicrock.com +animeone.com +asiancutes.com +a-teens.net +ausi.com +autoindia.com +autopm.com +barriolife.com +b-boy.com +beautifulboy.com +bgay.com +bicycledata.com +bicycling.com +bigheavyworld.com +bigmailbox.net +bikerheaven.net +bikermail.com +billssite.com +blackandchristian.com +blackcity.net +blackvault.com +bmxtrix.com +boarderzone.com +boatnerd.com +bolbox.com +bongmail.com +bowl.com +butch-femme.org +byke.com +calle22.com +cannabismail.com +catlovers.com +certifiedbitches.com +championboxing.com +chatway.com +chillymail.com +classprod.com +classycouples.com +congiu.net +coolshit.com +corpusmail.com +cyberunlimited.org +cycledata.com +darkfear.com +darkforces.com +dirtythird.com +dopefiends.com +draac.com +drakmail.net +dr-dre.com +dreamstop.com +egypt.net +emailfast.com +envirocitizen.com +escapeartist.com +ezsweeps.com +famous.as +farts.com +feelingnaughty.com +firemyst.com +freeonline.com +fudge.com +funkytimes.com +gamerssolution.com +gazabo.net +glittergrrrls.com +goatrance.com +goddess.com +gohip.com +gospelcity.com +gothicgirl.com +grapemail.net +greatautos.org +guy.com +haitisurf.com +happyhippo.com +hateinthebox.com +houseofhorrors.com +hugkiss.com +hullnumber.com +idunno4recipes.com +ihatenetscape.com +intimatefire.com +irow.com +jazzemail.com +juanitabynum.com +kanoodle.com +kickboxing.com +kidrock.com +kinkyemail.com +kool-things.com +latinabarbie.com +latinogreeks.com +leesville.com +loveemail.com +lowrider.com +lucky7lotto.net +madeniggaz.net +mailbomb.com +marillion.net +megarave.com +mofa.com +motley.com +music.com +musician.net +musicsites.com +netbroadcaster.com +netfingers.com +net-surf.com +nocharge.com +operationivy.com +paidoffers.net +pcbee.com +persian.com +petrofind.com +phunkybitches.com +pikaguam.com +pinkcity.net +pitbullmail.com +planetsmeg.com +poop.com +poormail.com +potsmokersnet.com +primetap.com +project420.com +prolife.net +puertoricowow.com +puppetweb.com +rapstar.com +rapworld.com +rastamall.com +ratedx.net +ravermail.com +relapsecult.com +remixer.com +rockeros.com +romance106fm.com +singalongcenter.com +sketchyfriends.com +slayerized.com +smartstocks.com +soulja-beatz.org +specialoperations.com +speedymail.net +spells.com +superbikeclub.com +superintendents.net +surfguiden.com +sweetwishes.com +tattoodesign.com +teamster.net +teenchatnow.com +the5thquarter.com +theblackmarket.com +tombstone.ws +troamail.org +u2tours.com +vitalogy.org +whatisthis.com +wrestlezone.com +abha.cc +agadir.cc +ahsa.ws +ajman.cc +ajman.us +ajman.ws +albaha.cc +algerie.cc +alriyadh.cc +amman.cc +aqaba.cc +arar.ws +aswan.cc +baalbeck.cc +bahraini.cc +banha.cc +bizerte.cc +blida.info +buraydah.cc +cameroon.cc +dhahran.cc +dhofar.cc +djibouti.cc +dominican.cc +eritrea.cc +falasteen.cc +fujairah.cc +fujairah.us +fujairah.ws +gabes.cc +gafsa.cc +giza.cc +guinea.cc +hamra.cc +hasakah.com +hebron.tv +homs.cc +ibra.cc +irbid.ws +ismailia.cc +jadida.cc +jadida.org +jerash.cc +jizan.cc +jouf.cc +kairouan.cc +karak.cc +khaimah.cc +khartoum.cc +khobar.cc +kuwaiti.tv +kyrgyzstan.cc +latakia.cc +lebanese.cc +lubnan.cc +lubnan.ws +madinah.cc +maghreb.cc +manama.cc +mansoura.tv +marrakesh.cc +mascara.ws +meknes.cc +muscat.tv +muscat.ws +nabeul.cc +nabeul.info +nablus.cc +nador.cc +najaf.cc +omani.ws +omdurman.cc +oran.cc +oued.info +oued.org +oujda.biz +oujda.cc +pakistani.ws +palmyra.cc +palmyra.ws +portsaid.cc +qassem.cc +quds.cc +rabat.cc +rafah.cc +ramallah.cc +safat.biz +safat.info +safat.us +safat.ws +salalah.cc +salmiya.biz +sanaa.cc +seeb.cc +sfax.ws +sharm.cc +sinai.cc +siria.cc +sousse.cc +sudanese.cc +suez.cc +tabouk.cc +tajikistan.cc +tangiers.cc +tanta.cc +tayef.cc +tetouan.cc +timor.cc +tunisian.cc +urdun.cc +yanbo.cc +yemeni.cc +yunus.cc +zagazig.cc +zambia.cc +5005.lv +a.org.ua +bmx.lv +company.org.ua +coolmail.ru +dino.lv +eclub.lv +e-mail.am +fit.lv +hacker.am +human.lv +iphon.biz +latchess.com +loveis.lv +lv-inter.net +pookmail.com +sexriga.lv diff --git a/crates/router/src/utils/user/password.rs b/crates/router/src/utils/user/password.rs new file mode 100644 index 000000000000..cff17863c32d --- /dev/null +++ b/crates/router/src/utils/user/password.rs @@ -0,0 +1,43 @@ +use argon2::{ + password_hash::{ + rand_core::OsRng, Error as argon2Err, PasswordHash, PasswordHasher, PasswordVerifier, + SaltString, + }, + Argon2, +}; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, Secret}; + +use crate::core::errors::UserErrors; + +pub fn generate_password_hash( + password: Secret, +) -> CustomResult, UserErrors> { + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::default(); + let password_hash = argon2 + .hash_password(password.expose().as_bytes(), &salt) + .into_report() + .change_context(UserErrors::InternalServerError)?; + Ok(Secret::new(password_hash.to_string())) +} + +pub fn is_correct_password( + candidate: Secret, + password: Secret, +) -> CustomResult { + let password = password.expose(); + let parsed_hash = PasswordHash::new(&password) + .into_report() + .change_context(UserErrors::InternalServerError)?; + let result = Argon2::default().verify_password(candidate.expose().as_bytes(), &parsed_hash); + match result { + Ok(_) => Ok(true), + Err(argon2Err::Password) => Ok(false), + Err(e) => Err(e), + } + .into_report() + .change_context(UserErrors::InternalServerError) +} diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index dca7bbfc9b44..4b2cbcb7c4a9 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -23,6 +23,7 @@ impl utils::Connector for AdyenTest { } } + #[cfg(feature = "payouts")] fn get_payout_data(&self) -> Option { use router::connector::Adyen; Some(types::api::PayoutConnectorData { @@ -68,6 +69,7 @@ impl AdyenTest { }) } + #[cfg(feature = "payouts")] fn get_payout_info(payout_type: enums::PayoutType) -> Option { Some(PaymentInfo { country: Some(api_models::enums::CountryAlpha2::NL), diff --git a/crates/router/tests/connectors/bankofamerica.rs b/crates/router/tests/connectors/bankofamerica.rs index ce264cbccc86..766078fa19c0 100644 --- a/crates/router/tests/connectors/bankofamerica.rs +++ b/crates/router/tests/connectors/bankofamerica.rs @@ -12,6 +12,7 @@ impl utils::Connector for BankofamericaTest { use router::connector::Bankofamerica; types::api::ConnectorData { connector: Box::new(&Bankofamerica), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/globepay.rs b/crates/router/tests/connectors/globepay.rs index 210f12b23d83..fcf61dd6b33d 100644 --- a/crates/router/tests/connectors/globepay.rs +++ b/crates/router/tests/connectors/globepay.rs @@ -14,7 +14,7 @@ impl utils::Connector for GlobepayTest { use router::connector::Globepay; types::api::ConnectorData { connector: Box::new(&Globepay), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Globepay, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/gocardless.rs b/crates/router/tests/connectors/gocardless.rs index 6b6bd6d86175..f19e90941b2e 100644 --- a/crates/router/tests/connectors/gocardless.rs +++ b/crates/router/tests/connectors/gocardless.rs @@ -12,7 +12,7 @@ impl utils::Connector for GocardlessTest { use router::connector::Gocardless; types::api::ConnectorData { connector: Box::new(&Gocardless), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Gocardless, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/helcim.rs b/crates/router/tests/connectors/helcim.rs index 0bac1e702360..c9a891988f3b 100644 --- a/crates/router/tests/connectors/helcim.rs +++ b/crates/router/tests/connectors/helcim.rs @@ -12,7 +12,7 @@ impl utils::Connector for HelcimTest { use router::connector::Helcim; types::api::ConnectorData { connector: Box::new(&Helcim), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Helcim, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/main.rs b/crates/router/tests/connectors/main.rs index 03b6181b8a89..fc474818b505 100644 --- a/crates/router/tests/connectors/main.rs +++ b/crates/router/tests/connectors/main.rs @@ -11,6 +11,7 @@ mod adyen; mod airwallex; mod authorizedotnet; mod bambora; +#[cfg(feature = "dummy_connector")] mod bankofamerica; mod bitpay; mod bluesnap; @@ -36,13 +37,16 @@ mod nexinets; mod nmi; mod noon; mod nuvei; +#[cfg(feature = "dummy_connector")] mod opayo; mod opennode; +#[cfg(feature = "dummy_connector")] mod payeezy; mod payme; mod paypal; mod payu; mod powertranz; +#[cfg(feature = "dummy_connector")] mod prophetpay; mod rapyd; mod shift4; diff --git a/crates/router/tests/connectors/opayo.rs b/crates/router/tests/connectors/opayo.rs index 6d76133d342e..97d744d1e9db 100644 --- a/crates/router/tests/connectors/opayo.rs +++ b/crates/router/tests/connectors/opayo.rs @@ -16,6 +16,7 @@ impl utils::Connector for OpayoTest { use router::connector::Opayo; types::api::ConnectorData { connector: Box::new(&Opayo), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/payeezy.rs b/crates/router/tests/connectors/payeezy.rs index 81d69503b4a9..1176ad7322bf 100644 --- a/crates/router/tests/connectors/payeezy.rs +++ b/crates/router/tests/connectors/payeezy.rs @@ -22,6 +22,7 @@ impl utils::Connector for PayeezyTest { use router::connector::Payeezy; types::api::ConnectorData { connector: Box::new(&Payeezy), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/powertranz.rs b/crates/router/tests/connectors/powertranz.rs index cc0028ef3c91..eca3f86b5690 100644 --- a/crates/router/tests/connectors/powertranz.rs +++ b/crates/router/tests/connectors/powertranz.rs @@ -14,7 +14,7 @@ impl utils::Connector for PowertranzTest { use router::connector::Powertranz; types::api::ConnectorData { connector: Box::new(&Powertranz), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Powertranz, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/prophetpay.rs b/crates/router/tests/connectors/prophetpay.rs index 2e4c6d7e380e..09e4ea422531 100644 --- a/crates/router/tests/connectors/prophetpay.rs +++ b/crates/router/tests/connectors/prophetpay.rs @@ -12,6 +12,7 @@ impl utils::Connector for ProphetpayTest { use router::connector::Prophetpay; types::api::ConnectorData { connector: Box::new(&Prophetpay), + // Remove `dummy_connector` feature gate from module in `main.rs` when updating this to use actual connector variant connector_name: types::Connector::DummyConnector1, get_token: types::api::GetToken::Connector, merchant_connector_id: None, diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 1cb3b48f72d5..1f450a19e776 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -4,9 +4,11 @@ use async_trait::async_trait; use common_utils::pii::Email; use error_stack::Report; use masking::Secret; +#[cfg(feature = "payouts")] +use router::core::utils as core_utils; use router::{ configs::settings::Settings, - core::{errors, errors::ConnectorError, payments, utils as core_utils}, + core::{errors, errors::ConnectorError, payments}, db::StorageImpl, routes, services, types::{self, api, storage::enums, AccessToken, PaymentAddress, RouterData}, @@ -17,15 +19,21 @@ use wiremock::{Mock, MockServer}; pub trait Connector { fn get_data(&self) -> types::api::ConnectorData; + fn get_auth_token(&self) -> types::ConnectorAuthType; + fn get_name(&self) -> String; + fn get_connector_meta(&self) -> Option { None } + /// interval in seconds to be followed when making the subsequent request whenever needed fn get_request_interval(&self) -> u64 { 5 } + + #[cfg(feature = "payouts")] fn get_payout_data(&self) -> Option { None } @@ -423,6 +431,7 @@ pub trait ConnectorActions: Connector { Err(errors::ConnectorError::ProcessingStepFailed(None).into()) } + #[cfg(feature = "payouts")] fn get_payout_request( &self, connector_payout_id: Option, @@ -534,6 +543,7 @@ pub trait ConnectorActions: Connector { } } + #[cfg(feature = "payouts")] async fn verify_payout_eligibility( &self, payout_type: enums::PayoutType, @@ -572,6 +582,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn fulfill_payout( &self, connector_payout_id: Option, @@ -611,6 +622,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn create_payout( &self, connector_customer: Option, @@ -651,6 +663,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn cancel_payout( &self, connector_payout_id: String, @@ -691,6 +704,7 @@ pub trait ConnectorActions: Connector { Ok(res.response.unwrap()) } + #[cfg(feature = "payouts")] async fn create_and_fulfill_payout( &self, connector_customer: Option, @@ -714,6 +728,7 @@ pub trait ConnectorActions: Connector { Ok(fulfill_res) } + #[cfg(feature = "payouts")] async fn create_and_cancel_payout( &self, connector_customer: Option, @@ -737,6 +752,7 @@ pub trait ConnectorActions: Connector { Ok(cancel_res) } + #[cfg(feature = "payouts")] async fn create_payout_recipient( &self, payout_type: enums::PayoutType, diff --git a/crates/router/tests/connectors/volt.rs b/crates/router/tests/connectors/volt.rs index 1c62c47ee03c..0df21640c777 100644 --- a/crates/router/tests/connectors/volt.rs +++ b/crates/router/tests/connectors/volt.rs @@ -12,7 +12,7 @@ impl utils::Connector for VoltTest { use router::connector::Volt; types::api::ConnectorData { connector: Box::new(&Volt), - connector_name: types::Connector::DummyConnector1, + connector_name: types::Connector::Volt, get_token: types::api::GetToken::Connector, merchant_connector_id: None, } diff --git a/crates/router/tests/connectors/wise.rs b/crates/router/tests/connectors/wise.rs index 753ed4f4ed66..fb65397e1a22 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -1,10 +1,16 @@ +#[cfg(feature = "payouts")] use api_models::payments::{Address, AddressDetails}; +#[cfg(feature = "payouts")] use masking::Secret; -use router::types::{self, api, storage::enums, PaymentAddress}; +use router::types; +#[cfg(feature = "payouts")] +use router::types::{api, storage::enums, PaymentAddress}; +#[cfg(feature = "payouts")] +use crate::utils::PaymentInfo; use crate::{ connector_auth, - utils::{self, ConnectorActions, PaymentInfo}, + utils::{self, ConnectorActions}, }; struct WiseTest; @@ -20,6 +26,7 @@ impl utils::Connector for WiseTest { } } + #[cfg(feature = "payouts")] fn get_payout_data(&self) -> Option { use router::connector::Wise; Some(types::api::PayoutConnectorData { @@ -44,6 +51,7 @@ impl utils::Connector for WiseTest { } impl WiseTest { + #[cfg(feature = "payouts")] fn get_payout_info() -> Option { Some(PaymentInfo { country: Some(api_models::enums::CountryAlpha2::NL), @@ -75,6 +83,7 @@ impl WiseTest { } } +#[cfg(feature = "payouts")] static CONNECTOR: WiseTest = WiseTest {}; /******************** Payouts test cases ********************/ diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 0c9751aee440..9cd678083959 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -243,6 +243,8 @@ pub enum Flow { GsmRuleUpdate, /// Gsm Rule Delete flow GsmRuleDelete, + /// User connect account + UserConnectAccount, } /// diff --git a/crates/scheduler/Cargo.toml b/crates/scheduler/Cargo.toml index 7ce61d9f59f4..e0b68c709e8d 100644 --- a/crates/scheduler/Cargo.toml +++ b/crates/scheduler/Cargo.toml @@ -32,9 +32,6 @@ redis_interface = { version = "0.1.0", path = "../redis_interface" } router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] } storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } -[target.'cfg(not(target_os = "windows"))'.dependencies] -signal-hook-tokio = { version = "0.3.1", features = ["futures-v0_3"] } - # [[bin]] # name = "scheduler" # path = "src/bin/scheduler.rs" diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 8fb59d213364..31115e91589f 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -9,22 +9,20 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -kms = ["external_services/kms"] default = ["olap", "oltp"] -oltp = ["data_models/oltp"] +oltp = [] olap = ["data_models/olap"] [dependencies] # First Party dependencies -common_utils = { version = "0.1.0", path = "../common_utils" } api_models = { version = "0.1.0", path = "../api_models" } -diesel_models = { version = "0.1.0", path = "../diesel_models" } +common_utils = { version = "0.1.0", path = "../common_utils" } data_models = { version = "0.1.0", path = "../data_models", default-features = false } +diesel_models = { version = "0.1.0", path = "../diesel_models" } masking = { version = "0.1.0", path = "../masking" } redis_interface = { version = "0.1.0", path = "../redis_interface" } -router_env = { version = "0.1.0", path = "../router_env" } -external_services = { version = "0.1.0", path = "../external_services" } router_derive = { version = "0.1.0", path = "../router_derive" } +router_env = { version = "0.1.0", path = "../router_env" } # Third party crates actix-web = "4.3.1" @@ -34,16 +32,16 @@ bb8 = "0.8.1" bytes = "1.4.0" config = { version = "0.13.3", features = ["toml"] } crc32fast = "1.3.2" -futures = "0.3.28" diesel = { version = "2.1.0", default-features = false, features = ["postgres"] } dyn-clone = "1.0.12" error-stack = "0.3.1" +futures = "0.3.28" http = "0.2.9" mime = "0.3.17" moka = { version = "0.11.3", features = ["future"] } once_cell = "1.18.0" ring = "0.16.20" -thiserror = "1.0.40" -tokio = { version = "1.28.2", features = ["rt-multi-thread"] } serde = { version = "1.0.185", features = ["derive"] } serde_json = "1.0.105" +thiserror = "1.0.40" +tokio = { version = "1.28.2", features = ["rt-multi-thread"] } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 21002917df83..d34230e2cb49 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1138,6 +1138,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => DieselPaymentAttemptUpdate::Update { amount, @@ -1152,6 +1154,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, }, Self::UpdateTrackers { @@ -1160,12 +1164,16 @@ impl DataModelExt for PaymentAttemptUpdate { straight_through_algorithm, amount_capturable, updated_by, + surcharge_amount, + tax_amount, merchant_connector_id, } => DieselPaymentAttemptUpdate::UpdateTrackers { payment_token, connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id, }, @@ -1193,8 +1201,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, } => DieselPaymentAttemptUpdate::ConfirmUpdate { @@ -1214,8 +1220,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1243,6 +1247,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, } => DieselPaymentAttemptUpdate::ResponseUpdate { @@ -1260,6 +1266,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, }, @@ -1379,6 +1387,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, } => Self::Update { amount, @@ -1393,6 +1403,8 @@ impl DataModelExt for PaymentAttemptUpdate { business_sub_label, amount_to_capture, capture_method, + surcharge_amount, + tax_amount, updated_by, }, DieselPaymentAttemptUpdate::UpdateTrackers { @@ -1401,12 +1413,16 @@ impl DataModelExt for PaymentAttemptUpdate { straight_through_algorithm, amount_capturable, updated_by, + surcharge_amount, + tax_amount, merchant_connector_id: connector_id, } => Self::UpdateTrackers { payment_token, connector, straight_through_algorithm, amount_capturable, + surcharge_amount, + tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1434,8 +1450,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, } => Self::ConfirmUpdate { @@ -1455,8 +1469,6 @@ impl DataModelExt for PaymentAttemptUpdate { error_code, error_message, amount_capturable, - surcharge_amount, - tax_amount, updated_by, merchant_connector_id: connector_id, }, @@ -1484,6 +1496,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, } => Self::ResponseUpdate { @@ -1501,6 +1515,8 @@ impl DataModelExt for PaymentAttemptUpdate { connector_response_reference_id, amount_capturable, updated_by, + surcharge_amount, + tax_amount, authentication_data, encoded_data, }, diff --git a/crates/storage_impl/src/redis/kv_store.rs b/crates/storage_impl/src/redis/kv_store.rs index 0c615d74f89a..3eadd8b83ade 100644 --- a/crates/storage_impl/src/redis/kv_store.rs +++ b/crates/storage_impl/src/redis/kv_store.rs @@ -111,7 +111,9 @@ where KvOperation::Hset(value, sql) => { logger::debug!(kv_operation= %operation, value = ?value); - redis_conn.set_hash_fields(key, value, Some(ttl)).await?; + redis_conn + .set_hash_fields(key, value, Some(ttl.into())) + .await?; store .push_to_drainer_stream::(sql, partition_key) diff --git a/crates/test_utils/Cargo.toml b/crates/test_utils/Cargo.toml index 44c835b21623..957a51171da7 100644 --- a/crates/test_utils/Cargo.toml +++ b/crates/test_utils/Cargo.toml @@ -9,30 +9,23 @@ license.workspace = true [features] default = ["dummy_connector", "payouts"] -dummy_connector = ["api_models/dummy_connector"] +dummy_connector = [] payouts = [] [dependencies] async-trait = "0.1.68" -actix-web = "4.3.1" base64 = "0.21.2" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } +rand = "0.8.5" +reqwest = { version = "0.11.18", features = ["native-tls"] } serde = { version = "1.0.163", features = ["derive"] } serde_json = "1.0.96" -serde_path_to_error = "0.1.11" -toml = "0.7.4" -serial_test = "2.0.0" serde_urlencoded = "0.7.1" -actix-http = "3.3.1" -awc = { version = "3.1.1", features = ["rustls"] } -derive_deref = "1.1.1" -rand = "0.8.5" -reqwest = { version = "0.11.18", features = ["native-tls"] } +serial_test = "2.0.0" thirtyfour = "0.31.0" time = { version = "0.3.21", features = ["macros"] } tokio = "1.28.2" -uuid = { version = "1.3.3", features = ["serde", "v4"] } +toml = "0.7.4" # First party crates -api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } masking = { version = "0.1.0", path = "../masking" }