From 2a4f5d13717a78dc2e2e4fc9a492a45b92151dbe Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Fri, 10 Nov 2023 14:39:32 +0530 Subject: [PATCH 01/15] feat(router): added Payment link new design (#2731) Co-authored-by: Sahkal Poddar Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Kashif <46213975+kashif-m@users.noreply.github.com> Co-authored-by: Kashif --- crates/api_models/src/admin.rs | 5 +- crates/api_models/src/payments.rs | 3 +- crates/common_enums/Cargo.toml | 4 +- crates/common_utils/src/consts.rs | 12 + crates/router/src/core/payment_link.rs | 98 +- .../src/core/payment_link/payment_link.html | 1274 +++++++++-------- openapi/openapi_spec.json | 8 +- 7 files changed, 737 insertions(+), 667 deletions(-) diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index e844d1900a1a..979214a071a9 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -463,9 +463,8 @@ pub struct PaymentLinkConfig { #[serde(deny_unknown_fields)] pub struct PaymentLinkColorSchema { - pub primary_color: Option, - pub primary_accent_color: Option, - pub secondary_color: Option, + pub background_primary_color: Option, + pub sdk_theme: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 196dd108333b..22579ed6d6ea 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3150,6 +3150,7 @@ pub struct PaymentLinkDetails { pub merchant_logo: String, pub return_url: String, pub merchant_name: String, - pub order_details: Vec, + pub order_details: Option>, pub max_items_visible_after_collapse: i8, + pub sdk_theme: Option, } diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index e9f2dffcc050..db37d27ab0f1 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -12,9 +12,9 @@ dummy_connector = [] [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } -serde = { version = "1.0.160", features = [ "derive" ] } +serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" -strum = { version = "0.25", features = [ "derive" ] } +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"] } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 2f517295ae48..7bc248bf8d1b 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -29,3 +29,15 @@ pub const SURCHARGE_PERCENTAGE_PRECISION_LENGTH: u8 = 2; /// Header Key for application overhead of a request pub const X_HS_LATENCY: &str = "x-hs-latency"; + +/// SDK Default Theme const +pub const DEFAULT_SDK_THEME: &str = "#7EA8F6"; + +/// Default Payment Link Background color +pub const DEFAULT_BACKGROUND_COLOR: &str = "#E5E5E5"; + +/// Default product Img Link +pub const DEFAULT_PRODUCT_IMG: &str = "https://i.imgur.com/On3VtKF.png"; + +/// Default Merchant Logo Link +pub const DEFAULT_MERCHANT_LOGO: &str = "https://i.imgur.com/RfxPFQo.png"; diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 0012efc86c9f..2ea6a4d7f219 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -1,6 +1,12 @@ use api_models::admin as admin_types; +use common_utils::{ + consts::{ + DEFAULT_BACKGROUND_COLOR, DEFAULT_MERCHANT_LOGO, DEFAULT_PRODUCT_IMG, DEFAULT_SDK_THEME, + }, + ext_traits::ValueExt, +}; use error_stack::{IntoReport, ResultExt}; -use masking::PeekInterface; +use masking::{PeekInterface, Secret}; use super::errors::{self, RouterResult, StorageErrorExt}; use crate::{ @@ -76,12 +82,7 @@ pub async fn intiate_payment_link_flow( }) .transpose()?; - let order_details = payment_intent - .order_details - .get_required_value("order_details") - .change_context(errors::ApiErrorResponse::MissingRequiredField { - field_name: "order_details", - })?; + let order_details = validate_order_details(payment_intent.order_details)?; let return_url = if let Some(payment_create_return_url) = payment_intent.return_url { payment_create_return_url @@ -99,6 +100,9 @@ pub async fn intiate_payment_link_flow( payment_intent.client_secret, )?; + let (default_sdk_theme, default_background_color) = + (DEFAULT_SDK_THEME, DEFAULT_BACKGROUND_COLOR); + let payment_details = api_models::payments::PaymentLinkDetails { amount: payment_intent.amount, currency, @@ -116,13 +120,25 @@ pub async fn intiate_payment_link_flow( client_secret, merchant_logo: payment_link_config .clone() - .map(|pl_metadata| pl_metadata.merchant_logo.unwrap_or_default()) + .map(|pl_config| { + pl_config + .merchant_logo + .unwrap_or(DEFAULT_MERCHANT_LOGO.to_string()) + }) .unwrap_or_default(), max_items_visible_after_collapse: 3, + sdk_theme: payment_link_config.clone().and_then(|pl_config| { + pl_config + .color_scheme + .map(|color| color.sdk_theme.unwrap_or(default_sdk_theme.to_string())) + }), }; let js_script = get_js_script(payment_details)?; - let css_script = get_color_scheme_css(payment_link_config.clone()); + let css_script = get_color_scheme_css( + payment_link_config.clone(), + default_background_color.to_string(), + ); let payment_link_data = services::PaymentLinkFormData { js_script, sdk_url: state.conf.payment_link.sdk_url.clone(), @@ -149,38 +165,21 @@ fn get_js_script( fn get_color_scheme_css( payment_link_config: Option, + default_primary_color: String, ) -> String { - let (default_primary_color, default_accent_color, default_secondary_color) = ( - "#C6C7C8".to_string(), - "#6A8EF5".to_string(), - "#0C48F6".to_string(), - ); - - let (primary_color, primary_accent_color, secondary_color) = payment_link_config + let background_primary_color = payment_link_config .and_then(|pl_config| { pl_config.color_scheme.map(|color| { - ( - color.primary_color.unwrap_or(default_primary_color.clone()), - color - .primary_accent_color - .unwrap_or(default_accent_color.clone()), - color - .secondary_color - .unwrap_or(default_secondary_color.clone()), - ) + color + .background_primary_color + .unwrap_or(default_primary_color.clone()) }) }) - .unwrap_or(( - default_primary_color, - default_accent_color, - default_secondary_color, - )); + .unwrap_or(default_primary_color); format!( ":root {{ - --primary-color: {primary_color}; - --primary-accent-color: {primary_accent_color}; - --secondary-color: {secondary_color}; + --primary-color: {background_primary_color}; }}" ) } @@ -203,3 +202,36 @@ fn validate_sdk_requirements( })?; Ok((pub_key, currency, client_secret)) } + +fn validate_order_details( + order_details: Option>>, +) -> Result< + Option>, + error_stack::Report, +> { + let order_details = order_details + .map(|order_details| { + order_details + .iter() + .map(|data| { + data.to_owned() + .parse_value("OrderDetailsWithAmount") + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "OrderDetailsWithAmount", + }) + .attach_printable("Unable to parse OrderDetailsWithAmount") + }) + .collect::, _>>() + }) + .transpose()?; + + let updated_order_details = order_details.map(|mut order_details| { + for order in order_details.iter_mut() { + if order.product_img_link.is_none() { + order.product_img_link = Some(DEFAULT_PRODUCT_IMG.to_string()); + } + } + order_details + }); + Ok(updated_order_details) +} diff --git a/crates/router/src/core/payment_link/payment_link.html b/crates/router/src/core/payment_link/payment_link.html index 462a11d2567e..67410cac8418 100644 --- a/crates/router/src/core/payment_link/payment_link.html +++ b/crates/router/src/core/payment_link/payment_link.html @@ -112,8 +112,8 @@ } #hyper-checkout-merchant-image > img { - height: 48px; - width: 48px; + height: 40px; + width: 40px; } #hyper-checkout-cart-image { @@ -175,8 +175,8 @@ } .hyper-checkout-cart-product-image { - height: 72px; - width: 72px; + height: 56px; + width: 56px; } .hyper-checkout-card-item-name { @@ -234,13 +234,21 @@ background-color: var(--primary-color); box-shadow: 0px 1px 10px #f2f2f2; display: flex; + flex-flow: column; align-items: center; justify-content: center; } #payment-form-wrap { - min-width: 584px; - padding: 50px; + min-width: 300px; + width: 30vw; + padding: 20px; + background-color: white; + border-radius: 3px; + } + + .powered-by-hyper { + margin-top: 20px; } #hyper-checkout-sdk-header { @@ -295,28 +303,13 @@ margin-top: 10px; } - .checkoutButton { - height: 48px; - border-radius: 25px; - width: 100%; - border: transparent; - background: var(--secondary-color); - color: #ffffff; - font-weight: 600; - cursor: pointer; - } - .page-spinner, .page-spinner::before, - .page-spinner::after, - .spinner, - .spinner:before, - .spinner:after { + .page-spinner::after { border-radius: 50%; } - .page-spinner, - .spinner { + .page-spinner { color: #ffffff; font-size: 22px; text-indent: -99999px; @@ -331,9 +324,7 @@ } .page-spinner::before, - .page-spinner::after, - .spinner:before, - .spinner:after { + .page-spinner::after { position: absolute; content: ""; } @@ -405,19 +396,6 @@ } } - .spinner:before { - width: 10.4px; - height: 20.4px; - background: var(--primary-color); - border-radius: 20.4px 0 0 20.4px; - top: -0.2px; - left: -0.2px; - -webkit-transform-origin: 10.4px 10.2px; - transform-origin: 10.4px 10.2px; - -webkit-animation: loading 2s infinite ease 1.5s; - animation: loading 2s infinite ease 1.5s; - } - #payment-message { font-size: 12px; font-weight: 500; @@ -426,19 +404,6 @@ font-family: "Montserrat"; } - .spinner:after { - width: 10.4px; - height: 10.2px; - background: var(--primary-color); - border-radius: 0 10.2px 10.2px 0; - top: -0.1px; - left: 10.2px; - -webkit-transform-origin: 0px 10.2px; - transform-origin: 0px 10.2px; - -webkit-animation: loading 2s infinite ease; - animation: loading 2s infinite ease; - } - #payment-form { max-width: 560px; width: 100%; @@ -447,11 +412,6 @@ } @media only screen and (max-width: 1200px) { - .checkoutButton { - width: 95%; - background-color: var(--primary-color); - } - .hyper-checkout { flex-flow: column; margin: 0; @@ -627,16 +587,16 @@ @@ -700,7 +660,7 @@
-
+
-
+ +
+ + + + + + + + + + + + + + + + +
- - + function showSDK(e) { + if (window.state.isMobileView) { + hide("#hyper-checkout-cart"); + } else { + show("#hyper-checkout-cart"); + } + setPageLoading(true); + checkStatus() + .then((res) => { + if (res.showSdk) { + renderPaymentDetails(); + renderCart(); + renderSDKHeader(); + show("#hyper-checkout-sdk"); + show("#hyper-checkout-details"); + } else { + show("#hyper-checkout-status"); + show("#hyper-footer"); + } + }) + .catch((err) => {}) + .finally(() => { + setPageLoading(false); + }); + } + + window.addEventListener("resize", (event) => { + const currentHeight = window.innerHeight; + const currentWidth = window.innerWidth; + if (currentWidth <= 1200 && window.state.prevWidth > 1200) { + hide("#hyper-checkout-cart"); + } else if (currentWidth > 1200 && window.state.prevWidth <= 1200) { + show("#hyper-checkout-cart"); + } + + window.state.prevHeight = currentHeight; + window.state.prevWidth = currentWidth; + window.state.isMobileView = currentWidth <= 1200; + }); + + diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 6e61f2eb614e..23f8f1b3628b 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7809,15 +7809,11 @@ "PaymentLinkColorSchema": { "type": "object", "properties": { - "primary_color": { + "background_primary_color": { "type": "string", "nullable": true }, - "primary_accent_color": { - "type": "string", - "nullable": true - }, - "secondary_color": { + "sdk_theme": { "type": "string", "nullable": true } From b5ea8db2d2b7e7544931704a7191b42d3a8299be Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:38:30 +0530 Subject: [PATCH 02/15] refactor(connector): [Zen] change error message from NotSupported to NotImplemented (#2831) --- .../router/src/connector/zen/transformers.rs | 91 +++++++------------ 1 file changed, 32 insertions(+), 59 deletions(-) diff --git a/crates/router/src/connector/zen/transformers.rs b/crates/router/src/connector/zen/transformers.rs index d13c9b6421f4..6b0d46dec8d1 100644 --- a/crates/router/src/connector/zen/transformers.rs +++ b/crates/router/src/connector/zen/transformers.rs @@ -290,10 +290,9 @@ impl | api_models::payments::VoucherData::FamilyMart { .. } | api_models::payments::VoucherData::Seicomart { .. } | api_models::payments::VoucherData::PayEasy { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -342,12 +341,8 @@ impl api_models::payments::BankTransferData::Pse { .. } => { ZenPaymentChannels::PclBoacompraPse } - api_models::payments::BankTransferData::SepaBankTransfer { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::BankTransferData::AchBankTransfer { .. } + api_models::payments::BankTransferData::SepaBankTransfer { .. } + | api_models::payments::BankTransferData::AchBankTransfer { .. } | api_models::payments::BankTransferData::BacsBankTransfer { .. } | api_models::payments::BankTransferData::PermataBankTransfer { .. } | api_models::payments::BankTransferData::BcaBankTransfer { .. } @@ -356,10 +351,9 @@ impl | api_models::payments::BankTransferData::CimbVaBankTransfer { .. } | api_models::payments::BankTransferData::DanamonVaBankTransfer { .. } | api_models::payments::BankTransferData::MandiriVaBankTransfer { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; Ok(Self::ApiRequest(Box::new(ApiRequest { @@ -489,12 +483,8 @@ impl api_models::payments::WalletData::WeChatPayRedirect(_) | api_models::payments::WalletData::PaypalRedirect(_) | api_models::payments::WalletData::ApplePay(_) - | api_models::payments::WalletData::GooglePay(_) => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ))? - } - api_models::payments::WalletData::AliPayQr(_) + | api_models::payments::WalletData::GooglePay(_) + | api_models::payments::WalletData::AliPayQr(_) | api_models::payments::WalletData::AliPayRedirect(_) | api_models::payments::WalletData::AliPayHkRedirect(_) | api_models::payments::WalletData::MomoRedirect(_) @@ -514,10 +504,9 @@ impl | api_models::payments::WalletData::CashappQr(_) | api_models::payments::WalletData::SwishQr(_) | api_models::payments::WalletData::WeChatPayQr(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } }; let terminal_uuid = session_data @@ -719,10 +708,9 @@ impl TryFrom<&ZenRouterData<&types::PaymentsAuthorizeRouterData>> for ZenPayment | api_models::payments::PaymentMethodData::MandatePayment | api_models::payments::PaymentMethodData::Reward | api_models::payments::PaymentMethodData::Upi(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - })? + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ))? } } } @@ -736,13 +724,8 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::Sofort { .. } | api_models::payments::BankRedirectData::BancontactCard { .. } | api_models::payments::BankRedirectData::Blik { .. } - | api_models::payments::BankRedirectData::Trustly { .. } => { - Err(errors::ConnectorError::NotImplemented( - utils::get_unimplemented_payment_method_error_message("Zen"), - ) - .into()) - } - api_models::payments::BankRedirectData::Eps { .. } + | api_models::payments::BankRedirectData::Trustly { .. } + | api_models::payments::BankRedirectData::Eps { .. } | api_models::payments::BankRedirectData::Giropay { .. } | api_models::payments::BankRedirectData::Przelewy24 { .. } | api_models::payments::BankRedirectData::Bizum {} @@ -754,10 +737,9 @@ impl TryFrom<&api_models::payments::BankRedirectData> for ZenPaymentsRequest { | api_models::payments::BankRedirectData::OpenBankingUk { .. } | api_models::payments::BankRedirectData::OnlineBankingFpx { .. } | api_models::payments::BankRedirectData::OnlineBankingThailand { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -776,10 +758,9 @@ impl TryFrom<&api_models::payments::PayLaterData> for ZenPaymentsRequest { | api_models::payments::PayLaterData::WalleyRedirect {} | api_models::payments::PayLaterData::AlmaRedirect {} | api_models::payments::PayLaterData::AtomeRedirect {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -794,10 +775,9 @@ impl TryFrom<&api_models::payments::BankDebitData> for ZenPaymentsRequest { | api_models::payments::BankDebitData::SepaBankDebit { .. } | api_models::payments::BankDebitData::BecsBankDebit { .. } | api_models::payments::BankDebitData::BacsBankDebit { .. } => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -811,10 +791,9 @@ impl TryFrom<&api_models::payments::CardRedirectData> for ZenPaymentsRequest { api_models::payments::CardRedirectData::Knet {} | api_models::payments::CardRedirectData::Benefit {} | api_models::payments::CardRedirectData::MomoAtm {} => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("Zen"), + ) .into()) } } @@ -825,19 +804,13 @@ impl TryFrom<&api_models::payments::GiftCardData> for ZenPaymentsRequest { type Error = error_stack::Report; fn try_from(value: &api_models::payments::GiftCardData) -> Result { match value { - api_models::payments::GiftCardData::PaySafeCard {} => { + api_models::payments::GiftCardData::PaySafeCard {} + | api_models::payments::GiftCardData::Givex(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Zen"), ) .into()) } - api_models::payments::GiftCardData::Givex(_) => { - Err(errors::ConnectorError::NotSupported { - message: utils::SELECTED_PAYMENT_METHOD.to_string(), - connector: "Zen", - } - .into()) - } } } } From f847802339bfedb24cbaa47ad55e31d80cefddca Mon Sep 17 00:00:00 2001 From: ivor-juspay <138492857+ivor-juspay@users.noreply.github.com> Date: Fri, 10 Nov 2023 17:08:09 +0530 Subject: [PATCH 03/15] feat(analytics): analytics APIs (#2792) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Sampras Lopes --- Cargo.lock | 281 ++++++++- config/config.example.toml | 14 +- config/docker_compose.toml | 11 + crates/api_models/src/analytics.rs | 152 +++++ crates/api_models/src/analytics/payments.rs | 180 ++++++ crates/api_models/src/analytics/refunds.rs | 183 ++++++ crates/api_models/src/lib.rs | 1 + crates/common_utils/src/custom_serde.rs | 48 ++ crates/router/Cargo.toml | 5 +- crates/router/src/analytics.rs | 129 +++++ crates/router/src/analytics/core.rs | 96 ++++ crates/router/src/analytics/errors.rs | 32 ++ crates/router/src/analytics/metrics.rs | 9 + .../router/src/analytics/metrics/request.rs | 60 ++ crates/router/src/analytics/payments.rs | 13 + .../src/analytics/payments/accumulator.rs | 150 +++++ crates/router/src/analytics/payments/core.rs | 129 +++++ .../router/src/analytics/payments/filters.rs | 58 ++ .../router/src/analytics/payments/metrics.rs | 137 +++++ .../payments/metrics/avg_ticket_size.rs | 126 +++++ .../payments/metrics/payment_count.rs | 117 ++++ .../metrics/payment_processed_amount.rs | 128 +++++ .../payments/metrics/payment_success_count.rs | 127 +++++ .../payments/metrics/success_rate.rs | 123 ++++ crates/router/src/analytics/payments/types.rs | 46 ++ crates/router/src/analytics/query.rs | 533 ++++++++++++++++++ crates/router/src/analytics/refunds.rs | 10 + .../src/analytics/refunds/accumulator.rs | 110 ++++ crates/router/src/analytics/refunds/core.rs | 104 ++++ .../router/src/analytics/refunds/filters.rs | 59 ++ .../router/src/analytics/refunds/metrics.rs | 126 +++++ .../analytics/refunds/metrics/refund_count.rs | 116 ++++ .../metrics/refund_processed_amount.rs | 122 ++++ .../refunds/metrics/refund_success_count.rs | 122 ++++ .../refunds/metrics/refund_success_rate.rs | 117 ++++ crates/router/src/analytics/refunds/types.rs | 41 ++ crates/router/src/analytics/routes.rs | 145 +++++ crates/router/src/analytics/sqlx.rs | 401 +++++++++++++ crates/router/src/analytics/types.rs | 119 ++++ crates/router/src/analytics/utils.rs | 22 + crates/router/src/configs/settings.rs | 4 + crates/router/src/lib.rs | 3 + crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 12 + crates/router_env/src/lib.rs | 19 +- crates/router_env/src/metrics.rs | 19 + loadtest/config/development.toml | 12 + 47 files changed, 4559 insertions(+), 14 deletions(-) create mode 100644 crates/api_models/src/analytics.rs create mode 100644 crates/api_models/src/analytics/payments.rs create mode 100644 crates/api_models/src/analytics/refunds.rs create mode 100644 crates/router/src/analytics.rs create mode 100644 crates/router/src/analytics/core.rs create mode 100644 crates/router/src/analytics/errors.rs create mode 100644 crates/router/src/analytics/metrics.rs create mode 100644 crates/router/src/analytics/metrics/request.rs create mode 100644 crates/router/src/analytics/payments.rs create mode 100644 crates/router/src/analytics/payments/accumulator.rs create mode 100644 crates/router/src/analytics/payments/core.rs create mode 100644 crates/router/src/analytics/payments/filters.rs create mode 100644 crates/router/src/analytics/payments/metrics.rs create mode 100644 crates/router/src/analytics/payments/metrics/avg_ticket_size.rs create mode 100644 crates/router/src/analytics/payments/metrics/payment_count.rs create mode 100644 crates/router/src/analytics/payments/metrics/payment_processed_amount.rs create mode 100644 crates/router/src/analytics/payments/metrics/payment_success_count.rs create mode 100644 crates/router/src/analytics/payments/metrics/success_rate.rs create mode 100644 crates/router/src/analytics/payments/types.rs create mode 100644 crates/router/src/analytics/query.rs create mode 100644 crates/router/src/analytics/refunds.rs create mode 100644 crates/router/src/analytics/refunds/accumulator.rs create mode 100644 crates/router/src/analytics/refunds/core.rs create mode 100644 crates/router/src/analytics/refunds/filters.rs create mode 100644 crates/router/src/analytics/refunds/metrics.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_count.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_success_count.rs create mode 100644 crates/router/src/analytics/refunds/metrics/refund_success_rate.rs create mode 100644 crates/router/src/analytics/refunds/types.rs create mode 100644 crates/router/src/analytics/routes.rs create mode 100644 crates/router/src/analytics/sqlx.rs create mode 100644 crates/router/src/analytics/types.rs create mode 100644 crates/router/src/analytics/utils.rs diff --git a/Cargo.lock b/Cargo.lock index ac7fde55d8e3..c96ce2c18258 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,7 +19,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "smallvec", "tokio", @@ -361,6 +361,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + [[package]] name = "android-tzdata" version = "0.1.1" @@ -590,6 +596,15 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "atoi" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -1139,10 +1154,21 @@ dependencies = [ "async-trait", "futures-channel", "futures-util", - "parking_lot", + "parking_lot 0.12.1", "tokio", ] +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bincode" version = "1.3.3" @@ -1632,6 +1658,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ec7a15cbe22e59248fc7eadb1907dab5ba09372595da4d73dd805ed4417dfe" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" + [[package]] name = "crc16" version = "0.4.0" @@ -1726,6 +1767,16 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -1825,7 +1876,7 @@ dependencies = [ "hashbrown 0.14.1", "lock_api", "once_cell", - "parking_lot_core", + "parking_lot_core 0.9.8", ] [[package]] @@ -2048,6 +2099,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "drainer" version = "0.1.0" @@ -2269,6 +2326,12 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "finl_unicode" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + [[package]] name = "flate2" version = "1.0.27" @@ -2334,7 +2397,7 @@ dependencies = [ "futures", "lazy_static", "log", - "parking_lot", + "parking_lot 0.12.1", "rand 0.8.5", "redis-protocol", "semver", @@ -2441,6 +2504,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + [[package]] name = "futures-io" version = "0.3.28" @@ -2651,12 +2725,28 @@ name = "hashbrown" version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +dependencies = [ + "ahash 0.8.3", + "allocator-api2", +] + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.1", +] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -2670,6 +2760,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -3377,7 +3476,7 @@ dependencies = [ "crossbeam-utils", "futures-util", "once_cell", - "parking_lot", + "parking_lot 0.12.1", "quanta", "rustc_version", "scheduled-thread-pool", @@ -3692,6 +3791,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -3699,7 +3809,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core", + "parking_lot_core 0.9.8", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", ] [[package]] @@ -4115,7 +4239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" dependencies = [ "log", - "parking_lot", + "parking_lot 0.12.1", "scheduled-thread-pool", ] @@ -4445,10 +4569,12 @@ dependencies = [ "aws-sdk-s3", "base64 0.21.4", "bb8", + "bigdecimal", "blake3", "bytes", "cards", "clap", + "common_enums", "common_utils", "config", "data_models", @@ -4501,6 +4627,7 @@ dependencies = [ "sha-1 0.9.8", "signal-hook", "signal-hook-tokio", + "sqlx", "storage_impl", "strum 0.24.1", "tera", @@ -4774,7 +4901,7 @@ version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" dependencies = [ - "parking_lot", + "parking_lot 0.12.1", ] [[package]] @@ -5012,7 +5139,7 @@ dependencies = [ "futures", "lazy_static", "log", - "parking_lot", + "parking_lot 0.12.1", "serial_test_derive", ] @@ -5205,6 +5332,111 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "sqlformat" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" +dependencies = [ + "itertools 0.11.0", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +dependencies = [ + "ahash 0.7.6", + "atoi", + "base64 0.13.1", + "bigdecimal", + "bitflags 1.3.2", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "dirs", + "dotenvy", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "hkdf", + "hmac", + "indexmap 1.9.3", + "itoa", + "libc", + "log", + "md-5", + "memchr", + "num-bigint", + "once_cell", + "paste", + "percent-encoding", + "rand 0.8.5", + "serde", + "serde_json", + "sha1", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "time", + "tokio-stream", + "url", + "whoami", +] + +[[package]] +name = "sqlx-macros" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" +dependencies = [ + "dotenvy", + "either", + "heck", + "once_cell", + "proc-macro2", + "quote", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn 1.0.109", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" +dependencies = [ + "native-tls", + "once_cell", + "tokio", + "tokio-native-tls", +] + [[package]] name = "storage_impl" version = "0.1.0" @@ -5249,6 +5481,17 @@ dependencies = [ "regex", ] +[[package]] +name = "stringprep" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" +dependencies = [ + "finl_unicode", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.10.0" @@ -5483,7 +5726,7 @@ dependencies = [ "futures", "http", "log", - "parking_lot", + "parking_lot 0.12.1", "serde", "serde_json", "serde_repr", @@ -5611,7 +5854,7 @@ dependencies = [ "libc", "mio", "num_cpus", - "parking_lot", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2 0.5.4", @@ -6040,6 +6283,12 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "unidecode" version = "0.3.0" @@ -6330,6 +6579,16 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "whoami" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22fc3756b8a9133049b26c7f61ab35416c130e8c09b660f5b3958b446f52cc50" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/config/config.example.toml b/config/config.example.toml index ed9cf9698984..f0083bb48b19 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -434,10 +434,22 @@ apple_pay_ppc_key = "APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY" #Private apple_pay_merchant_cert = "APPLE_PAY_MERCHNAT_CERTIFICATE" #Merchant Certificate provided by Apple Pay (https://developer.apple.com/) Certificates, Identifiers & Profiles > Apple Pay Merchant Identity Certificate apple_pay_merchant_cert_key = "APPLE_PAY_MERCHNAT_CERTIFICATE_KEY" #Private key generate by RSA:2048 algorithm - [payment_link] sdk_url = "http://localhost:9090/dist/HyperLoader.js" +# Analytics configuration. +[analytics] +source = "sqlx" # The Analytics source/strategy to be used + +[analytics.sqlx] +username = "db_user" # Analytics DB Username +password = "db_pass" # Analytics DB Password +host = "localhost" # Analytics DB Host +port = 5432 # Analytics DB Port +dbname = "hyperswitch_db" # Name of Database +pool_size = 5 # Number of connections to keep open +connection_timeout = 10 # Timeout for database connection in seconds + # Config for KV setup [kv_config] # TTL for KV in seconds diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 282894b56d43..ddda7e7021a4 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -319,5 +319,16 @@ supported_connectors = "braintree" redis_lock_expiry_seconds = 180 # 3 * 60 seconds delay_between_retries_in_milliseconds = 500 +[analytics] +source = "sqlx" + +[analytics.sqlx] +username = "db_user" +password = "db_pass" +host = "pg" +port = 5432 +dbname = "hyperswitch_db" +pool_size = 5 + [kv_config] ttl = 900 # 15 * 60 seconds diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs new file mode 100644 index 000000000000..0358b6b313cf --- /dev/null +++ b/crates/api_models/src/analytics.rs @@ -0,0 +1,152 @@ +use std::collections::HashSet; + +use common_utils::events::ApiEventMetric; +use time::PrimitiveDateTime; + +use self::{ + payments::{PaymentDimensions, PaymentMetrics}, + refunds::{RefundDimensions, RefundMetrics}, +}; + +pub mod payments; +pub mod refunds; + +#[derive(Debug, serde::Serialize)] +pub struct NameDescription { + pub name: String, + pub desc: String, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetInfoResponse { + pub metrics: Vec, + pub download_dimensions: Option>, + pub dimensions: Vec, +} + +impl ApiEventMetric for GetInfoResponse {} + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "camelCase")] +pub struct TimeRange { + #[serde(with = "common_utils::custom_serde::iso8601")] + pub start_time: PrimitiveDateTime, + #[serde(default, with = "common_utils::custom_serde::iso8601::option")] + pub end_time: Option, +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +pub struct TimeSeries { + pub granularity: Granularity, +} + +#[derive(Clone, Copy, Debug, serde::Deserialize, masking::Serialize)] +pub enum Granularity { + #[serde(rename = "G_ONEMIN")] + OneMin, + #[serde(rename = "G_FIVEMIN")] + FiveMin, + #[serde(rename = "G_FIFTEENMIN")] + FifteenMin, + #[serde(rename = "G_THIRTYMIN")] + ThirtyMin, + #[serde(rename = "G_ONEHOUR")] + OneHour, + #[serde(rename = "G_ONEDAY")] + OneDay, +} + +#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPaymentMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: payments::PaymentFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} + +impl ApiEventMetric for GetPaymentMetricRequest {} + +#[derive(Clone, Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRefundMetricRequest { + pub time_series: Option, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, + #[serde(default)] + pub filters: refunds::RefundFilters, + pub metrics: HashSet, + #[serde(default)] + pub delta: bool, +} + +impl ApiEventMetric for GetRefundMetricRequest {} + +#[derive(Debug, serde::Serialize)] +pub struct AnalyticsMetadata { + pub current_time_range: TimeRange, +} + +#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPaymentFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +impl ApiEventMetric for GetPaymentFiltersRequest {} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentFiltersResponse { + pub query_data: Vec, +} + +impl ApiEventMetric for PaymentFiltersResponse {} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FilterValue { + pub dimension: PaymentDimensions, + pub values: Vec, +} + +#[derive(Debug, serde::Deserialize, masking::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetRefundFilterRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec, +} + +impl ApiEventMetric for GetRefundFilterRequest {} + +#[derive(Debug, Default, serde::Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RefundFiltersResponse { + pub query_data: Vec, +} + +impl ApiEventMetric for RefundFiltersResponse {} + +#[derive(Debug, serde::Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RefundFilterValue { + pub dimension: RefundDimensions, + pub values: Vec, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MetricsResponse { + pub query_data: Vec, + pub meta_data: [AnalyticsMetadata; 1], +} diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs new file mode 100644 index 000000000000..b5e5852d6283 --- /dev/null +++ b/crates/api_models/src/analytics/payments.rs @@ -0,0 +1,180 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use common_enums::enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}; +use common_utils::events::ApiEventMetric; + +use super::{NameDescription, TimeRange}; +use crate::{analytics::MetricsResponse, enums::Connector}; + +#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +pub struct PaymentFilters { + #[serde(default)] + pub currency: Vec, + #[serde(default)] + pub status: Vec, + #[serde(default)] + pub connector: Vec, + #[serde(default)] + pub auth_type: Vec, + #[serde(default)] + pub payment_method: Vec, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PaymentDimensions { + // Do not change the order of these enums + // Consult the Dashboard FE folks since these also affects the order of metrics on FE + Connector, + PaymentMethod, + Currency, + #[strum(serialize = "authentication_type")] + #[serde(rename = "authentication_type")] + AuthType, + #[strum(serialize = "status")] + #[serde(rename = "status")] + PaymentStatus, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum PaymentMetrics { + PaymentSuccessRate, + PaymentCount, + PaymentSuccessCount, + PaymentProcessedAmount, + AvgTicketSize, +} + +pub mod metric_behaviour { + pub struct PaymentSuccessRate; + pub struct PaymentCount; + pub struct PaymentSuccessCount; + pub struct PaymentProcessedAmount; + pub struct AvgTicketSize; +} + +impl From for NameDescription { + fn from(value: PaymentMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From for NameDescription { + fn from(value: PaymentDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct PaymentMetricsBucketIdentifier { + pub currency: Option, + pub status: Option, + pub connector: Option, + #[serde(rename = "authentication_type")] + pub auth_type: Option, + pub payment_method: Option, + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + // Coz FE sucks + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl PaymentMetricsBucketIdentifier { + pub fn new( + currency: Option, + status: Option, + connector: Option, + auth_type: Option, + payment_method: Option, + normalized_time_range: TimeRange, + ) -> Self { + Self { + currency, + status, + connector, + auth_type, + payment_method, + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +impl Hash for PaymentMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.currency.hash(state); + self.status.map(|i| i.to_string()).hash(state); + self.connector.hash(state); + self.auth_type.map(|i| i.to_string()).hash(state); + self.payment_method.hash(state); + self.time_bucket.hash(state); + } +} + +impl PartialEq for PaymentMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct PaymentMetricsBucketValue { + pub payment_success_rate: Option, + pub payment_count: Option, + pub payment_success_count: Option, + pub payment_processed_amount: Option, + pub avg_ticket_size: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct MetricsBucketResponse { + #[serde(flatten)] + pub values: PaymentMetricsBucketValue, + #[serde(flatten)] + pub dimensions: PaymentMetricsBucketIdentifier, +} + +impl ApiEventMetric for MetricsBucketResponse {} +impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs new file mode 100644 index 000000000000..c5d444338d38 --- /dev/null +++ b/crates/api_models/src/analytics/refunds.rs @@ -0,0 +1,183 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use common_enums::enums::{Currency, RefundStatus}; +use common_utils::events::ApiEventMetric; + +use crate::analytics::MetricsResponse; + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, +)] +// TODO RefundType common_enums need to mapped to storage_model +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RefundType { + InstantRefund, + #[default] + RegularRefund, + RetryRefund, +} + +use super::{NameDescription, TimeRange}; +#[derive(Clone, Debug, Default, serde::Deserialize, masking::Serialize)] +pub struct RefundFilters { + #[serde(default)] + pub currency: Vec, + #[serde(default)] + pub refund_status: Vec, + #[serde(default)] + pub connector: Vec, + #[serde(default)] + pub refund_type: Vec, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RefundDimensions { + Currency, + RefundStatus, + Connector, + RefundType, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum RefundMetrics { + RefundSuccessRate, + RefundCount, + RefundSuccessCount, + RefundProcessedAmount, +} + +pub mod metric_behaviour { + pub struct RefundSuccessRate; + pub struct RefundCount; + pub struct RefundSuccessCount; + pub struct RefundProcessedAmount; +} + +impl From for NameDescription { + fn from(value: RefundMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From for NameDescription { + fn from(value: RefundDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct RefundMetricsBucketIdentifier { + pub currency: Option, + pub refund_status: Option, + pub connector: Option, + pub refund_type: Option, + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl Hash for RefundMetricsBucketIdentifier { + fn hash(&self, state: &mut H) { + self.currency.hash(state); + self.refund_status.map(|i| i.to_string()).hash(state); + self.connector.hash(state); + self.refund_type.hash(state); + self.time_bucket.hash(state); + } +} +impl PartialEq for RefundMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +impl RefundMetricsBucketIdentifier { + pub fn new( + currency: Option, + refund_status: Option, + connector: Option, + refund_type: Option, + normalized_time_range: TimeRange, + ) -> Self { + Self { + currency, + refund_status, + connector, + refund_type, + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +#[derive(Debug, serde::Serialize)] +pub struct RefundMetricsBucketValue { + pub refund_success_rate: Option, + pub refund_count: Option, + pub refund_success_count: Option, + pub refund_processed_amount: Option, +} + +#[derive(Debug, serde::Serialize)] +pub struct RefundMetricsBucketResponse { + #[serde(flatten)] + pub values: RefundMetricsBucketValue, + #[serde(flatten)] + pub dimensions: RefundMetricsBucketIdentifier, +} + +impl ApiEventMetric for RefundMetricsBucketResponse {} +impl ApiEventMetric for MetricsResponse {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 5da916b14817..75509ed7386d 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -1,5 +1,6 @@ #![forbid(unsafe_code)] pub mod admin; +pub mod analytics; pub mod api_keys; pub mod bank_accounts; pub mod cards_info; diff --git a/crates/common_utils/src/custom_serde.rs b/crates/common_utils/src/custom_serde.rs index d64abe38e5b0..edbfa143a667 100644 --- a/crates/common_utils/src/custom_serde.rs +++ b/crates/common_utils/src/custom_serde.rs @@ -170,3 +170,51 @@ pub mod json_string { serde_json::from_str(&j).map_err(de::Error::custom) } } + +/// Use a custom ISO 8601 format when serializing and deserializing +/// [`PrimitiveDateTime`][PrimitiveDateTime]. +/// +/// [PrimitiveDateTime]: ::time::PrimitiveDateTime +pub mod iso8601custom { + + use serde::{ser::Error as _, Deserializer, Serialize, Serializer}; + use time::{ + format_description::well_known::{ + iso8601::{Config, EncodedConfig, TimePrecision}, + Iso8601, + }, + serde::iso8601, + PrimitiveDateTime, UtcOffset, + }; + + const FORMAT_CONFIG: EncodedConfig = Config::DEFAULT + .set_time_precision(TimePrecision::Second { + decimal_digits: None, + }) + .encode(); + + /// Serialize a [`PrimitiveDateTime`] using the well-known ISO 8601 format. + pub fn serialize(date_time: &PrimitiveDateTime, serializer: S) -> Result + where + S: Serializer, + { + date_time + .assume_utc() + .format(&Iso8601::) + .map_err(S::Error::custom)? + .replace('T', " ") + .replace('Z', "") + .serialize(serializer) + } + + /// Deserialize an [`PrimitiveDateTime`] from its ISO 8601 representation. + pub fn deserialize<'a, D>(deserializer: D) -> Result + where + D: Deserializer<'a>, + { + iso8601::deserialize(deserializer).map(|offset_date_time| { + let utc_date_time = offset_date_time.to_offset(UtcOffset::UTC); + PrimitiveDateTime::new(utc_date_time.date(), utc_date_time.time()) + }) + } +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 9ab955813336..7456944a8e4e 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -15,7 +15,7 @@ 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"] +release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "olap"] olap = ["data_models/olap", "storage_impl/olap", "scheduler/olap"] oltp = ["data_models/oltp", "storage_impl/oltp"] kv_store = ["scheduler/kv_store"] @@ -44,6 +44,7 @@ aws-config = { version = "0.55.3", optional = true } aws-sdk-s3 = { version = "0.28.0", optional = true } base64 = "0.21.2" bb8 = "0.8" +bigdecimal = "0.3.1" blake3 = "1.3.3" bytes = "1.4.0" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } @@ -83,6 +84,7 @@ serde_urlencoded = "0.7.1" serde_with = "3.0.0" signal-hook = "0.3.15" strum = { version = "0.24.1", features = ["derive"] } +sqlx = { version = "0.6.3", features = ["postgres", "runtime-actix", "runtime-actix-native-tls", "time", "bigdecimal"] } 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"] } @@ -100,6 +102,7 @@ digest = "0.9" api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } cards = { version = "0.1.0", path = "../cards" } 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" } euclid = { version = "0.1.0", path = "../euclid", features = ["valued_jit"] } masking = { version = "0.1.0", path = "../masking" } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs new file mode 100644 index 000000000000..d57403d92989 --- /dev/null +++ b/crates/router/src/analytics.rs @@ -0,0 +1,129 @@ +mod core; +mod errors; +pub mod metrics; +mod payments; +mod query; +mod refunds; +pub mod routes; + +mod sqlx; +mod types; +mod utils; + +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use router_env::{instrument, tracing}; + +use self::{ + payments::metrics::{PaymentMetric, PaymentMetricRow}, + refunds::metrics::{RefundMetric, RefundMetricRow}, + sqlx::SqlxClient, +}; +use crate::configs::settings::Database; + +#[derive(Clone, Debug)] +pub enum AnalyticsProvider { + Sqlx(SqlxClient), +} + +impl Default for AnalyticsProvider { + fn default() -> Self { + Self::Sqlx(SqlxClient::default()) + } +} + +impl AnalyticsProvider { + #[instrument(skip_all)] + pub async fn get_payment_metrics( + &self, + metric: &PaymentMetrics, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + // Metrics to get the fetch time for each payment metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + + pub async fn get_refund_metrics( + &self, + metric: &RefundMetrics, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + ) -> types::MetricsResult> { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } + + pub async fn from_conf( + config: &AnalyticsConfig, + #[cfg(feature = "kms")] kms_client: &external_services::kms::KmsClient, + ) -> Self { + match config { + AnalyticsConfig::Sqlx { sqlx } => Self::Sqlx( + SqlxClient::from_conf( + sqlx, + #[cfg(feature = "kms")] + kms_client, + ) + .await, + ), + } + } +} + +#[derive(Clone, Debug, serde::Deserialize)] +#[serde(tag = "source")] +#[serde(rename_all = "lowercase")] +pub enum AnalyticsConfig { + Sqlx { sqlx: Database }, +} + +impl Default for AnalyticsConfig { + fn default() -> Self { + Self::Sqlx { + sqlx: Database::default(), + } + } +} diff --git a/crates/router/src/analytics/core.rs b/crates/router/src/analytics/core.rs new file mode 100644 index 000000000000..bf124a6c0e85 --- /dev/null +++ b/crates/router/src/analytics/core.rs @@ -0,0 +1,96 @@ +use api_models::analytics::{ + payments::PaymentDimensions, refunds::RefundDimensions, FilterValue, GetInfoResponse, + GetPaymentFiltersRequest, GetRefundFilterRequest, PaymentFiltersResponse, RefundFilterValue, + RefundFiltersResponse, +}; +use error_stack::ResultExt; + +use super::{ + errors::{self, AnalyticsError}, + payments::filters::{get_payment_filter_for_dimension, FilterRow}, + refunds::filters::{get_refund_filter_for_dimension, RefundFilterRow}, + types::AnalyticsDomain, + utils, AnalyticsProvider, +}; +use crate::{services::ApplicationResponse, types::domain}; + +pub type AnalyticsApiResponse = errors::AnalyticsResult>; + +pub async fn get_domain_info(domain: AnalyticsDomain) -> AnalyticsApiResponse { + let info = match domain { + AnalyticsDomain::Payments => GetInfoResponse { + metrics: utils::get_payment_metrics_info(), + download_dimensions: None, + dimensions: utils::get_payment_dimensions(), + }, + AnalyticsDomain::Refunds => GetInfoResponse { + metrics: utils::get_refund_metrics_info(), + download_dimensions: None, + dimensions: utils::get_refund_dimensions(), + }, + }; + Ok(ApplicationResponse::Json(info)) +} + +pub async fn payment_filters_core( + pool: AnalyticsProvider, + req: GetPaymentFiltersRequest, + merchant: domain::MerchantAccount, +) -> AnalyticsApiResponse { + let mut res = PaymentFiltersResponse::default(); + + for dim in req.group_by_names { + let values = match pool.clone() { + AnalyticsProvider::Sqlx(pool) => { + get_payment_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: FilterRow| match dim { + PaymentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentStatus => fil.status.map(|i| i.as_ref().to_string()), + PaymentDimensions::Connector => fil.connector, + PaymentDimensions::AuthType => fil.authentication_type.map(|i| i.as_ref().to_string()), + PaymentDimensions::PaymentMethod => fil.payment_method, + }) + .collect::>(); + res.query_data.push(FilterValue { + dimension: dim, + values, + }) + } + + Ok(ApplicationResponse::Json(res)) +} + +pub async fn refund_filter_core( + pool: AnalyticsProvider, + req: GetRefundFilterRequest, + merchant: domain::MerchantAccount, +) -> AnalyticsApiResponse { + let mut res = RefundFiltersResponse::default(); + for dim in req.group_by_names { + let values = match pool.clone() { + AnalyticsProvider::Sqlx(pool) => { + get_refund_filter_for_dimension(dim, &merchant.merchant_id, &req.time_range, &pool) + .await + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: RefundFilterRow| match dim { + RefundDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + RefundDimensions::RefundStatus => fil.refund_status.map(|i| i.as_ref().to_string()), + RefundDimensions::Connector => fil.connector, + RefundDimensions::RefundType => fil.refund_type.map(|i| i.as_ref().to_string()), + }) + .collect::>(); + res.query_data.push(RefundFilterValue { + dimension: dim, + values, + }) + } + Ok(ApplicationResponse::Json(res)) +} diff --git a/crates/router/src/analytics/errors.rs b/crates/router/src/analytics/errors.rs new file mode 100644 index 000000000000..da0b2f239cd7 --- /dev/null +++ b/crates/router/src/analytics/errors.rs @@ -0,0 +1,32 @@ +use api_models::errors::types::{ApiError, ApiErrorResponse}; +use common_utils::errors::{CustomResult, ErrorSwitch}; + +pub type AnalyticsResult = CustomResult; + +#[derive(Debug, Clone, serde::Serialize, thiserror::Error)] +pub enum AnalyticsError { + #[allow(dead_code)] + #[error("Not implemented: {0}")] + NotImplemented(&'static str), + #[error("Unknown Analytics Error")] + UnknownError, +} + +impl ErrorSwitch for AnalyticsError { + fn switch(&self) -> ApiErrorResponse { + match self { + Self::NotImplemented(feature) => ApiErrorResponse::NotImplemented(ApiError::new( + "IR", + 0, + format!("{feature} is not implemented."), + None, + )), + Self::UnknownError => ApiErrorResponse::InternalServerError(ApiError::new( + "HE", + 0, + "Something went wrong", + None, + )), + } + } +} diff --git a/crates/router/src/analytics/metrics.rs b/crates/router/src/analytics/metrics.rs new file mode 100644 index 000000000000..6222315a8c06 --- /dev/null +++ b/crates/router/src/analytics/metrics.rs @@ -0,0 +1,9 @@ +use router_env::{global_meter, histogram_metric, histogram_metric_u64, metrics_context}; + +metrics_context!(CONTEXT); +global_meter!(GLOBAL_METER, "ROUTER_API"); + +histogram_metric!(METRIC_FETCH_TIME, GLOBAL_METER); +histogram_metric_u64!(BUCKETS_FETCHED, GLOBAL_METER); + +pub mod request; diff --git a/crates/router/src/analytics/metrics/request.rs b/crates/router/src/analytics/metrics/request.rs new file mode 100644 index 000000000000..b7c202f2db25 --- /dev/null +++ b/crates/router/src/analytics/metrics/request.rs @@ -0,0 +1,60 @@ +pub fn add_attributes>( + key: &'static str, + value: T, +) -> router_env::opentelemetry::KeyValue { + router_env::opentelemetry::KeyValue::new(key, value) +} + +#[inline] +pub async fn record_operation_time( + future: F, + metric: &once_cell::sync::Lazy>, + metric_name: &api_models::analytics::payments::PaymentMetrics, + source: &crate::analytics::AnalyticsProvider, +) -> R +where + F: futures::Future, +{ + let (result, time) = time_future(future).await; + let attributes = &[ + add_attributes("metric_name", metric_name.to_string()), + add_attributes( + "source", + match source { + crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", + }, + ), + ]; + let value = time.as_secs_f64(); + metric.record(&super::CONTEXT, value, attributes); + + router_env::logger::debug!("Attributes: {:?}, Time: {}", attributes, value); + result +} + +use std::time; + +#[inline] +pub async fn time_future(future: F) -> (R, time::Duration) +where + F: futures::Future, +{ + let start = time::Instant::now(); + let result = future.await; + let time_spent = start.elapsed(); + (result, time_spent) +} + +#[macro_export] +macro_rules! histogram_metric { + ($name:ident, $meter:ident) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); + }; + ($name:ident, $meter:ident, $description:literal) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); + }; +} diff --git a/crates/router/src/analytics/payments.rs b/crates/router/src/analytics/payments.rs new file mode 100644 index 000000000000..527bf75a3c72 --- /dev/null +++ b/crates/router/src/analytics/payments.rs @@ -0,0 +1,13 @@ +pub mod accumulator; +mod core; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{PaymentMetricAccumulator, PaymentMetricsAccumulator}; + +pub trait PaymentAnalytics: + metrics::PaymentMetricAnalytics + filters::PaymentFilterAnalytics +{ +} + +pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/payments/accumulator.rs b/crates/router/src/analytics/payments/accumulator.rs new file mode 100644 index 000000000000..5eebd0974693 --- /dev/null +++ b/crates/router/src/analytics/payments/accumulator.rs @@ -0,0 +1,150 @@ +use api_models::analytics::payments::PaymentMetricsBucketValue; +use common_enums::enums as storage_enums; +use router_env::logger; + +use super::metrics::PaymentMetricRow; + +#[derive(Debug, Default)] +pub struct PaymentMetricsAccumulator { + pub payment_success_rate: SuccessRateAccumulator, + pub payment_count: CountAccumulator, + pub payment_success: CountAccumulator, + pub processed_amount: SumAccumulator, + pub avg_ticket_size: AverageAccumulator, +} + +#[derive(Debug, Default)] +pub struct SuccessRateAccumulator { + pub success: i64, + pub total: i64, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct SumAccumulator { + pub total: Option, +} + +#[derive(Debug, Default)] +pub struct AverageAccumulator { + pub total: u32, + pub count: u32, +} + +pub trait PaymentMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl PaymentMetricAccumulator for SuccessRateAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + if let Some(ref status) = metrics.status { + if status.as_ref() == &storage_enums::AttemptStatus::Charged { + self.success += metrics.count.unwrap_or_default(); + } + }; + self.total += metrics.count.unwrap_or_default(); + } + + fn collect(self) -> Self::MetricOutput { + if self.total <= 0 { + None + } else { + Some( + f64::from(u32::try_from(self.success).ok()?) * 100.0 + / f64::from(u32::try_from(self.total).ok()?), + ) + } + } +} + +impl PaymentMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl PaymentMetricAccumulator for SumAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + self.total = match ( + self.total, + metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_i64), + ) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + u64::try_from(self.total.unwrap_or(0)).ok() + } +} + +impl PaymentMetricAccumulator for AverageAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) { + let total = metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_u32); + let count = metrics.count.and_then(|total| u32::try_from(total).ok()); + + match (total, count) { + (Some(total), Some(count)) => { + self.total += total; + self.count += count; + } + _ => { + logger::error!(message="Dropping metrics for average accumulator", metric=?metrics); + } + } + } + + fn collect(self) -> Self::MetricOutput { + if self.count == 0 { + None + } else { + Some(f64::from(self.total) / f64::from(self.count)) + } + } +} + +impl PaymentMetricsAccumulator { + pub fn collect(self) -> PaymentMetricsBucketValue { + PaymentMetricsBucketValue { + payment_success_rate: self.payment_success_rate.collect(), + payment_count: self.payment_count.collect(), + payment_success_count: self.payment_success.collect(), + payment_processed_amount: self.processed_amount.collect(), + avg_ticket_size: self.avg_ticket_size.collect(), + } + } +} diff --git a/crates/router/src/analytics/payments/core.rs b/crates/router/src/analytics/payments/core.rs new file mode 100644 index 000000000000..23eca8879a70 --- /dev/null +++ b/crates/router/src/analytics/payments/core.rs @@ -0,0 +1,129 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + payments::{MetricsBucketResponse, PaymentMetrics, PaymentMetricsBucketIdentifier}, + AnalyticsMetadata, GetPaymentMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + instrument, logger, + tracing::{self, Instrument}, +}; + +use super::PaymentMetricsAccumulator; +use crate::{ + analytics::{ + core::AnalyticsApiResponse, errors::AnalyticsError, metrics, + payments::PaymentMetricAccumulator, AnalyticsProvider, + }, + services::ApplicationResponse, + types::domain, +}; + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: AnalyticsProvider, + merchant_account: domain::MerchantAccount, + req: GetPaymentMetricRequest, +) -> AnalyticsApiResponse> { + let mut metrics_accumulator: HashMap< + PaymentMetricsBucketIdentifier, + PaymentMetricsAccumulator, + > = HashMap::new(); + + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let merchant_id = merchant_account.merchant_id.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_payments_query", + payment_metric = metric_type.as_ref() + ); + set.spawn( + async move { + let data = pool + .get_payment_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + let data = data?; + let attributes = &[ + metrics::request::add_attributes("metric_type", metric.to_string()), + metrics::request::add_attributes( + "source", + match pool { + crate::analytics::AnalyticsProvider::Sqlx(_) => "Sqlx", + }, + ), + ]; + + let value = u64::try_from(data.len()); + if let Ok(val) = value { + metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes); + logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val); + } + + for (id, value) in data { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + PaymentMetrics::PaymentSuccessRate => metrics_builder + .payment_success_rate + .add_metrics_bucket(&value), + PaymentMetrics::PaymentCount => { + metrics_builder.payment_count.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentSuccessCount => { + metrics_builder.payment_success.add_metrics_bucket(&value) + } + PaymentMetrics::PaymentProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + PaymentMetrics::AvgTicketSize => { + metrics_builder.avg_ticket_size.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(ApplicationResponse::Json(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + })) +} diff --git a/crates/router/src/analytics/payments/filters.rs b/crates/router/src/analytics/payments/filters.rs new file mode 100644 index 000000000000..f009aaa76329 --- /dev/null +++ b/crates/router/src/analytics/payments/filters.rs @@ -0,0 +1,58 @@ +use api_models::analytics::{payments::PaymentDimensions, Granularity, TimeRange}; +use common_enums::enums::{AttemptStatus, AuthenticationType, Currency}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, + types::{ + AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, + LoadRow, + }, +}; + +pub trait PaymentFilterAnalytics: LoadRow {} + +pub async fn get_payment_filter_for_dimension( + dimension: PaymentDimensions, + merchant: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + PaymentFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq)] +pub struct FilterRow { + pub currency: Option>, + pub status: Option>, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, +} diff --git a/crates/router/src/analytics/payments/metrics.rs b/crates/router/src/analytics/payments/metrics.rs new file mode 100644 index 000000000000..f492e5bd4df9 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics.rs @@ -0,0 +1,137 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use time::PrimitiveDateTime; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; + +mod avg_ticket_size; +mod payment_count; +mod payment_processed_amount; +mod payment_success_count; +mod success_rate; + +use avg_ticket_size::AvgTicketSize; +use payment_count::PaymentCount; +use payment_processed_amount::PaymentProcessedAmount; +use payment_success_count::PaymentSuccessCount; +use success_rate::PaymentSuccessRate; + +#[derive(Debug, PartialEq, Eq)] +pub struct PaymentMetricRow { + pub currency: Option>, + pub status: Option>, + pub connector: Option, + pub authentication_type: Option>, + pub payment_method: Option, + pub total: Option, + pub count: Option, + pub start_bucket: Option, + pub end_bucket: Option, +} + +pub trait PaymentMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait PaymentMetric +where + T: AnalyticsDataSource + PaymentMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl PaymentMetric for PaymentMetrics +where + T: AnalyticsDataSource + PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::PaymentSuccessRate => { + PaymentSuccessRate + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentCount => { + PaymentCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentSuccessCount => { + PaymentSuccessCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentProcessedAmount => { + PaymentProcessedAmount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::AvgTicketSize => { + AvgTicketSize + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs b/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs new file mode 100644 index 000000000000..2230d870e68a --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/avg_ticket_size.rs @@ -0,0 +1,126 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::{PaymentMetric, PaymentMetricRow}; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct AvgTicketSize; + +#[async_trait::async_trait] +impl PaymentMetric for AvgTicketSize +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.status.as_ref().map(|i| i.0), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_count.rs b/crates/router/src/analytics/payments/metrics/payment_count.rs new file mode 100644 index 000000000000..661cec3dac36 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/payment_count.rs @@ -0,0 +1,117 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentCount; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentCount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.status.as_ref().map(|i| i.0), + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::analytics::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs b/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs new file mode 100644 index 000000000000..2ec0c6f18f9c --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/payment_processed_amount.rs @@ -0,0 +1,128 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentProcessedAmount; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentProcessedAmount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Charged, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/payment_success_count.rs b/crates/router/src/analytics/payments/metrics/payment_success_count.rs new file mode 100644 index 000000000000..8245fe7aeb88 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/payment_success_count.rs @@ -0,0 +1,127 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentSuccessCount; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentSuccessCount +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .add_filter_clause( + PaymentDimensions::PaymentStatus, + storage_enums::AttemptStatus::Charged, + ) + .switch()?; + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/metrics/success_rate.rs b/crates/router/src/analytics/payments/metrics/success_rate.rs new file mode 100644 index 000000000000..c63956d4b157 --- /dev/null +++ b/crates/router/src/analytics/payments/metrics/success_rate.rs @@ -0,0 +1,123 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentSuccessRate; + +#[async_trait::async_trait] +impl super::PaymentMetric for PaymentSuccessRate +where + T: AnalyticsDataSource + super::PaymentMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentDimensions], + merchant_id: &str, + filters: &PaymentFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Payment); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(PaymentDimensions::PaymentStatus); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.authentication_type.as_ref().map(|i| i.0), + i.payment_method.clone(), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/payments/types.rs b/crates/router/src/analytics/payments/types.rs new file mode 100644 index 000000000000..fdfbedef383d --- /dev/null +++ b/crates/router/src/analytics/payments/types.rs @@ -0,0 +1,46 @@ +use api_models::analytics::payments::{PaymentDimensions, PaymentFilters}; +use error_stack::ResultExt; + +use crate::analytics::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for PaymentFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.currency.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::Currency, &self.currency) + .attach_printable("Error adding currency filter")?; + } + + if !self.status.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::PaymentStatus, &self.status) + .attach_printable("Error adding payment status filter")?; + } + + if !self.connector.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::Connector, &self.connector) + .attach_printable("Error adding connector filter")?; + } + + if !self.auth_type.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::AuthType, &self.auth_type) + .attach_printable("Error adding auth type filter")?; + } + + if !self.payment_method.is_empty() { + builder + .add_filter_in_range_clause(PaymentDimensions::PaymentMethod, &self.payment_method) + .attach_printable("Error adding payment method filter")?; + } + Ok(()) + } +} diff --git a/crates/router/src/analytics/query.rs b/crates/router/src/analytics/query.rs new file mode 100644 index 000000000000..b1f621d8153d --- /dev/null +++ b/crates/router/src/analytics/query.rs @@ -0,0 +1,533 @@ +#![allow(dead_code)] +use std::marker::PhantomData; + +use api_models::{ + analytics::{ + self as analytics_api, + payments::PaymentDimensions, + refunds::{RefundDimensions, RefundType}, + Granularity, + }, + enums::Connector, + refunds::RefundStatus, +}; +use common_enums::{ + enums as storage_enums, + enums::{AttemptStatus, AuthenticationType, Currency, PaymentMethod}, +}; +use common_utils::errors::{CustomResult, ParsingError}; +use error_stack::{IntoReport, ResultExt}; +use router_env::logger; + +use super::types::{AnalyticsCollection, AnalyticsDataSource, LoadRow}; +use crate::analytics::types::QueryExecutionError; +pub type QueryResult = error_stack::Result; +pub trait QueryFilter +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()>; +} + +pub trait GroupByClause +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_group_by_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()>; +} + +pub trait SeriesBucket { + type SeriesType; + type GranularityLevel; + + fn get_lowest_common_granularity_level(&self) -> Self::GranularityLevel; + + fn get_bucket_size(&self) -> u8; + + fn clip_to_start( + &self, + value: Self::SeriesType, + ) -> error_stack::Result; + + fn clip_to_end( + &self, + value: Self::SeriesType, + ) -> error_stack::Result; +} + +impl QueryFilter for analytics_api::TimeRange +where + T: AnalyticsDataSource, + time::PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + builder.add_custom_filter_clause("created_at", self.start_time, FilterTypes::Gte)?; + if let Some(end) = self.end_time { + builder.add_custom_filter_clause("created_at", end, FilterTypes::Lte)?; + } + Ok(()) + } +} + +impl GroupByClause for Granularity { + fn set_group_by_clause( + &self, + builder: &mut QueryBuilder, + ) -> QueryResult<()> { + let trunc_scale = self.get_lowest_common_granularity_level(); + + let granularity_bucket_scale = match self { + Self::OneMin => None, + Self::FiveMin | Self::FifteenMin | Self::ThirtyMin => Some("minute"), + Self::OneHour | Self::OneDay => None, + }; + + let granularity_divisor = self.get_bucket_size(); + + builder + .add_group_by_clause(format!("DATE_TRUNC('{trunc_scale}', modified_at)")) + .attach_printable("Error adding time prune group by")?; + if let Some(scale) = granularity_bucket_scale { + builder + .add_group_by_clause(format!( + "FLOOR(DATE_PART('{scale}', modified_at)/{granularity_divisor})" + )) + .attach_printable("Error adding time binning group by")?; + } + Ok(()) + } +} + +#[derive(strum::Display)] +#[strum(serialize_all = "lowercase")] +pub enum TimeGranularityLevel { + Minute, + Hour, + Day, +} + +impl SeriesBucket for Granularity { + type SeriesType = time::PrimitiveDateTime; + + type GranularityLevel = TimeGranularityLevel; + + fn get_lowest_common_granularity_level(&self) -> Self::GranularityLevel { + match self { + Self::OneMin => TimeGranularityLevel::Minute, + Self::FiveMin | Self::FifteenMin | Self::ThirtyMin | Self::OneHour => { + TimeGranularityLevel::Hour + } + Self::OneDay => TimeGranularityLevel::Day, + } + } + + fn get_bucket_size(&self) -> u8 { + match self { + Self::OneMin => 60, + Self::FiveMin => 5, + Self::FifteenMin => 15, + Self::ThirtyMin => 30, + Self::OneHour => 60, + Self::OneDay => 24, + } + } + + fn clip_to_start( + &self, + value: Self::SeriesType, + ) -> error_stack::Result { + let clip_start = |value: u8, modulo: u8| -> u8 { value - value % modulo }; + + let clipped_time = match ( + self.get_lowest_common_granularity_level(), + self.get_bucket_size(), + ) { + (TimeGranularityLevel::Minute, i) => time::Time::MIDNIGHT + .replace_second(clip_start(value.second(), i)) + .and_then(|t| t.replace_minute(value.minute())) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Hour, i) => time::Time::MIDNIGHT + .replace_minute(clip_start(value.minute(), i)) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Day, i) => { + time::Time::MIDNIGHT.replace_hour(clip_start(value.hour(), i)) + } + } + .into_report() + .change_context(PostProcessingError::BucketClipping)?; + + Ok(value.replace_time(clipped_time)) + } + + fn clip_to_end( + &self, + value: Self::SeriesType, + ) -> error_stack::Result { + let clip_end = |value: u8, modulo: u8| -> u8 { value + modulo - 1 - value % modulo }; + + let clipped_time = match ( + self.get_lowest_common_granularity_level(), + self.get_bucket_size(), + ) { + (TimeGranularityLevel::Minute, i) => time::Time::MIDNIGHT + .replace_second(clip_end(value.second(), i)) + .and_then(|t| t.replace_minute(value.minute())) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Hour, i) => time::Time::MIDNIGHT + .replace_minute(clip_end(value.minute(), i)) + .and_then(|t| t.replace_hour(value.hour())), + (TimeGranularityLevel::Day, i) => { + time::Time::MIDNIGHT.replace_hour(clip_end(value.hour(), i)) + } + } + .into_report() + .change_context(PostProcessingError::BucketClipping) + .attach_printable_lazy(|| format!("Bucket Clip Error: {value}"))?; + + Ok(value.replace_time(clipped_time)) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum QueryBuildingError { + #[allow(dead_code)] + #[error("Not Implemented: {0}")] + NotImplemented(String), + #[error("Failed to Serialize to SQL")] + SqlSerializeError, + #[error("Failed to build sql query: {0}")] + InvalidQuery(&'static str), +} + +#[derive(thiserror::Error, Debug)] +pub enum PostProcessingError { + #[error("Error Clipping values to bucket sizes")] + BucketClipping, +} + +#[derive(Debug)] +pub enum Aggregate { + Count { + field: Option, + alias: Option<&'static str>, + }, + Sum { + field: R, + alias: Option<&'static str>, + }, + Min { + field: R, + alias: Option<&'static str>, + }, + Max { + field: R, + alias: Option<&'static str>, + }, +} + +#[derive(Debug)] +pub struct QueryBuilder +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + columns: Vec, + filters: Vec<(String, FilterTypes, String)>, + group_by: Vec, + having: Option>, + table: AnalyticsCollection, + distinct: bool, + db_type: PhantomData, +} + +pub trait ToSql { + fn to_sql(&self) -> error_stack::Result; +} + +/// Implement `ToSql` on arrays of types that impl `ToString`. +macro_rules! impl_to_sql_for_to_string { + ($($type:ty),+) => { + $( + impl ToSql for $type { + fn to_sql(&self) -> error_stack::Result { + Ok(self.to_string()) + } + } + )+ + }; +} + +impl_to_sql_for_to_string!( + String, + &str, + &PaymentDimensions, + &RefundDimensions, + PaymentDimensions, + RefundDimensions, + PaymentMethod, + AuthenticationType, + Connector, + AttemptStatus, + RefundStatus, + storage_enums::RefundStatus, + Currency, + RefundType, + &String, + &bool, + &u64 +); + +#[allow(dead_code)] +#[derive(Debug)] +pub enum FilterTypes { + Equal, + EqualBool, + In, + Gte, + Lte, + Gt, +} + +impl QueryBuilder +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + pub fn new(table: AnalyticsCollection) -> Self { + Self { + columns: Default::default(), + filters: Default::default(), + group_by: Default::default(), + having: Default::default(), + table, + distinct: Default::default(), + db_type: Default::default(), + } + } + + pub fn add_select_column(&mut self, column: impl ToSql) -> QueryResult<()> { + self.columns.push( + column + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing select column")?, + ); + Ok(()) + } + + pub fn set_distinct(&mut self) { + self.distinct = true + } + + pub fn add_filter_clause( + &mut self, + key: impl ToSql, + value: impl ToSql, + ) -> QueryResult<()> { + self.add_custom_filter_clause(key, value, FilterTypes::Equal) + } + + pub fn add_bool_filter_clause( + &mut self, + key: impl ToSql, + value: impl ToSql, + ) -> QueryResult<()> { + self.add_custom_filter_clause(key, value, FilterTypes::EqualBool) + } + + pub fn add_custom_filter_clause( + &mut self, + lhs: impl ToSql, + rhs: impl ToSql, + comparison: FilterTypes, + ) -> QueryResult<()> { + self.filters.push(( + lhs.to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing filter key")?, + comparison, + rhs.to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing filter value")?, + )); + Ok(()) + } + + pub fn add_filter_in_range_clause( + &mut self, + key: impl ToSql, + values: &[impl ToSql], + ) -> QueryResult<()> { + let list = values + .iter() + .map(|i| { + // trimming whitespaces from the filter values received in request, to prevent a possibility of an SQL injection + i.to_sql().map(|s| { + let trimmed_str = s.replace(' ', ""); + format!("'{trimmed_str}'") + }) + }) + .collect::, ParsingError>>() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing range filter value")? + .join(", "); + self.add_custom_filter_clause(key, list, FilterTypes::In) + } + + pub fn add_group_by_clause(&mut self, column: impl ToSql) -> QueryResult<()> { + self.group_by.push( + column + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing group by field")?, + ); + Ok(()) + } + + pub fn add_granularity_in_mins(&mut self, granularity: &Granularity) -> QueryResult<()> { + let interval = match granularity { + Granularity::OneMin => "1", + Granularity::FiveMin => "5", + Granularity::FifteenMin => "15", + Granularity::ThirtyMin => "30", + Granularity::OneHour => "60", + Granularity::OneDay => "1440", + }; + let _ = self.add_select_column(format!( + "toStartOfInterval(created_at, INTERVAL {interval} MINUTE) as time_bucket" + )); + Ok(()) + } + + fn get_filter_clause(&self) -> String { + self.filters + .iter() + .map(|(l, op, r)| match op { + FilterTypes::EqualBool => format!("{l} = {r}"), + FilterTypes::Equal => format!("{l} = '{r}'"), + FilterTypes::In => format!("{l} IN ({r})"), + FilterTypes::Gte => format!("{l} >= '{r}'"), + FilterTypes::Gt => format!("{l} > {r}"), + FilterTypes::Lte => format!("{l} <= '{r}'"), + }) + .collect::>() + .join(" AND ") + } + + fn get_select_clause(&self) -> String { + self.columns.join(", ") + } + + fn get_group_by_clause(&self) -> String { + self.group_by.join(", ") + } + + #[allow(dead_code)] + pub fn add_having_clause( + &mut self, + aggregate: Aggregate, + filter_type: FilterTypes, + value: impl ToSql, + ) -> QueryResult<()> + where + Aggregate: ToSql, + { + let aggregate = aggregate + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing having aggregate")?; + let value = value + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing having value")?; + let entry = (aggregate, filter_type, value); + if let Some(having) = &mut self.having { + having.push(entry); + } else { + self.having = Some(vec![entry]); + } + Ok(()) + } + + pub fn get_filter_type_clause(&self) -> Option { + self.having.as_ref().map(|vec| { + vec.iter() + .map(|(l, op, r)| match op { + FilterTypes::Equal | FilterTypes::EqualBool => format!("{l} = {r}"), + FilterTypes::In => format!("{l} IN ({r})"), + FilterTypes::Gte => format!("{l} >= {r}"), + FilterTypes::Lte => format!("{l} < {r}"), + FilterTypes::Gt => format!("{l} > {r}"), + }) + .collect::>() + .join(" AND ") + }) + } + + pub fn build_query(&mut self) -> QueryResult + where + Aggregate<&'static str>: ToSql, + { + if self.columns.is_empty() { + Err(QueryBuildingError::InvalidQuery( + "No select fields provided", + )) + .into_report()?; + } + let mut query = String::from("SELECT "); + + if self.distinct { + query.push_str("DISTINCT "); + } + + query.push_str(&self.get_select_clause()); + + query.push_str(" FROM "); + + query.push_str( + &self + .table + .to_sql() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Error serializing table value")?, + ); + + if !self.filters.is_empty() { + query.push_str(" WHERE "); + query.push_str(&self.get_filter_clause()); + } + + if !self.group_by.is_empty() { + query.push_str(" GROUP BY "); + query.push_str(&self.get_group_by_clause()); + } + + if self.having.is_some() { + if let Some(condition) = self.get_filter_type_clause() { + query.push_str(" HAVING "); + query.push_str(condition.as_str()); + } + } + Ok(query) + } + + pub async fn execute_query( + &mut self, + store: &P, + ) -> CustomResult, QueryExecutionError>, QueryBuildingError> + where + P: LoadRow, + Aggregate<&'static str>: ToSql, + { + let query = self + .build_query() + .change_context(QueryBuildingError::SqlSerializeError) + .attach_printable("Failed to execute query")?; + logger::debug!(?query); + Ok(store.load_results(query.as_str()).await) + } +} diff --git a/crates/router/src/analytics/refunds.rs b/crates/router/src/analytics/refunds.rs new file mode 100644 index 000000000000..a8b52effe76d --- /dev/null +++ b/crates/router/src/analytics/refunds.rs @@ -0,0 +1,10 @@ +pub mod accumulator; +mod core; + +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{RefundMetricAccumulator, RefundMetricsAccumulator}; + +pub trait RefundAnalytics: metrics::RefundMetricAnalytics {} +pub use self::core::get_metrics; diff --git a/crates/router/src/analytics/refunds/accumulator.rs b/crates/router/src/analytics/refunds/accumulator.rs new file mode 100644 index 000000000000..3d0c0e659f6c --- /dev/null +++ b/crates/router/src/analytics/refunds/accumulator.rs @@ -0,0 +1,110 @@ +use api_models::analytics::refunds::RefundMetricsBucketValue; +use common_enums::enums as storage_enums; + +use super::metrics::RefundMetricRow; +#[derive(Debug, Default)] +pub struct RefundMetricsAccumulator { + pub refund_success_rate: SuccessRateAccumulator, + pub refund_count: CountAccumulator, + pub refund_success: CountAccumulator, + pub processed_amount: SumAccumulator, +} + +#[derive(Debug, Default)] +pub struct SuccessRateAccumulator { + pub success: i64, + pub total: i64, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct SumAccumulator { + pub total: Option, +} + +pub trait RefundMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +impl RefundMetricAccumulator for CountAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { + self.count = match (self.count, metrics.count) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.count.and_then(|i| u64::try_from(i).ok()) + } +} + +impl RefundMetricAccumulator for SumAccumulator { + type MetricOutput = Option; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { + self.total = match ( + self.total, + metrics + .total + .as_ref() + .and_then(bigdecimal::ToPrimitive::to_i64), + ) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.total.and_then(|i| u64::try_from(i).ok()) + } +} + +impl RefundMetricAccumulator for SuccessRateAccumulator { + type MetricOutput = Option; + + fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { + if let Some(ref refund_status) = metrics.refund_status { + if refund_status.as_ref() == &storage_enums::RefundStatus::Success { + self.success += metrics.count.unwrap_or_default(); + } + }; + self.total += metrics.count.unwrap_or_default(); + } + + fn collect(self) -> Self::MetricOutput { + if self.total <= 0 { + None + } else { + Some( + f64::from(u32::try_from(self.success).ok()?) * 100.0 + / f64::from(u32::try_from(self.total).ok()?), + ) + } + } +} + +impl RefundMetricsAccumulator { + pub fn collect(self) -> RefundMetricsBucketValue { + RefundMetricsBucketValue { + refund_success_rate: self.refund_success_rate.collect(), + refund_count: self.refund_count.collect(), + refund_success_count: self.refund_success.collect(), + refund_processed_amount: self.processed_amount.collect(), + } + } +} diff --git a/crates/router/src/analytics/refunds/core.rs b/crates/router/src/analytics/refunds/core.rs new file mode 100644 index 000000000000..4c2d2c394181 --- /dev/null +++ b/crates/router/src/analytics/refunds/core.rs @@ -0,0 +1,104 @@ +use std::collections::HashMap; + +use api_models::analytics::{ + refunds::{RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse}, + AnalyticsMetadata, GetRefundMetricRequest, MetricsResponse, +}; +use error_stack::{IntoReport, ResultExt}; +use router_env::{ + logger, + tracing::{self, Instrument}, +}; + +use super::RefundMetricsAccumulator; +use crate::{ + analytics::{ + core::AnalyticsApiResponse, errors::AnalyticsError, refunds::RefundMetricAccumulator, + AnalyticsProvider, + }, + services::ApplicationResponse, + types::domain, +}; + +pub async fn get_metrics( + pool: AnalyticsProvider, + merchant_account: domain::MerchantAccount, + req: GetRefundMetricRequest, +) -> AnalyticsApiResponse> { + let mut metrics_accumulator: HashMap = + HashMap::new(); + let mut set = tokio::task::JoinSet::new(); + for metric_type in req.metrics.iter().cloned() { + let req = req.clone(); + let merchant_id = merchant_account.merchant_id.clone(); + let pool = pool.clone(); + let task_span = tracing::debug_span!( + "analytics_refund_query", + refund_metric = metric_type.as_ref() + ); + set.spawn( + async move { + let data = pool + .get_refund_metrics( + &metric_type, + &req.group_by_names.clone(), + &merchant_id, + &req.filters, + &req.time_series.map(|t| t.granularity), + &req.time_range, + ) + .await + .change_context(AnalyticsError::UnknownError); + (metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some((metric, data)) = set + .join_next() + .await + .transpose() + .into_report() + .change_context(AnalyticsError::UnknownError)? + { + for (id, value) in data? { + logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); + let metrics_builder = metrics_accumulator.entry(id).or_default(); + match metric { + RefundMetrics::RefundSuccessRate => metrics_builder + .refund_success_rate + .add_metrics_bucket(&value), + RefundMetrics::RefundCount => { + metrics_builder.refund_count.add_metrics_bucket(&value) + } + RefundMetrics::RefundSuccessCount => { + metrics_builder.refund_success.add_metrics_bucket(&value) + } + RefundMetrics::RefundProcessedAmount => { + metrics_builder.processed_amount.add_metrics_bucket(&value) + } + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + let query_data: Vec = metrics_accumulator + .into_iter() + .map(|(id, val)| RefundMetricsBucketResponse { + values: val.collect(), + dimensions: id, + }) + .collect(); + + Ok(ApplicationResponse::Json(MetricsResponse { + query_data, + meta_data: [AnalyticsMetadata { + current_time_range: req.time_range, + }], + })) +} diff --git a/crates/router/src/analytics/refunds/filters.rs b/crates/router/src/analytics/refunds/filters.rs new file mode 100644 index 000000000000..6b45e9194fad --- /dev/null +++ b/crates/router/src/analytics/refunds/filters.rs @@ -0,0 +1,59 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundType}, + Granularity, TimeRange, +}; +use common_enums::enums::{Currency, RefundStatus}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql}, + types::{ + AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult, + LoadRow, + }, +}; +pub trait RefundFilterAnalytics: LoadRow {} + +pub async fn get_refund_filter_for_dimension( + dimension: RefundDimensions, + merchant: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult> +where + T: AnalyticsDataSource + RefundFilterAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); + + query_builder.add_select_column(dimension).switch()?; + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq)] +pub struct RefundFilterRow { + pub currency: Option>, + pub refund_status: Option>, + pub connector: Option, + pub refund_type: Option>, +} diff --git a/crates/router/src/analytics/refunds/metrics.rs b/crates/router/src/analytics/refunds/metrics.rs new file mode 100644 index 000000000000..d4f509b4a1e3 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics.rs @@ -0,0 +1,126 @@ +use api_models::analytics::{ + refunds::{ + RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier, RefundType, + }, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use time::PrimitiveDateTime; +mod refund_count; +mod refund_processed_amount; +mod refund_success_count; +mod refund_success_rate; +use refund_count::RefundCount; +use refund_processed_amount::RefundProcessedAmount; +use refund_success_count::RefundSuccessCount; +use refund_success_rate::RefundSuccessRate; + +use crate::analytics::{ + query::{Aggregate, GroupByClause, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, +}; + +#[derive(Debug, Eq, PartialEq)] +pub struct RefundMetricRow { + pub currency: Option>, + pub refund_status: Option>, + pub connector: Option, + pub refund_type: Option>, + pub total: Option, + pub count: Option, + pub start_bucket: Option, + pub end_bucket: Option, +} + +pub trait RefundMetricAnalytics: LoadRow {} + +#[async_trait::async_trait] +pub trait RefundMetric +where + T: AnalyticsDataSource + RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult>; +} + +#[async_trait::async_trait] +impl RefundMetric for RefundMetrics +where + T: AnalyticsDataSource + RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + match self { + Self::RefundSuccessRate => { + RefundSuccessRate::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::RefundCount => { + RefundCount::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::RefundSuccessCount => { + RefundSuccessCount::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::RefundProcessedAmount => { + RefundProcessedAmount::default() + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_count.rs b/crates/router/src/analytics/refunds/metrics/refund_count.rs new file mode 100644 index 000000000000..471327235073 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_count.rs @@ -0,0 +1,116 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct RefundCount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundCount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder + .add_group_by_clause(dim) + .attach_printable("Error grouping by dimensions") + .switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.refund_status.as_ref().map(|i| i.0), + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::analytics::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs b/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs new file mode 100644 index 000000000000..c5f3a706aaef --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_processed_amount.rs @@ -0,0 +1,122 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(super) struct RefundProcessedAmount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundProcessedAmount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::Refund); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "refund_amount", + alias: Some("total"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range + .set_filter_clause(&mut query_builder) + .attach_printable("Error filtering time range") + .switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause( + RefundDimensions::RefundStatus, + storage_enums::RefundStatus::Success, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, crate::analytics::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_count.rs b/crates/router/src/analytics/refunds/metrics/refund_success_count.rs new file mode 100644 index 000000000000..0c8032908fd7 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_success_count.rs @@ -0,0 +1,122 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_enums::enums as storage_enums; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct RefundSuccessCount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundSuccessCount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::Refund); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range.set_filter_clause(&mut query_builder).switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause( + RefundDimensions::RefundStatus, + storage_enums::RefundStatus::Success, + ) + .switch()?; + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs b/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs new file mode 100644 index 000000000000..42f9ccf8d3c0 --- /dev/null +++ b/crates/router/src/analytics/refunds/metrics/refund_success_rate.rs @@ -0,0 +1,117 @@ +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::analytics::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(super) struct RefundSuccessRate {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundSuccessRate +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + merchant_id: &str, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::Refund); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(RefundDimensions::RefundStatus); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Count { + field: None, + alias: Some("count"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Min { + field: "created_at", + alias: Some("start_bucket"), + }) + .switch()?; + query_builder + .add_select_column(Aggregate::Max { + field: "created_at", + alias: Some("end_bucket"), + }) + .switch()?; + + filters.set_filter_clause(&mut query_builder).switch()?; + + query_builder + .add_filter_clause("merchant_id", merchant_id) + .switch()?; + + time_range.set_filter_clause(&mut query_builder).switch()?; + + for dim in dimensions.iter() { + query_builder.add_group_by_clause(dim).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => time_range.start_time, + }, + end_time: granularity.as_ref().map_or_else( + || Ok(time_range.end_time), + |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(), + )?, + }, + ), + i, + )) + }) + .collect::, + crate::analytics::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/router/src/analytics/refunds/types.rs b/crates/router/src/analytics/refunds/types.rs new file mode 100644 index 000000000000..fbfd69972671 --- /dev/null +++ b/crates/router/src/analytics/refunds/types.rs @@ -0,0 +1,41 @@ +use api_models::analytics::refunds::{RefundDimensions, RefundFilters}; +use error_stack::ResultExt; + +use crate::analytics::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl QueryFilter for RefundFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder) -> QueryResult<()> { + if !self.currency.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::Currency, &self.currency) + .attach_printable("Error adding currency filter")?; + } + + if !self.refund_status.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::RefundStatus, &self.refund_status) + .attach_printable("Error adding refund status filter")?; + } + + if !self.connector.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::Connector, &self.connector) + .attach_printable("Error adding connector filter")?; + } + + if !self.refund_type.is_empty() { + builder + .add_filter_in_range_clause(RefundDimensions::RefundType, &self.refund_type) + .attach_printable("Error adding auth type filter")?; + } + + Ok(()) + } +} diff --git a/crates/router/src/analytics/routes.rs b/crates/router/src/analytics/routes.rs new file mode 100644 index 000000000000..298ec61ec903 --- /dev/null +++ b/crates/router/src/analytics/routes.rs @@ -0,0 +1,145 @@ +use actix_web::{web, Responder, Scope}; +use api_models::analytics::{ + GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, + GetRefundMetricRequest, +}; +use router_env::AnalyticsFlow; + +use super::{core::*, payments, refunds, types::AnalyticsDomain}; +use crate::{ + core::api_locking, + services::{api, authentication as auth, authentication::AuthenticationData}, + AppState, +}; + +pub struct Analytics; + +impl Analytics { + pub fn server(state: AppState) -> Scope { + let route = web::scope("/analytics/v1").app_data(web::Data::new(state)); + route + .service(web::resource("metrics/payments").route(web::post().to(get_payment_metrics))) + .service(web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics))) + .service(web::resource("filters/payments").route(web::post().to(get_payment_filters))) + .service(web::resource("filters/refunds").route(web::post().to(get_refund_filters))) + .service(web::resource("{domain}/info").route(web::get().to(get_info))) + } +} + +pub async fn get_info( + state: web::Data, + req: actix_web::HttpRequest, + domain: actix_web::web::Path, +) -> impl Responder { + let flow = AnalyticsFlow::GetInfo; + api::server_wrap( + flow, + state, + &req, + domain.into_inner(), + |_, _, domain| get_domain_info(domain), + &auth::NoAuth, + api_locking::LockAction::NotApplicable, + ) + .await +} + +/// # Panics +/// +/// Panics if `json_payload` array does not contain one `GetPaymentMetricRequest` element. +pub async fn get_payment_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetPaymentMetricRequest; 1]>, +) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetPaymentMetricRequest"); + let flow = AnalyticsFlow::GetPaymentMetrics; + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| { + payments::get_metrics(state.pool.clone(), auth.merchant_account, req) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} + +/// # Panics +/// +/// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. +pub async fn get_refunds_metrics( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetRefundMetricRequest; 1]>, +) -> impl Responder { + #[allow(clippy::expect_used)] + // safety: This shouldn't panic owing to the data type + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetRefundMetricRequest"); + let flow = AnalyticsFlow::GetRefundsMetrics; + api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req| { + refunds::get_metrics(state.pool.clone(), auth.merchant_account, req) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} + +pub async fn get_payment_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = AnalyticsFlow::GetPaymentFilters; + api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req| { + payment_filters_core(state.pool.clone(), req, auth.merchant_account) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} + +pub async fn get_refund_filters( + state: web::Data, + req: actix_web::HttpRequest, + json_payload: web::Json, +) -> impl Responder { + let flow = AnalyticsFlow::GetRefundFilters; + api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req: GetRefundFilterRequest| { + refund_filter_core(state.pool.clone(), req, auth.merchant_account) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), + api_locking::LockAction::NotApplicable, + ) + .await +} diff --git a/crates/router/src/analytics/sqlx.rs b/crates/router/src/analytics/sqlx.rs new file mode 100644 index 000000000000..b88a2065f0b0 --- /dev/null +++ b/crates/router/src/analytics/sqlx.rs @@ -0,0 +1,401 @@ +use std::{fmt::Display, str::FromStr}; + +use api_models::analytics::refunds::RefundType; +use common_enums::enums::{ + AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, +}; +use common_utils::errors::{CustomResult, ParsingError}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::{kms, kms::decrypt::KmsDecrypt}; +#[cfg(not(feature = "kms"))] +use masking::PeekInterface; +use sqlx::{ + postgres::{PgArgumentBuffer, PgPoolOptions, PgRow, PgTypeInfo, PgValueRef}, + Decode, Encode, + Error::ColumnNotFound, + FromRow, Pool, Postgres, Row, +}; +use time::PrimitiveDateTime; + +use super::{ + query::{Aggregate, ToSql}, + types::{ + AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, QueryExecutionError, + }, +}; +use crate::configs::settings::Database; + +#[derive(Debug, Clone)] +pub struct SqlxClient { + pool: Pool, +} + +impl Default for SqlxClient { + fn default() -> Self { + let database_url = format!( + "postgres://{}:{}@{}:{}/{}", + "db_user", "db_pass", "localhost", 5432, "hyperswitch_db" + ); + Self { + #[allow(clippy::expect_used)] + pool: PgPoolOptions::new() + .connect_lazy(&database_url) + .expect("SQLX Pool Creation failed"), + } + } +} + +impl SqlxClient { + pub async fn from_conf( + conf: &Database, + #[cfg(feature = "kms")] kms_client: &kms::KmsClient, + ) -> Self { + #[cfg(feature = "kms")] + #[allow(clippy::expect_used)] + let password = conf + .password + .decrypt_inner(kms_client) + .await + .expect("Failed to KMS decrypt database password"); + + #[cfg(not(feature = "kms"))] + let password = &conf.password.peek(); + let database_url = format!( + "postgres://{}:{}@{}:{}/{}", + conf.username, password, conf.host, conf.port, conf.dbname + ); + #[allow(clippy::expect_used)] + let pool = PgPoolOptions::new() + .max_connections(conf.pool_size) + .acquire_timeout(std::time::Duration::from_secs(conf.connection_timeout)) + .connect_lazy(&database_url) + .expect("SQLX Pool Creation failed"); + Self { pool } + } +} + +pub trait DbType { + fn name() -> &'static str; +} + +macro_rules! db_type { + ($a: ident, $str: tt) => { + impl DbType for $a { + fn name() -> &'static str { + stringify!($str) + } + } + }; + ($a:ident) => { + impl DbType for $a { + fn name() -> &'static str { + stringify!($a) + } + } + }; +} + +db_type!(Currency); +db_type!(AuthenticationType); +db_type!(AttemptStatus); +db_type!(PaymentMethod, TEXT); +db_type!(RefundStatus); +db_type!(RefundType); + +impl<'q, Type> Encode<'q, Postgres> for DBEnumWrapper +where + Type: DbType + FromStr + Display, +{ + fn encode_by_ref(&self, buf: &mut PgArgumentBuffer) -> sqlx::encode::IsNull { + self.0.to_string().encode(buf) + } + fn size_hint(&self) -> usize { + self.0.to_string().size_hint() + } +} + +impl<'r, Type> Decode<'r, Postgres> for DBEnumWrapper +where + Type: DbType + FromStr + Display, +{ + fn decode( + value: PgValueRef<'r>, + ) -> Result> { + let str_value = <&'r str as Decode<'r, Postgres>>::decode(value)?; + Type::from_str(str_value).map(DBEnumWrapper).or(Err(format!( + "invalid value {:?} for enum {}", + str_value, + Type::name() + ) + .into())) + } +} + +impl sqlx::Type for DBEnumWrapper +where + Type: DbType + FromStr + Display, +{ + fn type_info() -> PgTypeInfo { + PgTypeInfo::with_name(Type::name()) + } +} + +impl LoadRow for SqlxClient +where + for<'a> T: FromRow<'a, PgRow>, +{ + fn load_row(row: PgRow) -> CustomResult { + T::from_row(&row) + .into_report() + .change_context(QueryExecutionError::RowExtractionFailure) + } +} + +impl super::payments::filters::PaymentFilterAnalytics for SqlxClient {} +impl super::payments::metrics::PaymentMetricAnalytics for SqlxClient {} +impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} +impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} + +#[async_trait::async_trait] +impl AnalyticsDataSource for SqlxClient { + type Row = PgRow; + + async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> + where + Self: LoadRow, + { + sqlx::query(&format!("{query};")) + .fetch_all(&self.pool) + .await + .into_report() + .change_context(QueryExecutionError::DatabaseError) + .attach_printable_lazy(|| format!("Failed to run query {query}"))? + .into_iter() + .map(Self::load_row) + .collect::, _>>() + .change_context(QueryExecutionError::RowExtractionFailure) + } +} + +impl<'a> FromRow<'a, PgRow> for super::refunds::metrics::RefundMetricRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_status: Option> = + row.try_get("refund_status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_type: Option> = + row.try_get("refund_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + currency, + refund_status, + connector, + refund_type, + total, + count, + start_bucket, + end_bucket, + }) + } +} + +impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let status: Option> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let authentication_type: Option> = + row.try_get("authentication_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method: Option = + row.try_get("payment_method").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + + let start_bucket: Option = row + .try_get::, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option = row + .try_get::, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + currency, + status, + connector, + authentication_type, + payment_method, + total, + count, + start_bucket, + end_bucket, + }) + } +} + +impl<'a> FromRow<'a, PgRow> for super::payments::filters::FilterRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let status: Option> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let authentication_type: Option> = + row.try_get("authentication_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let payment_method: Option = + row.try_get("payment_method").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + Ok(Self { + currency, + status, + connector, + authentication_type, + payment_method, + }) + } +} + +impl<'a> FromRow<'a, PgRow> for super::refunds::filters::RefundFilterRow { + fn from_row(row: &'a PgRow) -> sqlx::Result { + let currency: Option> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_status: Option> = + row.try_get("refund_status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let connector: Option = row.try_get("connector").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let refund_type: Option> = + row.try_get("refund_type").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + Ok(Self { + currency, + refund_status, + connector, + refund_type, + }) + } +} + +impl ToSql for PrimitiveDateTime { + fn to_sql(&self) -> error_stack::Result { + Ok(self.to_string()) + } +} + +impl ToSql for AnalyticsCollection { + fn to_sql(&self) -> error_stack::Result { + match self { + Self::Payment => Ok("payment_attempt".to_string()), + Self::Refund => Ok("refund".to_string()), + } + } +} + +impl ToSql for Aggregate +where + T: ToSql, +{ + fn to_sql(&self) -> error_stack::Result { + Ok(match self { + Self::Count { field: _, alias } => { + format!( + "count(*){}", + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Sum { field, alias } => { + format!( + "sum({}){}", + field.to_sql().attach_printable("Failed to sum aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Min { field, alias } => { + format!( + "min({}){}", + field.to_sql().attach_printable("Failed to min aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + Self::Max { field, alias } => { + format!( + "max({}){}", + field.to_sql().attach_printable("Failed to max aggregate")?, + alias.map_or_else(|| "".to_owned(), |alias| format!(" as {}", alias)) + ) + } + }) + } +} diff --git a/crates/router/src/analytics/types.rs b/crates/router/src/analytics/types.rs new file mode 100644 index 000000000000..fe20e812a9b8 --- /dev/null +++ b/crates/router/src/analytics/types.rs @@ -0,0 +1,119 @@ +use std::{fmt::Display, str::FromStr}; + +use common_utils::{ + errors::{CustomResult, ErrorSwitch, ParsingError}, + events::ApiEventMetric, +}; +use error_stack::{report, Report, ResultExt}; + +use super::query::QueryBuildingError; + +#[derive(serde::Deserialize, Debug, masking::Serialize)] +#[serde(rename_all = "snake_case")] +pub enum AnalyticsDomain { + Payments, + Refunds, +} + +impl ApiEventMetric for AnalyticsDomain {} + +#[derive(Debug, strum::AsRefStr, strum::Display, Clone, Copy)] +pub enum AnalyticsCollection { + Payment, + Refund, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)] +#[serde(transparent)] +pub struct DBEnumWrapper(pub T); + +impl AsRef for DBEnumWrapper { + fn as_ref(&self) -> &T { + &self.0 + } +} + +impl FromStr for DBEnumWrapper +where + T: FromStr + Display, +{ + type Err = Report; + + fn from_str(s: &str) -> Result { + T::from_str(s) + .map_err(|_er| report!(ParsingError::EnumParseFailure(std::any::type_name::()))) + .map(DBEnumWrapper) + .attach_printable_lazy(|| format!("raw_value: {s}")) + } +} + +// Analytics Framework + +pub trait RefundAnalytics {} + +#[async_trait::async_trait] +pub trait AnalyticsDataSource +where + Self: Sized + Sync + Send, +{ + type Row; + async fn load_results(&self, query: &str) -> CustomResult, QueryExecutionError> + where + Self: LoadRow; +} + +pub trait LoadRow +where + Self: AnalyticsDataSource, + T: Sized, +{ + fn load_row(row: Self::Row) -> CustomResult; +} + +#[derive(thiserror::Error, Debug)] +pub enum MetricsError { + #[error("Error building query")] + QueryBuildingError, + #[error("Error running Query")] + QueryExecutionFailure, + #[error("Error processing query results")] + PostProcessingFailure, + #[allow(dead_code)] + #[error("Not Implemented")] + NotImplemented, +} + +#[derive(Debug, thiserror::Error)] +pub enum QueryExecutionError { + #[error("Failed to extract domain rows")] + RowExtractionFailure, + #[error("Database error")] + DatabaseError, +} + +pub type MetricsResult = CustomResult; + +impl ErrorSwitch for QueryBuildingError { + fn switch(&self) -> MetricsError { + MetricsError::QueryBuildingError + } +} + +pub type FiltersResult = CustomResult; + +#[derive(thiserror::Error, Debug)] +pub enum FiltersError { + #[error("Error building query")] + QueryBuildingError, + #[error("Error running Query")] + QueryExecutionFailure, + #[allow(dead_code)] + #[error("Not Implemented")] + NotImplemented, +} + +impl ErrorSwitch for QueryBuildingError { + fn switch(&self) -> FiltersError { + FiltersError::QueryBuildingError + } +} diff --git a/crates/router/src/analytics/utils.rs b/crates/router/src/analytics/utils.rs new file mode 100644 index 000000000000..f7e6ea69dc37 --- /dev/null +++ b/crates/router/src/analytics/utils.rs @@ -0,0 +1,22 @@ +use api_models::analytics::{ + payments::{PaymentDimensions, PaymentMetrics}, + refunds::{RefundDimensions, RefundMetrics}, + NameDescription, +}; +use strum::IntoEnumIterator; + +pub fn get_payment_dimensions() -> Vec { + PaymentDimensions::iter().map(Into::into).collect() +} + +pub fn get_refund_dimensions() -> Vec { + RefundDimensions::iter().map(Into::into).collect() +} + +pub fn get_payment_metrics_info() -> Vec { + PaymentMetrics::iter().map(Into::into).collect() +} + +pub fn get_refund_metrics_info() -> Vec { + RefundMetrics::iter().map(Into::into).collect() +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index df87c8a460ac..c5b71c6f7341 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -16,6 +16,8 @@ pub use router_env::config::{Log, LogConsole, LogFile, LogTelemetry}; use scheduler::SchedulerSettings; use serde::{de::Error, Deserialize, Deserializer}; +#[cfg(feature = "olap")] +use crate::analytics::AnalyticsConfig; use crate::{ core::errors::{ApplicationError, ApplicationResult}, env::{self, logger, Env}, @@ -101,6 +103,8 @@ pub struct Settings { pub lock_settings: LockSettings, pub temp_locker_enable_config: TempLockerEnableConfig, pub payment_link: PaymentLink, + #[cfg(feature = "olap")] + pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 38efe8b75134..5cd0b6cbea5f 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -1,6 +1,8 @@ #![forbid(unsafe_code)] #![recursion_limit = "256"] +#[cfg(feature = "olap")] +pub mod analytics; #[cfg(feature = "stripe")] pub mod compatibility; pub mod configs; @@ -141,6 +143,7 @@ pub fn mk_app( .service(routes::ApiKeys::server(state.clone())) .service(routes::Files::server(state.clone())) .service(routes::Disputes::server(state.clone())) + .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) .service(routes::Gsm::server(state.clone())) } diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 47b9f23cf8cb..ac5c14200600 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -42,3 +42,5 @@ pub use self::app::{ }; #[cfg(feature = "stripe")] pub use super::compatibility::stripe::StripeApis; +#[cfg(feature = "olap")] +pub use crate::analytics::routes::{self as analytics, Analytics}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ec87fcdc3900..67662961ed44 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -44,6 +44,8 @@ pub struct AppState { #[cfg(feature = "kms")] pub kms_secrets: Arc, pub api_client: Box, + #[cfg(feature = "olap")] + pub pool: crate::analytics::AnalyticsProvider, } impl scheduler::SchedulerAppState for AppState { @@ -128,6 +130,14 @@ impl AppState { ), }; + #[cfg(feature = "olap")] + let pool = crate::analytics::AnalyticsProvider::from_conf( + &conf.analytics, + #[cfg(feature = "kms")] + kms_client, + ) + .await; + #[cfg(feature = "kms")] #[allow(clippy::expect_used)] let kms_secrets = settings::ActiveKmsSecrets { @@ -149,6 +159,8 @@ impl AppState { kms_secrets: Arc::new(kms_secrets), api_client, event_handler: Box::::default(), + #[cfg(feature = "olap")] + pool, } } diff --git a/crates/router_env/src/lib.rs b/crates/router_env/src/lib.rs index d3612767ff9d..e75606aa1531 100644 --- a/crates/router_env/src/lib.rs +++ b/crates/router_env/src/lib.rs @@ -1,5 +1,5 @@ #![forbid(unsafe_code)] -#![warn(missing_docs, missing_debug_implementations)] +#![warn(missing_debug_implementations)] //! //! Environment of payment router: logger, basic config, its environment awareness. @@ -22,6 +22,7 @@ pub mod vergen; pub use logger::*; pub use once_cell; pub use opentelemetry; +use strum::Display; pub use tracing; #[cfg(feature = "actix_web")] pub use tracing_actix_web; @@ -29,3 +30,19 @@ pub use tracing_appender; #[doc(inline)] pub use self::env::*; +use crate::types::FlowMetric; + +/// Analytics Flow routes Enums +/// Info - Dimensions and filters available for the domain +/// Filters - Set of values present for the dimension +/// Metrics - Analytical data on dimensions and metrics +#[derive(Debug, Display, Clone, PartialEq, Eq)] +pub enum AnalyticsFlow { + GetInfo, + GetPaymentFilters, + GetRefundFilters, + GetRefundsMetrics, + GetPaymentMetrics, +} + +impl FlowMetric for AnalyticsFlow {} diff --git a/crates/router_env/src/metrics.rs b/crates/router_env/src/metrics.rs index e4943699ee5b..14402a7a6e91 100644 --- a/crates/router_env/src/metrics.rs +++ b/crates/router_env/src/metrics.rs @@ -63,3 +63,22 @@ macro_rules! histogram_metric { > = once_cell::sync::Lazy::new(|| $meter.f64_histogram($description).init()); }; } + +/// Create a [`Histogram`][Histogram] u64 metric with the specified name and an optional description, +/// associated with the specified meter. Note that the meter must be to a valid [`Meter`][Meter]. +/// +/// [Histogram]: opentelemetry::metrics::Histogram +/// [Meter]: opentelemetry::metrics::Meter +#[macro_export] +macro_rules! histogram_metric_u64 { + ($name:ident, $meter:ident) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram(stringify!($name)).init()); + }; + ($name:ident, $meter:ident, $description:literal) => { + pub(crate) static $name: once_cell::sync::Lazy< + $crate::opentelemetry::metrics::Histogram, + > = once_cell::sync::Lazy::new(|| $meter.u64_histogram($description).init()); + }; +} diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 352c4ff551bc..f70fc656d8e3 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -237,5 +237,17 @@ bank_debit.ach = { connector_list = "gocardless"} bank_debit.becs = { connector_list = "gocardless"} bank_debit.sepa = { connector_list = "gocardless"} +[analytics] +source = "sqlx" + +[analytics.sqlx] +username = "db_user" +password = "db_pass" +host = "localhost" +port = 5432 +dbname = "hyperswitch_db" +pool_size = 5 +connection_timeout = 10 + [kv_config] ttl = 300 # 5 * 60 seconds From b3d5062dc07676ec12e903b1999fdd9138c0891d Mon Sep 17 00:00:00 2001 From: Sampras Lopes Date: Fri, 10 Nov 2023 17:13:29 +0530 Subject: [PATCH 04/15] refactor(events): update api events to follow snake case naming (#2828) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- crates/common_utils/src/events.rs | 2 +- crates/router/src/events/api_logs.rs | 1 + crates/router/src/services/authentication.rs | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 8c52f6c36d63..753f1deeb676 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -8,7 +8,7 @@ pub trait ApiEventMetric { } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] -#[serde(tag = "flow_type")] +#[serde(tag = "flow_type", rename_all = "snake_case")] pub enum ApiEventsType { Payout, Payment { diff --git a/crates/router/src/events/api_logs.rs b/crates/router/src/events/api_logs.rs index 1a47568e7ad8..873102e81ec2 100644 --- a/crates/router/src/events/api_logs.rs +++ b/crates/router/src/events/api_logs.rs @@ -22,6 +22,7 @@ use crate::{ }; #[derive(Clone, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] pub struct ApiEvent { api_flow: String, created_at_timestamp: i128, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index faa7864aff5b..0a7f5189b904 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -29,7 +29,11 @@ pub struct AuthenticationData { } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] -#[serde(tag = "api_auth_type")] +#[serde( + tag = "api_auth_type", + content = "authentication_data", + rename_all = "snake_case" +)] pub enum AuthenticationType { ApiKey { merchant_id: String, From 0eb81f04b3e8f41483d85bc68dae3088245d16be Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 12 Nov 2023 14:49:16 +0000 Subject: [PATCH 05/15] chore(version): v1.76.0 --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 412b42afc2eb..c5926cfe86ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to HyperSwitch will be documented here. - - - +## 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 From f88eee7362be2cc3e8e8dc2bb7bfd263892ff01e Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 13 Nov 2023 11:17:35 +0530 Subject: [PATCH 06/15] feat(router): Add new JWT authentication variants and use them (#2835) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .typos.toml | 1 + Cargo.lock | 56 + crates/api_models/src/events.rs | 1 + crates/api_models/src/events/user.rs | 14 + crates/api_models/src/lib.rs | 1 + crates/api_models/src/user.rs | 21 + crates/data_models/Cargo.toml | 2 +- crates/router/Cargo.toml | 3 + crates/router/src/consts.rs | 6 + crates/router/src/consts/user.rs | 8 + crates/router/src/core.rs | 2 + crates/router/src/core/errors.rs | 4 + crates/router/src/core/errors/user.rs | 78 + crates/router/src/core/user.rs | 81 + crates/router/src/lib.rs | 1 + crates/router/src/routes.rs | 4 +- crates/router/src/routes/admin.rs | 91 +- crates/router/src/routes/api_keys.rs | 32 +- crates/router/src/routes/app.rs | 16 +- crates/router/src/routes/lock_utils.rs | 3 + crates/router/src/routes/payments.rs | 19 +- crates/router/src/routes/refunds.rs | 8 +- crates/router/src/routes/routing.rs | 44 +- crates/router/src/routes/user.rs | 31 + crates/router/src/services.rs | 2 + crates/router/src/services/authentication.rs | 101 +- crates/router/src/services/jwt.rs | 42 + crates/router/src/types/domain.rs | 4 + crates/router/src/types/domain/user.rs | 483 ++++ crates/router/src/utils.rs | 2 + crates/router/src/utils/user.rs | 1 + .../router/src/utils/user/blocker_emails.txt | 2349 +++++++++++++++++ crates/router/src/utils/user/password.rs | 43 + crates/router_env/src/logger/types.rs | 2 + 34 files changed, 3489 insertions(+), 67 deletions(-) create mode 100644 crates/api_models/src/events/user.rs create mode 100644 crates/api_models/src/user.rs create mode 100644 crates/router/src/consts/user.rs create mode 100644 crates/router/src/core/errors/user.rs create mode 100644 crates/router/src/core/user.rs create mode 100644 crates/router/src/routes/user.rs create mode 100644 crates/router/src/services/jwt.rs create mode 100644 crates/router/src/types/domain/user.rs create mode 100644 crates/router/src/utils/user.rs create mode 100644 crates/router/src/utils/user/blocker_emails.txt create mode 100644 crates/router/src/utils/user/password.rs 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/Cargo.lock b/Cargo.lock index c96ce2c18258..ae7afa85d7d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -436,6 +436,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 +1157,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 +1223,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" @@ -3854,6 +3881,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" @@ -4562,6 +4600,7 @@ dependencies = [ "actix-rt", "actix-web", "api_models", + "argon2", "async-bb8-diesel", "async-trait", "awc", @@ -4637,10 +4676,12 @@ dependencies = [ "time", "tokio", "toml 0.7.4", + "unicode-segmentation", "url", "utoipa", "utoipa-swagger-ui", "uuid", + "validator", "wiremock", "x509-parser", ] @@ -6376,6 +6417,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/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/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/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/data_models/Cargo.toml b/crates/data_models/Cargo.toml index 254c194182f3..c7c872771689 100644 --- a/crates/data_models/Cargo.toml +++ b/crates/data_models/Cargo.toml @@ -27,4 +27,4 @@ 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/router/Cargo.toml b/crates/router/Cargo.toml index 7456944a8e4e..d765a5b5c5ed 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -38,6 +38,7 @@ 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 } @@ -89,10 +90,12 @@ 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"] } +validator = "0.16.0" openssl = "0.10.55" x509-parser = "0.15.0" sha-1 = { version = "0.9"} 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/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/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/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..c34c542d1b6c 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::*}; @@ -710,3 +710,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/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..b87116f47fc5 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, 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/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/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_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, } /// From 8e538dbd5c189047d0a0b24fa752b9a1c67554f5 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Mon, 13 Nov 2023 14:57:34 +0530 Subject: [PATCH 07/15] feat(router): profile specific fallback derivation while routing payments (#2806) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Aprabhat19 Co-authored-by: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> --- crates/api_models/src/events/routing.rs | 16 ++- crates/api_models/src/routing.rs | 13 ++ crates/router/Cargo.toml | 5 +- crates/router/src/core/admin.rs | 16 ++- crates/router/src/core/payments/routing.rs | 70 +++++++-- crates/router/src/core/routing.rs | 159 ++++++++++++++++++--- crates/router/src/routes/app.rs | 10 ++ crates/router/src/routes/routing.rs | 57 ++++++++ 8 files changed, 312 insertions(+), 34 deletions(-) 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/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/router/Cargo.toml b/crates/router/Cargo.toml index d765a5b5c5ed..8f6906e06855 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,13 +9,13 @@ 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"] 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"] kv_store = ["scheduler/kv_store"] @@ -24,6 +24,7 @@ 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"] +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"] 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/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/routes/app.rs b/crates/router/src/routes/app.rs index c34c542d1b6c..7f5c720be607 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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), + ), + ) } } diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index b87116f47fc5..606111a88818 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -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 +} From c124511052ed8911a2ccfcf648c0793b5c1ca690 Mon Sep 17 00:00:00 2001 From: harsh-sharma-juspay <125131007+harsh-sharma-juspay@users.noreply.github.com> Date: Mon, 13 Nov 2023 15:58:29 +0530 Subject: [PATCH 08/15] feat(apievent): added hs latency to api event (#2734) Co-authored-by: Sampras lopes --- crates/router/src/events/api_logs.rs | 3 +++ crates/router/src/services/api.rs | 15 +++++++++++++++ 2 files changed, 18 insertions(+) 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/services/api.rs b/crates/router/src/services/api.rs index bb0e70b4b27b..321bf909ea0c 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -830,6 +830,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 +840,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 +868,7 @@ where status_code, serialized_request, serialized_response, + overhead_latency, auth_type, event_type.unwrap_or(ApiEventsType::Miscellaneous), request, From 05535871152f4a6ac24ce6b5b5390da13cc29b96 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:50:18 +0530 Subject: [PATCH 09/15] build(deps): remove unused dependencies and features (#2854) --- Cargo.lock | 62 ------------------- crates/api_models/Cargo.toml | 3 +- crates/common_enums/Cargo.toml | 4 -- crates/common_utils/Cargo.toml | 4 +- crates/data_models/Cargo.toml | 6 +- crates/diesel_models/Cargo.toml | 6 -- crates/drainer/Cargo.toml | 4 +- crates/euclid/Cargo.toml | 4 +- crates/euclid_wasm/Cargo.toml | 19 ++---- crates/kgraph_utils/Cargo.toml | 4 +- crates/redis_interface/Cargo.toml | 2 +- crates/router/Cargo.toml | 37 +++++------ crates/router/tests/connectors/adyen.rs | 2 + .../router/tests/connectors/bankofamerica.rs | 1 + crates/router/tests/connectors/globepay.rs | 2 +- crates/router/tests/connectors/gocardless.rs | 2 +- crates/router/tests/connectors/helcim.rs | 2 +- crates/router/tests/connectors/main.rs | 4 ++ crates/router/tests/connectors/opayo.rs | 1 + crates/router/tests/connectors/payeezy.rs | 1 + crates/router/tests/connectors/powertranz.rs | 2 +- crates/router/tests/connectors/prophetpay.rs | 1 + crates/router/tests/connectors/utils.rs | 18 +++++- crates/router/tests/connectors/volt.rs | 2 +- crates/router/tests/connectors/wise.rs | 13 +++- crates/scheduler/Cargo.toml | 3 - crates/storage_impl/Cargo.toml | 16 +++-- crates/test_utils/Cargo.toml | 17 ++--- 28 files changed, 86 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ae7afa85d7d5..222bc02212ec 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" @@ -418,7 +383,6 @@ dependencies = [ "serde_json", "serde_with", "strum 0.24.1", - "thiserror", "time", "url", "utoipa", @@ -1561,7 +1525,6 @@ dependencies = [ "serde", "serde_json", "strum 0.25.0", - "time", "utoipa", ] @@ -1924,7 +1887,6 @@ dependencies = [ "masking", "serde", "serde_json", - "strum 0.25.0", "thiserror", "time", ] @@ -2035,13 +1997,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", @@ -3271,12 +3230,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" @@ -4593,7 +4546,6 @@ dependencies = [ name = "router" version = "0.2.0" dependencies = [ - "actix", "actix-cors", "actix-http", "actix-multipart", @@ -4635,7 +4587,6 @@ dependencies = [ "josekit", "jsonwebtoken", "kgraph_utils", - "literally", "masking", "maud", "mimalloc", @@ -4664,18 +4615,14 @@ 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", @@ -4962,7 +4909,6 @@ dependencies = [ "router_env", "serde", "serde_json", - "signal-hook-tokio", "storage_impl", "strum 0.24.1", "thiserror", @@ -5496,7 +5442,6 @@ dependencies = [ "diesel_models", "dyn-clone", "error-stack", - "external_services", "futures", "http", "masking", @@ -5730,27 +5675,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]] diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index d15fdeabf387..ac624c899c6d 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 = [] @@ -30,7 +30,6 @@ 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/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 c7c872771689..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"] } 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/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/router/Cargo.toml b/crates/router/Cargo.toml index 8f6906e06855..5e8cb7a72979 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -17,24 +17,22 @@ basilisk = ["kms"] stripe = ["dep:serde_qs"] 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 = [] [dependencies] -actix = "0.13.0" actix-cors = "0.6.4" actix-multipart = "0.6.0" actix-rt = "2.8.0" @@ -52,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" @@ -63,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" @@ -84,44 +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"] } validator = "0.16.0" -openssl = "0.10.55" 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 } @@ -131,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/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/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/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" } From fc92ec770a98875fdc9737611ae20dff2ae13a83 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 13 Nov 2023 14:32:02 +0000 Subject: [PATCH 10/15] chore(version): v1.77.0 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5926cfe86ab..61cb4839fd7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to HyperSwitch will be documented here. - - - +## 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 From d2968c94978a57422fa46a8195d906736a95b864 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:19:37 +0530 Subject: [PATCH 11/15] feat(router): add automatic retries and step up 3ds flow (#2834) --- crates/router/Cargo.toml | 4 +- crates/router/src/core/payments.rs | 31 +- crates/router/src/core/payments/retry.rs | 579 +++++++++++++++++++++++ crates/router/src/routes/metrics.rs | 8 + 4 files changed, 619 insertions(+), 3 deletions(-) create mode 100644 crates/router/src/core/payments/retry.rs diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 5e8cb7a72979..4d9c315a10b0 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,7 +9,7 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing"] +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"] @@ -30,7 +30,7 @@ connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connect external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] payouts = [] - +retry = [] [dependencies] actix-cors = "0.6.4" diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index a114b20380bf..5c8089271bd9 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; @@ -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; diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs new file mode 100644 index 000000000000..f58e9ea298f7 --- /dev/null +++ b/crates/router/src/core/payments/retry.rs @@ -0,0 +1,579 @@ +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 + }, + 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/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; From 856c7af77e17599ca0d4d119744ac582e9c3c971 Mon Sep 17 00:00:00 2001 From: Kashif <46213975+kashif-m@users.noreply.github.com> Date: Tue, 14 Nov 2023 15:22:50 +0530 Subject: [PATCH 12/15] feat: Payment link status page UI (#2740) Co-authored-by: Kashif Co-authored-by: Sahkal Poddar Co-authored-by: Sahkal Poddar --- .../src/core/payment_link/payment_link.html | 685 ++++++++++-------- 1 file changed, 364 insertions(+), 321 deletions(-) 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"); } From cafea45982d7b520fe68fde967984ce88f68c6c0 Mon Sep 17 00:00:00 2001 From: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Date: Tue, 14 Nov 2023 16:11:38 +0530 Subject: [PATCH 13/15] fix: handle session and confirm flow discrepancy in surcharge details (#2696) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- Cargo.lock | 5 +- crates/api_models/Cargo.toml | 1 - crates/api_models/src/payment_methods.rs | 83 +++++++++++- crates/api_models/src/payments.rs | 48 +++++++ .../src/payments/payment_attempt.rs | 8 +- crates/diesel_models/src/payment_attempt.rs | 24 +++- crates/redis_interface/src/commands.rs | 5 +- .../router/src/core/payment_methods/cards.rs | 2 + crates/router/src/core/payments.rs | 39 ++++-- .../payments/operations/payment_confirm.rs | 128 ++++++++++++++---- .../payments/operations/payment_create.rs | 28 +++- .../payments/operations/payment_response.rs | 2 + .../payments/operations/payment_update.rs | 16 ++- crates/router/src/core/payments/retry.rs | 2 + crates/router/src/core/utils.rs | 74 +++++++++- crates/router/src/services/api.rs | 6 +- crates/router/src/types.rs | 22 ++- crates/router/src/types/api.rs | 25 ++++ .../src/payments/payment_attempt.rs | 32 +++-- crates/storage_impl/src/redis/kv_store.rs | 4 +- 20 files changed, 477 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 222bc02212ec..1574933810b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,7 +381,6 @@ dependencies = [ "router_derive", "serde", "serde_json", - "serde_with", "strum 0.24.1", "time", "url", @@ -1659,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" diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ac624c899c6d..ce882e913282 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -25,7 +25,6 @@ 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"] } 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/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/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/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/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 5c8089271bd9..e7408cecf163 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -14,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}; @@ -290,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, @@ -298,7 +300,7 @@ where &operation, payment_data, &customer, - None, + session_surcharge_data, ) .await? } @@ -353,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, @@ -920,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, @@ -957,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 index f58e9ea298f7..376b9048c856 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -412,6 +412,8 @@ where } else { None }, + surcharge_amount: None, + tax_amount: None, updated_by: storage_scheme.to_string(), authentication_data, encoded_data, 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/services/api.rs b/crates/router/src/services/api.rs index 321bf909ea0c..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()) } } 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/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) From 496245d990e123b626089e70c848856ace295fb5 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 14:32:13 +0000 Subject: [PATCH 14/15] chore(version): v1.78.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61cb4839fd7b..e5da650def02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ 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 From d634fdeac349b92e3619234580299a6c6c38e6d4 Mon Sep 17 00:00:00 2001 From: Arun Raj M Date: Thu, 16 Nov 2023 10:27:34 +0530 Subject: [PATCH 15/15] feat: change async-bb8 fork and tokio spawn for concurrent database calls (#2774) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: akshay-97 Co-authored-by: akshay.s Co-authored-by: Kartikeya Hegde --- Cargo.lock | 820 ++++++++++++++---- crates/common_utils/src/types.rs | 4 +- crates/diesel_models/Cargo.toml | 2 +- crates/drainer/Cargo.toml | 2 +- crates/router/Cargo.toml | 3 +- crates/router/src/bin/router.rs | 4 +- crates/router/src/bin/scheduler.rs | 7 +- .../router/src/core/payment_methods/cards.rs | 13 +- crates/router/src/core/payments.rs | 33 +- .../src/core/payments/flows/approve_flow.rs | 7 +- .../src/core/payments/flows/authorize_flow.rs | 15 +- .../src/core/payments/flows/cancel_flow.rs | 7 +- .../src/core/payments/flows/capture_flow.rs | 7 +- .../payments/flows/complete_authorize_flow.rs | 4 +- .../src/core/payments/flows/psync_flow.rs | 7 +- .../src/core/payments/flows/reject_flow.rs | 7 +- .../src/core/payments/flows/session_flow.rs | 7 +- .../core/payments/flows/setup_mandate_flow.rs | 12 +- crates/router/src/core/payments/operations.rs | 4 +- .../payments/operations/payment_approve.rs | 3 +- .../payments/operations/payment_cancel.rs | 27 +- .../payments/operations/payment_capture.rs | 4 +- .../operations/payment_complete_authorize.rs | 2 +- .../payments/operations/payment_confirm.rs | 393 ++++++--- .../payments/operations/payment_create.rs | 10 +- .../operations/payment_method_validate.rs | 5 +- .../payments/operations/payment_reject.rs | 9 +- .../payments/operations/payment_response.rs | 172 ++-- .../payments/operations/payment_session.rs | 5 +- .../core/payments/operations/payment_start.rs | 2 +- .../payments/operations/payment_status.rs | 4 +- .../payments/operations/payment_update.rs | 11 +- crates/router/src/core/webhooks.rs | 79 +- crates/router/src/db/address.rs | 4 +- crates/router/src/db/refund.rs | 20 +- crates/router/src/db/reverse_lookup.rs | 6 +- crates/router/src/lib.rs | 2 +- crates/router/src/routes/admin.rs | 16 +- crates/router/src/routes/api_keys.rs | 4 +- crates/router/src/routes/app.rs | 103 ++- crates/router/src/routes/customers.rs | 12 +- crates/router/src/routes/disputes.rs | 16 +- crates/router/src/routes/files.rs | 12 +- crates/router/src/routes/payment_link.rs | 4 +- crates/router/src/routes/payment_methods.rs | 28 +- crates/router/src/routes/payments.rs | 36 +- crates/router/src/routes/payouts.rs | 20 +- crates/router/src/routes/refunds.rs | 12 +- crates/router/src/routes/verification.rs | 4 +- crates/router/src/routes/webhooks.rs | 4 +- crates/router/src/types/domain/customer.rs | 2 +- crates/router/src/utils.rs | 17 +- crates/router/src/workflows/payment_sync.rs | 18 +- crates/router/src/workflows/refund_router.rs | 2 +- crates/router/tests/cache.rs | 12 +- crates/router/tests/connectors/utils.rs | 18 +- crates/router/tests/customers.rs | 4 +- crates/router/tests/integration_demo.rs | 6 +- crates/router/tests/payments.rs | 76 +- crates/router/tests/payments2.rs | 8 +- crates/router/tests/payouts.rs | 2 +- crates/router/tests/refunds.rs | 6 +- crates/router/tests/services.rs | 18 +- crates/router/tests/utils.rs | 4 +- crates/storage_impl/Cargo.toml | 2 +- crates/storage_impl/src/lib.rs | 2 +- crates/storage_impl/src/lookup.rs | 6 +- .../src/payments/payment_attempt.rs | 26 +- .../src/payments/payment_intent.rs | 6 +- 69 files changed, 1512 insertions(+), 717 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1574933810b3..a03340093c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,12 +9,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617a8268e3537fe1d8c9ead925fca49ef6400927ee7bc26750e90ecee14ce4b8" dependencies = [ "bitflags 1.3.2", - "bytes", + "bytes 1.5.0", "futures-core", "futures-sink", "memchr", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -31,7 +31,7 @@ dependencies = [ "futures-util", "log", "once_cell", - "smallvec", + "smallvec 1.11.1", ] [[package]] @@ -48,7 +48,7 @@ dependencies = [ "base64 0.21.4", "bitflags 1.3.2", "brotli", - "bytes", + "bytes 1.5.0", "bytestring", "derive_more", "encoding_rs", @@ -66,8 +66,8 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "sha1", - "smallvec", - "tokio", + "smallvec 1.11.1", + "tokio 1.32.0", "tokio-util", "tracing", "zstd", @@ -92,7 +92,7 @@ dependencies = [ "actix-multipart-derive", "actix-utils", "actix-web", - "bytes", + "bytes 1.5.0", "derive_more", "futures-core", "futures-util", @@ -105,7 +105,7 @@ dependencies = [ "serde_json", "serde_plain", "tempfile", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -142,7 +142,7 @@ checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" dependencies = [ "actix-macros", "futures-core", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -156,9 +156,9 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 0.8.8", "socket2 0.5.4", - "tokio", + "tokio 1.32.0", "tracing", ] @@ -188,7 +188,7 @@ dependencies = [ "pin-project-lite", "rustls 0.21.7", "rustls-webpki", - "tokio", + "tokio 1.32.0", "tokio-rustls", "tokio-util", "tracing", @@ -221,9 +221,9 @@ dependencies = [ "actix-utils", "actix-web-codegen", "ahash 0.7.6", - "bytes", + "bytes 1.5.0", "bytestring", - "cfg-if", + "cfg-if 1.0.0", "cookie", "derive_more", "encoding_rs", @@ -240,7 +240,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "smallvec", + "smallvec 1.11.1", "socket2 0.4.9", "time", "url", @@ -296,7 +296,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "getrandom 0.2.10", "once_cell", "version_check", @@ -475,14 +475,14 @@ dependencies = [ [[package]] name = "async-bb8-diesel" version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779f1fa3defe66bf147fe5c811b23a02cfcaa528a25293e0b20d1911eac1fb05" +source = "git+https://github.com/jarnura/async-bb8-diesel?rev=53b4ab901aab7635c8215fd1c2d542c8db443094#53b4ab901aab7635c8215fd1c2d542c8db443094" dependencies = [ "async-trait", "bb8", "diesel", "thiserror", - "tokio", + "tokio 1.32.0", + "tracing", ] [[package]] @@ -506,7 +506,7 @@ dependencies = [ "futures-core", "memchr", "pin-project-lite", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -517,7 +517,7 @@ checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" dependencies = [ "async-lock", "autocfg", - "cfg-if", + "cfg-if 1.0.0", "concurrent-queue", "futures-lite", "log", @@ -606,8 +606,8 @@ dependencies = [ "actix-utils", "ahash 0.7.6", "base64 0.21.4", - "bytes", - "cfg-if", + "bytes 1.5.0", + "cfg-if 1.0.0", "cookie", "derive_more", "futures-core", @@ -624,7 +624,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -644,14 +644,14 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "fastrand 1.9.0", "hex", "http", "hyper", "ring", "time", - "tokio", + "tokio 1.32.0", "tower", "tracing", "zeroize", @@ -666,7 +666,7 @@ dependencies = [ "aws-smithy-async", "aws-smithy-types", "fastrand 1.9.0", - "tokio", + "tokio 1.32.0", "tracing", "zeroize", ] @@ -695,7 +695,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "lazy_static", @@ -721,7 +721,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -750,7 +750,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "once_cell", @@ -779,7 +779,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -804,7 +804,7 @@ dependencies = [ "aws-smithy-json", "aws-smithy-types", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tokio-stream", @@ -831,7 +831,7 @@ dependencies = [ "aws-smithy-types", "aws-smithy-xml", "aws-types", - "bytes", + "bytes 1.5.0", "http", "regex", "tower", @@ -861,7 +861,7 @@ checksum = "9d2ce6f507be68e968a33485ced670111d1cbad161ddbbab1e313c03d37d8f4c" dependencies = [ "aws-smithy-eventstream", "aws-smithy-http", - "bytes", + "bytes 1.5.0", "form_urlencoded", "hex", "hmac", @@ -882,7 +882,7 @@ checksum = "13bda3996044c202d75b91afeb11a9afae9db9a721c6a7a427410018e286b880" dependencies = [ "futures-util", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-stream", ] @@ -894,7 +894,7 @@ checksum = "07ed8b96d95402f3f6b8b57eb4e0e45ee365f78b1a924faf20ff6e97abf1eae6" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "crc32c", "crc32fast", "hex", @@ -917,7 +917,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-http-tower", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "fastrand 1.9.0", "http", "http-body", @@ -926,7 +926,7 @@ dependencies = [ "lazy_static", "pin-project-lite", "rustls 0.20.9", - "tokio", + "tokio 1.32.0", "tower", "tracing", ] @@ -938,7 +938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460c8da5110835e3d9a717c61f5556b20d03c32a1dec57f8fc559b360f733bb8" dependencies = [ "aws-smithy-types", - "bytes", + "bytes 1.5.0", "crc32fast", ] @@ -950,7 +950,7 @@ checksum = "2b3b693869133551f135e1f2c77cb0b8277d9e3e17feaf2213f735857c4f0d28" dependencies = [ "aws-smithy-eventstream", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "bytes-utils", "futures-core", "http", @@ -960,7 +960,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "pin-utils", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -973,7 +973,7 @@ checksum = "3ae4f6c5798a247fac98a867698197d9ac22643596dc3777f0c76b91917616b9" dependencies = [ "aws-smithy-http", "aws-smithy-types", - "bytes", + "bytes 1.5.0", "http", "http-body", "pin-project-lite", @@ -1034,7 +1034,7 @@ dependencies = [ "aws-smithy-http", "aws-smithy-types", "http", - "rustc_version", + "rustc_version 0.4.0", "tracing", ] @@ -1047,7 +1047,7 @@ dependencies = [ "async-trait", "axum-core", "bitflags 1.3.2", - "bytes", + "bytes 1.5.0", "futures-util", "http", "http-body", @@ -1073,7 +1073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" dependencies = [ "async-trait", - "bytes", + "bytes 1.5.0", "futures-util", "http", "http-body", @@ -1091,7 +1091,7 @@ checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide 0.7.1", "object", @@ -1136,7 +1136,7 @@ dependencies = [ "futures-channel", "futures-util", "parking_lot 0.12.1", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1204,7 +1204,7 @@ dependencies = [ "arrayref", "arrayvec", "cc", - "cfg-if", + "cfg-if 1.0.0", "constant_time_eq", "digest 0.10.7", ] @@ -1282,6 +1282,16 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + [[package]] name = "bytes" version = "1.5.0" @@ -1294,7 +1304,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9" dependencies = [ - "bytes", + "bytes 1.5.0", "either", ] @@ -1304,7 +1314,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "238e4886760d98c4f899360c834fa93e62cf7f721ac3c2da375cbdf4b8679aae" dependencies = [ - "bytes", + "bytes 1.5.0", ] [[package]] @@ -1347,7 +1357,7 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.19", "serde", "serde_json", ] @@ -1360,7 +1370,7 @@ checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" dependencies = [ "camino", "cargo-platform", - "semver", + "semver 1.0.19", "serde", "serde_json", "thiserror", @@ -1393,6 +1403,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -1509,6 +1525,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1532,12 +1557,12 @@ name = "common_utils" version = "0.1.0" dependencies = [ "async-trait", - "bytes", + "bytes 1.5.0", "common_enums", "diesel", "error-stack", "fake", - "futures", + "futures 0.3.28", "hex", "http", "masking", @@ -1562,7 +1587,7 @@ dependencies = [ "test-case", "thiserror", "time", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1571,7 +1596,7 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f057a694a54f12365049b0958a1685bb52d567f5593b355fbf685838e873d400" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.8.16", ] [[package]] @@ -1674,7 +1699,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8f48d60e5b4d2c53d5c2b1d8a58c849a70ae5e5509b08a48d047e3b65714a74" dependencies = [ - "rustc_version", + "rustc_version 0.4.0", ] [[package]] @@ -1683,7 +1708,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1728,8 +1753,19 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch 0.8.2", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] @@ -1738,9 +1774,24 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-epoch 0.9.15", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset 0.5.6", + "scopeguard", ] [[package]] @@ -1750,20 +1801,42 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", - "cfg-if", - "crossbeam-utils", - "memoffset", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", + "memoffset 0.9.0", "scopeguard", ] +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ - "cfg-if", - "crossbeam-utils", + "cfg-if 1.0.0", + "crossbeam-utils 0.8.16", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", ] [[package]] @@ -1772,7 +1845,7 @@ version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1861,9 +1934,9 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "hashbrown 0.14.1", - "lock_api", + "lock_api 0.4.10", "once_cell", "parking_lot_core 0.9.8", ] @@ -1900,7 +1973,7 @@ dependencies = [ "deadpool-runtime", "num_cpus", "retain_mut", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -1953,7 +2026,7 @@ dependencies = [ "convert_case", "proc-macro2", "quote", - "rustc_version", + "rustc_version 0.4.0", "syn 1.0.109", ] @@ -2064,7 +2137,7 @@ checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", "redox_users", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -2111,7 +2184,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -2132,7 +2205,7 @@ version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -2187,7 +2260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f00447f331c7f726db5b8532ebc9163519eed03c6d7c8b73c90b3ff5646ac85" dependencies = [ "anyhow", - "rustc_version", + "rustc_version 0.4.0", "serde", ] @@ -2261,7 +2334,7 @@ dependencies = [ "router_env", "serde", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -2291,7 +2364,7 @@ dependencies = [ "serde", "serde_json", "time", - "tokio", + "tokio 1.32.0", "url", "webdriver", ] @@ -2375,19 +2448,19 @@ dependencies = [ "arc-swap", "arcstr", "async-trait", - "bytes", + "bytes 1.5.0", "bytes-utils", - "cfg-if", + "cfg-if 1.0.0", "float-cmp", - "futures", + "futures 0.3.28", "lazy_static", "log", "parking_lot 0.12.1", "rand 0.8.5", "redis-protocol", - "semver", + "semver 1.0.19", "sha-1 0.10.1", - "tokio", + "tokio 1.32.0", "tokio-stream", "tokio-util", "tracing", @@ -2447,6 +2520,28 @@ dependencies = [ "syn 2.0.38", ] +[[package]] +name = "fuchsia-zircon" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +dependencies = [ + "bitflags 1.3.2", + "fuchsia-zircon-sys", +] + +[[package]] +name = "fuchsia-zircon-sys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.28" @@ -2496,7 +2591,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" dependencies = [ "futures-core", - "lock_api", + "lock_api 0.4.10", "parking_lot 0.11.2", ] @@ -2594,7 +2689,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -2605,7 +2700,7 @@ version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -2677,7 +2772,7 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "futures-core", "futures-sink", @@ -2685,7 +2780,7 @@ dependencies = [ "http", "indexmap 1.9.3", "slab", - "tokio", + "tokio 1.32.0", "tokio-util", "tracing", ] @@ -2769,7 +2864,7 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ - "bytes", + "bytes 1.5.0", "fnv", "itoa", ] @@ -2780,7 +2875,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes", + "bytes 1.5.0", "http", "pin-project-lite", ] @@ -2833,7 +2928,7 @@ version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-channel", "futures-core", "futures-util", @@ -2845,7 +2940,7 @@ dependencies = [ "itoa", "pin-project-lite", "socket2 0.4.9", - "tokio", + "tokio 1.32.0", "tower-service", "tracing", "want", @@ -2862,7 +2957,7 @@ dependencies = [ "log", "rustls 0.20.9", "rustls-native-certs", - "tokio", + "tokio 1.32.0", "tokio-rustls", ] @@ -2874,7 +2969,7 @@ checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tokio-io-timeout", ] @@ -2884,10 +2979,10 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ - "bytes", + "bytes 1.5.0", "hyper", "native-tls", - "tokio", + "tokio 1.32.0", "tokio-native-tls", ] @@ -3015,7 +3110,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -3029,6 +3124,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.8.0" @@ -3140,6 +3244,16 @@ dependencies = [ "simple_asn1", ] +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + [[package]] name = "kgraph_utils" version = "0.1.0" @@ -3246,6 +3360,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" +[[package]] +name = "lock_api" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4da24a77a3d8a6d4862d95f72e6fdb9c09a643ecdb402d754004a557f2bec75" +dependencies = [ + "scopeguard", +] + [[package]] name = "lock_api" version = "0.4.10" @@ -3293,7 +3416,7 @@ dependencies = [ name = "masking" version = "0.1.0" dependencies = [ - "bytes", + "bytes 1.5.0", "diesel", "serde", "serde_json", @@ -3340,13 +3463,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "md-5" version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "digest 0.10.7", ] @@ -3362,6 +3491,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + [[package]] name = "memoffset" version = "0.9.0" @@ -3430,6 +3568,25 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" +dependencies = [ + "cfg-if 0.1.10", + "fuchsia-zircon", + "fuchsia-zircon-sys", + "iovec", + "kernel32-sys", + "libc", + "log", + "miow", + "net2", + "slab", + "winapi 0.2.8", +] + [[package]] name = "mio" version = "0.8.8" @@ -3442,6 +3599,29 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mio-uds" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afcb699eb26d4332647cc848492bbc15eafb26f08d0304550d5aa1f612e066f0" +dependencies = [ + "iovec", + "libc", + "mio 0.6.23", +] + +[[package]] +name = "miow" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +dependencies = [ + "kernel32-sys", + "net2", + "winapi 0.2.8", + "ws2_32-sys", +] + [[package]] name = "moka" version = "0.11.3" @@ -3451,16 +3631,16 @@ dependencies = [ "async-io", "async-lock", "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-epoch 0.9.15", + "crossbeam-utils 0.8.16", "futures-util", "once_cell", "parking_lot 0.12.1", "quanta", - "rustc_version", + "rustc_version 0.4.0", "scheduled-thread-pool", "skeptic", - "smallvec", + "smallvec 1.11.1", "tagptr", "thiserror", "triomphe", @@ -3494,6 +3674,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "net2" +version = "0.2.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "winapi 0.3.9", +] + [[package]] name = "nom" version = "7.1.3" @@ -3511,7 +3702,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" dependencies = [ "overload", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -3626,7 +3817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ "bitflags 2.4.0", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -3680,14 +3871,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8af72d59a4484654ea8eb183fea5ae4eb6a41d7ac3e3bae5f4d2a282a3a7d3ca" dependencies = [ "async-trait", - "futures", + "futures 0.3.28", "futures-util", "http", "opentelemetry", "opentelemetry-proto", "prost", "thiserror", - "tokio", + "tokio 1.32.0", "tonic", ] @@ -3697,7 +3888,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "045f8eea8c0fa19f7d48e7bc3128a39c2e5c533d5c61298c548dfefc1064474c" dependencies = [ - "futures", + "futures 0.3.28", "futures-util", "opentelemetry", "prost", @@ -3738,7 +3929,7 @@ dependencies = [ "percent-encoding", "rand 0.8.5", "thiserror", - "tokio", + "tokio 1.32.0", "tokio-stream", ] @@ -3770,6 +3961,17 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e52c774a4c39359c1d1c52e43f73dd91a75a614652c825408eec30c95a9b2067" +[[package]] +name = "parking_lot" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252" +dependencies = [ + "lock_api 0.3.4", + "parking_lot_core 0.6.3", + "rustc_version 0.2.3", +] + [[package]] name = "parking_lot" version = "0.11.2" @@ -3777,7 +3979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", - "lock_api", + "lock_api 0.4.10", "parking_lot_core 0.8.6", ] @@ -3787,22 +3989,37 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ - "lock_api", + "lock_api 0.4.10", "parking_lot_core 0.9.8", ] +[[package]] +name = "parking_lot_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66b810a62be75176a80873726630147a5ca780cd33921e0b5709033e66b0a" +dependencies = [ + "cfg-if 0.1.10", + "cloudabi", + "libc", + "redox_syscall 0.1.57", + "rustc_version 0.2.3", + "smallvec 0.6.14", + "winapi 0.3.9", +] + [[package]] name = "parking_lot_core" version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall 0.2.16", - "smallvec", - "winapi", + "smallvec 1.11.1", + "winapi 0.3.9", ] [[package]] @@ -3811,10 +4028,10 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall 0.3.5", - "smallvec", + "smallvec 1.11.1", "windows-targets", ] @@ -4061,7 +4278,7 @@ checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", - "cfg-if", + "cfg-if 1.0.0", "concurrent-queue", "libc", "log", @@ -4143,7 +4360,7 @@ version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" dependencies = [ - "bytes", + "bytes 1.5.0", "prost-derive", ] @@ -4187,14 +4404,14 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.8.16", "libc", "mach2", "once_cell", "raw-cpuid", "wasi 0.11.0+wasi-snapshot-preview1", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4338,8 +4555,8 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" dependencies = [ - "crossbeam-deque", - "crossbeam-utils", + "crossbeam-deque 0.8.3", + "crossbeam-utils 0.8.16", ] [[package]] @@ -4348,7 +4565,7 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c31deddf734dc0a39d3112e73490e88b61a05e83e074d211f348404cee4d2c6" dependencies = [ - "bytes", + "bytes 1.5.0", "bytes-utils", "cookie-factory", "crc16", @@ -4363,13 +4580,19 @@ dependencies = [ "common_utils", "error-stack", "fred", - "futures", + "futures 0.3.28", "router_env", "serde", "thiserror", - "tokio", + "tokio 1.32.0", ] +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -4463,7 +4686,7 @@ checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" dependencies = [ "async-compression", "base64 0.21.4", - "bytes", + "bytes 1.5.0", "encoding_rs", "futures-core", "futures-util", @@ -4485,7 +4708,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "system-configuration", - "tokio", + "tokio 1.32.0", "tokio-native-tls", "tokio-util", "tower-service", @@ -4514,7 +4737,7 @@ dependencies = [ "spin", "untrusted", "web-sys", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -4561,7 +4784,7 @@ dependencies = [ "bb8", "bigdecimal", "blake3", - "bytes", + "bytes 1.5.0", "cards", "clap", "common_enums", @@ -4577,7 +4800,7 @@ dependencies = [ "error-stack", "euclid", "external_services", - "futures", + "futures 0.3.28", "hex", "http", "hyper", @@ -4621,7 +4844,8 @@ dependencies = [ "test_utils", "thiserror", "time", - "tokio", + "tokio 1.32.0", + "tracing-futures", "unicode-segmentation", "url", "utoipa", @@ -4664,7 +4888,7 @@ dependencies = [ "serde_path_to_error", "strum 0.24.1", "time", - "tokio", + "tokio 1.32.0", "tracing", "tracing-actix-web", "tracing-appender", @@ -4724,7 +4948,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "ordered-multimap", ] @@ -4740,13 +4964,22 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + [[package]] name = "rustc_version" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" dependencies = [ - "semver", + "semver 1.0.19", ] [[package]] @@ -4900,7 +5133,7 @@ dependencies = [ "diesel_models", "error-stack", "external_services", - "futures", + "futures 0.3.28", "masking", "once_cell", "rand 0.8.5", @@ -4912,7 +5145,7 @@ dependencies = [ "strum 0.24.1", "thiserror", "time", - "tokio", + "tokio 1.32.0", "uuid", ] @@ -4961,6 +5194,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + [[package]] name = "semver" version = "1.0.19" @@ -4970,6 +5212,12 @@ dependencies = [ "serde", ] +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + [[package]] name = "serde" version = "1.0.188" @@ -5122,7 +5370,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" dependencies = [ "dashmap", - "futures", + "futures 0.3.28", "lazy_static", "log", "parking_lot 0.12.1", @@ -5147,7 +5395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.9.0", "opaque-debug", @@ -5159,7 +5407,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5170,7 +5418,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5181,7 +5429,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest 0.10.7", ] @@ -5232,7 +5480,7 @@ dependencies = [ "futures-core", "libc", "signal-hook", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5286,6 +5534,15 @@ dependencies = [ "deunicode", ] +[[package]] +name = "smallvec" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" +dependencies = [ + "maybe-uninit", +] + [[package]] name = "smallvec" version = "1.11.1" @@ -5299,7 +5556,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", - "winapi", + "winapi 0.3.9", ] [[package]] @@ -5351,9 +5608,9 @@ dependencies = [ "bigdecimal", "bitflags 1.3.2", "byteorder", - "bytes", + "bytes 1.5.0", "crc", - "crossbeam-queue", + "crossbeam-queue 0.3.8", "dirs", "dotenvy", "either", @@ -5381,7 +5638,7 @@ dependencies = [ "serde_json", "sha1", "sha2", - "smallvec", + "smallvec 1.11.1", "sqlformat", "sqlx-rt", "stringprep", @@ -5419,7 +5676,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" dependencies = [ "native-tls", "once_cell", - "tokio", + "tokio 1.32.0", "tokio-native-tls", ] @@ -5432,7 +5689,7 @@ dependencies = [ "async-bb8-diesel", "async-trait", "bb8", - "bytes", + "bytes 1.5.0", "common_utils", "config", "crc32fast", @@ -5441,7 +5698,7 @@ dependencies = [ "diesel_models", "dyn-clone", "error-stack", - "futures", + "futures 0.3.28", "http", "masking", "mime", @@ -5454,7 +5711,7 @@ dependencies = [ "serde", "serde_json", "thiserror", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5606,7 +5863,7 @@ version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand 2.0.1", "redox_syscall 0.3.5", "rustix 0.38.17", @@ -5650,7 +5907,7 @@ version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54c25e2cb8f5fcd7318157634e8838aa6f7e4715c96637f969fabaccd1ef5462" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "proc-macro-error", "proc-macro2", "quote", @@ -5686,7 +5943,7 @@ dependencies = [ "serial_test", "thirtyfour", "time", - "tokio", + "tokio 1.32.0", "toml 0.7.4", ] @@ -5701,7 +5958,7 @@ dependencies = [ "chrono", "cookie", "fantoccini", - "futures", + "futures 0.3.28", "http", "log", "parking_lot 0.12.1", @@ -5711,7 +5968,7 @@ dependencies = [ "stringmatch", "thirtyfour-macros", "thiserror", - "tokio", + "tokio 1.32.0", "url", "urlparse", ] @@ -5754,7 +6011,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "once_cell", ] @@ -5821,6 +6078,30 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a09c0b5bb588872ab2f09afa13ee6e9dac11e10a0ec9e8e3ba39a5a5d530af6" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "mio 0.6.23", + "num_cpus", + "tokio-codec", + "tokio-current-thread", + "tokio-executor", + "tokio-fs", + "tokio-io", + "tokio-reactor", + "tokio-sync", + "tokio-tcp", + "tokio-threadpool", + "tokio-timer", + "tokio-udp", + "tokio-uds", +] + [[package]] name = "tokio" version = "1.32.0" @@ -5828,9 +6109,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ "backtrace", - "bytes", + "bytes 1.5.0", "libc", - "mio", + "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", "pin-project-lite", @@ -5840,6 +6121,59 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "tokio-codec" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25b2998660ba0e70d18684de5d06b70b70a3a747469af9dea7618cc59e75976b" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "tokio-io", +] + +[[package]] +name = "tokio-current-thread" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1de0e32a83f131e002238d7ccde18211c0a5397f60cbfffcb112868c2e0e20e" +dependencies = [ + "futures 0.1.31", + "tokio-executor", +] + +[[package]] +name = "tokio-executor" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb2d1b8f4548dbf5e1f7818512e9c406860678f29c300cdf0ebac72d1a3a1671" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", +] + +[[package]] +name = "tokio-fs" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297a1206e0ca6302a0eed35b700d292b275256f596e2f3fea7729d5e629b6ff4" +dependencies = [ + "futures 0.1.31", + "tokio-io", + "tokio-threadpool", +] + +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", +] + [[package]] name = "tokio-io-timeout" version = "1.2.0" @@ -5847,7 +6181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ "pin-project-lite", - "tokio", + "tokio 1.32.0", ] [[package]] @@ -5868,7 +6202,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "tokio-reactor" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09bc590ec4ba8ba87652da2068d150dcada2cfa2e07faae270a5e0409aa51351" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "mio 0.6.23", + "num_cpus", + "parking_lot 0.9.0", + "slab", + "tokio-executor", + "tokio-io", + "tokio-sync", ] [[package]] @@ -5878,7 +6231,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ "rustls 0.20.9", - "tokio", + "tokio 1.32.0", "webpki", ] @@ -5890,7 +6243,93 @@ checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "tokio-sync" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edfe50152bc8164fcc456dab7891fa9bf8beaf01c5ee7e1dd43a397c3cf87dee" +dependencies = [ + "fnv", + "futures 0.1.31", +] + +[[package]] +name = "tokio-tcp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98df18ed66e3b72e742f185882a9e201892407957e45fbff8da17ae7a7c51f72" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "mio 0.6.23", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-threadpool" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df720b6581784c118f0eb4310796b12b1d242a7eb95f716a8367855325c25f89" +dependencies = [ + "crossbeam-deque 0.7.4", + "crossbeam-queue 0.2.3", + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "lazy_static", + "log", + "num_cpus", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-timer" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93044f2d313c95ff1cb7809ce9a7a05735b012288a888b62d4434fd58c94f296" +dependencies = [ + "crossbeam-utils 0.7.2", + "futures 0.1.31", + "slab", + "tokio-executor", +] + +[[package]] +name = "tokio-udp" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2a0b10e610b39c38b031a2fcab08e4b82f16ece36504988dcbd81dbba650d82" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", + "mio 0.6.23", + "tokio-codec", + "tokio-io", + "tokio-reactor", +] + +[[package]] +name = "tokio-uds" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab57a4ac4111c8c9dbcf70779f6fc8bc35ae4b2454809febac840ad19bd7e4e0" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "iovec", + "libc", + "log", + "mio 0.6.23", + "mio-uds", + "tokio-codec", + "tokio-io", + "tokio-reactor", ] [[package]] @@ -5899,11 +6338,11 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ - "bytes", + "bytes 1.5.0", "futures-core", "futures-sink", "pin-project-lite", - "tokio", + "tokio 1.32.0", "tracing", ] @@ -5960,7 +6399,7 @@ dependencies = [ "async-trait", "axum", "base64 0.13.1", - "bytes", + "bytes 1.5.0", "futures-core", "futures-util", "h2", @@ -5972,7 +6411,7 @@ dependencies = [ "pin-project", "prost", "prost-derive", - "tokio", + "tokio 1.32.0", "tokio-stream", "tokio-util", "tower", @@ -5995,7 +6434,7 @@ dependencies = [ "pin-project-lite", "rand 0.8.5", "slab", - "tokio", + "tokio 1.32.0", "tokio-util", "tower-layer", "tower-service", @@ -6020,7 +6459,7 @@ version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "log", "pin-project-lite", "tracing-attributes", @@ -6080,6 +6519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" dependencies = [ "pin-project", + "tokio 0.1.22", "tracing", ] @@ -6131,7 +6571,7 @@ dependencies = [ "serde", "serde_json", "sharded-slab", - "smallvec", + "smallvec 1.11.1", "thread_local", "tracing", "tracing-core", @@ -6389,7 +6829,7 @@ checksum = "8b3c89c2c7e50f33e4d35527e5bf9c11d6d132226dbbd1753f0fbe9f19ef88c6" dependencies = [ "anyhow", "git2", - "rustc_version", + "rustc_version 0.4.0", "rustversion", "time", ] @@ -6458,7 +6898,7 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -6483,7 +6923,7 @@ version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -6535,7 +6975,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f" dependencies = [ "base64 0.13.1", - "bytes", + "bytes 1.5.0", "cookie", "http", "log", @@ -6582,6 +7022,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + [[package]] name = "winapi" version = "0.3.9" @@ -6592,6 +7038,12 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -6604,7 +7056,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ - "winapi", + "winapi 0.3.9", ] [[package]] @@ -6703,7 +7155,7 @@ version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "windows-sys", ] @@ -6717,7 +7169,7 @@ dependencies = [ "async-trait", "base64 0.21.4", "deadpool", - "futures", + "futures 0.3.28", "futures-timer", "http-types", "hyper", @@ -6726,7 +7178,17 @@ dependencies = [ "regex", "serde", "serde_json", - "tokio", + "tokio 1.32.0", +] + +[[package]] +name = "ws2_32-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +dependencies = [ + "winapi 0.2.8", + "winapi-build", ] [[package]] @@ -6775,7 +7237,7 @@ checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ "byteorder", "crc32fast", - "crossbeam-utils", + "crossbeam-utils 0.8.16", "flate2", ] diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index b28bffe0dc90..111f0f43c0f2 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -77,7 +77,9 @@ impl Percentage { if value.contains('.') { // if string has '.' then take the decimal part and verify precision length match value.split('.').last() { - Some(decimal_part) => decimal_part.trim_end_matches('0').len() <= PRECISION.into(), + Some(decimal_part) => { + decimal_part.trim_end_matches('0').len() <= >::into(PRECISION) + } // will never be None None => false, } diff --git a/crates/diesel_models/Cargo.toml b/crates/diesel_models/Cargo.toml index 9521c690366f..ccef0bf4e742 100644 --- a/crates/diesel_models/Cargo.toml +++ b/crates/diesel_models/Cargo.toml @@ -12,7 +12,7 @@ default = ["kv_store"] kv_store = [] [dependencies] -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } diesel = { version = "2.1.0", features = ["postgres", "serde_json", "time", "64-column-tables"] } error-stack = "0.3.1" frunk = "0.4.1" diff --git a/crates/drainer/Cargo.toml b/crates/drainer/Cargo.toml index 56bebdce6b86..668e8b0574fe 100644 --- a/crates/drainer/Cargo.toml +++ b/crates/drainer/Cargo.toml @@ -13,7 +13,7 @@ kms = ["external_services/kms"] vergen = ["router_env/vergen"] [dependencies] -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } bb8 = "0.8" clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] } config = { version = "0.13.3", features = ["toml"] } diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 4d9c315a10b0..01595dc18cd5 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -37,8 +37,8 @@ actix-cors = "0.6.4" actix-multipart = "0.6.0" actix-rt = "2.8.0" actix-web = "4.3.1" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } 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 } aws-sdk-s3 = { version = "0.28.0", optional = true } @@ -97,6 +97,7 @@ utoipa-swagger-ui = { version = "3.1.3", features = ["actix-web"] } uuid = { version = "1.3.3", features = ["serde", "v4"] } validator = "0.16.0" x509-parser = "0.15.0" +tracing-futures = { version = "0.2.5", features = ["tokio"] } # First party crates api_models = { version = "0.1.0", path = "../api_models", features = ["errors"] } diff --git a/crates/router/src/bin/router.rs b/crates/router/src/bin/router.rs index cb3a8d83b031..beb2869f998c 100644 --- a/crates/router/src/bin/router.rs +++ b/crates/router/src/bin/router.rs @@ -4,7 +4,7 @@ use router::{ logger, }; -#[actix_web::main] +#[tokio::main] async fn main() -> ApplicationResult<()> { // get commandline config before initializing config let cmd_line = ::parse(); @@ -43,7 +43,7 @@ async fn main() -> ApplicationResult<()> { logger::info!("Application started [{:?}] [{:?}]", conf.server, conf.log); #[allow(clippy::expect_used)] - let server = router::start_server(conf) + let server = Box::pin(router::start_server(conf)) .await .expect("Failed to create the server"); let _ = server.await; diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 09f23bc3b2f3..4c19408582bc 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -40,7 +40,12 @@ async fn main() -> CustomResult<(), ProcessTrackerError> { ); // channel for listening to redis disconnect events let (redis_shutdown_signal_tx, redis_shutdown_signal_rx) = oneshot::channel(); - let state = routes::AppState::new(conf, redis_shutdown_signal_tx, api_client).await; + let state = Box::pin(routes::AppState::new( + conf, + redis_shutdown_signal_tx, + api_client, + )) + .await; // channel to shutdown scheduler gracefully let (tx, rx) = mpsc::channel(1); tokio::spawn(router::receiver_for_error( diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 6b3cf11f5891..38ab03ddcb77 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1944,7 +1944,14 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( ) -> errors::RouterResponse { let db = state.store.as_ref(); if let Some(customer_id) = customer_id { - list_customer_payment_method(&state, merchant_account, key_store, None, customer_id).await + Box::pin(list_customer_payment_method( + &state, + merchant_account, + key_store, + None, + customer_id, + )) + .await } else { let cloned_secret = req.and_then(|r| r.client_secret.as_ref().cloned()); let payment_intent = helpers::verify_payment_intent_time_and_client_secret( @@ -1957,13 +1964,13 @@ pub async fn do_list_customer_pm_fetch_customer_if_not_passed( .as_ref() .and_then(|intent| intent.customer_id.to_owned()) .ok_or(errors::ApiErrorResponse::CustomerNotFound)?; - list_customer_payment_method( + Box::pin(list_customer_payment_method( &state, merchant_account, key_store, payment_intent, &customer_id, - ) + )) .await } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e7408cecf163..7e19b0b60571 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -193,7 +193,7 @@ where ) .await?; let operation = Box::new(PaymentResponse); - let db = &*state.store; + connector_http_status_code = router_data.connector_http_status_code; external_latency = router_data.external_latency; //add connector http status code metrics @@ -201,7 +201,7 @@ where operation .to_post_update_tracker()? .update_tracker( - db, + state, &validate_result.payment_id, payment_data, router_data, @@ -272,7 +272,6 @@ where } let operation = Box::new(PaymentResponse); - let db = &*state.store; connector_http_status_code = router_data.connector_http_status_code; external_latency = router_data.external_latency; //add connector http status code metrics @@ -280,7 +279,7 @@ where operation .to_post_update_tracker()? .update_tracker( - db, + state, &validate_result.payment_id, payment_data, router_data, @@ -323,7 +322,7 @@ where (_, payment_data) = operation .to_update_tracker()? .update_trackers( - &*state.store, + state, payment_data.clone(), customer.clone(), validate_result.storage_scheme, @@ -582,7 +581,14 @@ impl PaymentRedirectFlow for PaymentRedirectCom }), ..Default::default() }; - payments_core::( + Box::pin(payments_core::< + api::CompleteAuthorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account, merchant_key_store, @@ -592,7 +598,7 @@ impl PaymentRedirectFlow for PaymentRedirectCom connector_action, None, HeaderPayload::default(), - ) + )) .await } @@ -678,7 +684,14 @@ impl PaymentRedirectFlow for PaymentRedirectSyn expand_attempts: None, expand_captures: None, }; - payments_core::( + Box::pin(payments_core::< + api::PSync, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account, merchant_key_store, @@ -688,7 +701,7 @@ impl PaymentRedirectFlow for PaymentRedirectSyn connector_action, None, HeaderPayload::default(), - ) + )) .await } fn generate_response( @@ -889,7 +902,7 @@ where (_, *payment_data) = operation .to_update_tracker()? .update_trackers( - &*state.store, + state, payment_data.clone(), customer.clone(), merchant_account.storage_scheme, diff --git a/crates/router/src/core/payments/flows/approve_flow.rs b/crates/router/src/core/payments/flows/approve_flow.rs index 24f7e05e7b9d..14b710de914a 100644 --- a/crates/router/src/core/payments/flows/approve_flow.rs +++ b/crates/router/src/core/payments/flows/approve_flow.rs @@ -25,7 +25,10 @@ impl customer: &Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Approve, + types::PaymentsApproveData, + >( state, self.clone(), connector_id, @@ -33,7 +36,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index e27fe54c0ed0..04bd7f0b4338 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -39,7 +39,10 @@ impl types::PaymentsResponseData, >, > { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Authorize, + types::PaymentsAuthorizeData, + >( state, self.clone(), connector_id, @@ -47,7 +50,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } @@ -96,7 +99,7 @@ impl Feature for types::PaymentsAu metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics if resp.request.setup_mandate_details.clone().is_some() { - let payment_method_id = tokenization::save_payment_method( + let payment_method_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -104,7 +107,7 @@ impl Feature for types::PaymentsAu merchant_account, self.request.payment_method_type, key_store, - ) + )) .await?; Ok(mandate::mandate_procedure( state, @@ -127,7 +130,7 @@ impl Feature for types::PaymentsAu tokio::spawn(async move { logger::info!("Starting async call to save_payment_method in locker"); - let result = tokenization::save_payment_method( + let result = Box::pin(tokenization::save_payment_method( &state, &connector, response, @@ -135,7 +138,7 @@ impl Feature for types::PaymentsAu &merchant_account, self.request.payment_method_type, &key_store, - ) + )) .await; if let Err(err) = result { diff --git a/crates/router/src/core/payments/flows/cancel_flow.rs b/crates/router/src/core/payments/flows/cancel_flow.rs index 3a3ac1b5b0bb..5918380ee0b2 100644 --- a/crates/router/src/core/payments/flows/cancel_flow.rs +++ b/crates/router/src/core/payments/flows/cancel_flow.rs @@ -24,7 +24,10 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Void, + types::PaymentsCancelData, + >( state, self.clone(), connector_id, @@ -32,7 +35,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Capture, + types::PaymentsCaptureData, + >( state, self.clone(), connector_id, @@ -33,7 +36,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index 6fbbb01e1a64..44d8728fd4d2 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -35,7 +35,7 @@ impl types::PaymentsResponseData, >, > { - transformers::construct_payment_router_data::< + Box::pin(transformers::construct_payment_router_data::< api::CompleteAuthorize, types::CompleteAuthorizeData, >( @@ -46,7 +46,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/psync_flow.rs b/crates/router/src/core/payments/flows/psync_flow.rs index 36d418a3ae8c..cb7a764985d1 100644 --- a/crates/router/src/core/payments/flows/psync_flow.rs +++ b/crates/router/src/core/payments/flows/psync_flow.rs @@ -28,7 +28,10 @@ impl ConstructFlowSpecificData RouterResult< types::RouterData, > { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::PSync, + types::PaymentsSyncData, + >( state, self.clone(), connector_id, @@ -36,7 +39,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Reject, + types::PaymentsRejectData, + >( state, self.clone(), connector_id, @@ -32,7 +35,7 @@ impl ConstructFlowSpecificData, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::( + Box::pin(transformers::construct_payment_router_data::< + api::Session, + types::PaymentsSessionData, + >( state, self.clone(), connector_id, @@ -40,7 +43,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index dae9ed0bf833..0c03c8ce123b 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -31,7 +31,7 @@ impl customer: &Option, merchant_connector_account: &helpers::MerchantConnectorAccountType, ) -> RouterResult { - transformers::construct_payment_router_data::< + Box::pin(transformers::construct_payment_router_data::< api::SetupMandate, types::SetupMandateRequestData, >( @@ -42,7 +42,7 @@ impl key_store, customer, merchant_connector_account, - ) + )) .await } } @@ -75,7 +75,7 @@ impl Feature for types::Setup .await .to_setup_mandate_failed_response()?; - let pm_id = tokenization::save_payment_method( + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -83,7 +83,7 @@ impl Feature for types::Setup merchant_account, self.request.payment_method_type, key_store, - ) + )) .await?; mandate::mandate_procedure( @@ -208,7 +208,7 @@ impl types::SetupMandateRouterData { .to_setup_mandate_failed_response()?; let payment_method_type = self.request.payment_method_type; - let pm_id = tokenization::save_payment_method( + let pm_id = Box::pin(tokenization::save_payment_method( state, connector, resp.to_owned(), @@ -216,7 +216,7 @@ impl types::SetupMandateRouterData { merchant_account, payment_method_type, key_store, - ) + )) .await?; Ok(mandate::mandate_procedure( diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index ad747ac2792a..f65e65459e00 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -154,7 +154,7 @@ pub trait Domain: Send + Sync { pub trait UpdateTracker: Send { async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_data: D, customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -171,7 +171,7 @@ pub trait UpdateTracker: Send { pub trait PostUpdateTracker: Send { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: D, response: types::RouterData, diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index d5d0d2d01765..538e65e4b22e 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -336,7 +336,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -356,6 +356,7 @@ impl updated_by: storage_scheme.to_string(), }; payment_data.payment_intent = db + .store .update_payment_intent( payment_data.payment_intent, intent_status_update, diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index f734afef7826..535edf736ca6 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -14,7 +14,6 @@ use crate::{ payment_methods::PaymentMethodRetrieve, payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -178,7 +177,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -207,6 +206,7 @@ impl if let Some(payment_intent_update) = intent_status_update { payment_data.payment_intent = db + .store .update_payment_intent( payment_data.payment_intent, payment_intent_update, @@ -216,17 +216,18 @@ impl .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; } - db.update_payment_attempt_with_attempt_id( - payment_data.payment_attempt.clone(), - storage::PaymentAttemptUpdate::VoidUpdate { - status: attempt_status_update, - cancellation_reason, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + db.store + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + storage::PaymentAttemptUpdate::VoidUpdate { + status: attempt_status_update, + cancellation_reason, + updated_by: storage_scheme.to_string(), + }, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 6e794b1ba618..ff51a2c49d77 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -13,7 +13,6 @@ use crate::{ payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, types::MultipleCaptureData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -222,7 +221,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, mut payment_data: payments::PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -239,6 +238,7 @@ impl { payment_data.payment_attempt = match &payment_data.multiple_capture_data { Some(multiple_capture_data) => db + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::MultipleCaptureCountUpdate { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 038d34ea290f..c648d95a4950 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -326,7 +326,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: storage_enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 96cd4f5c622f..88462e7f8563 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -11,6 +11,7 @@ use futures::FutureExt; use redis_interface::errors::RedisError; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; +use tracing_futures::Instrument; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ @@ -65,20 +66,46 @@ impl // Stage 1 - let payment_intent_fut = db - .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) - .map(|x| x.change_context(errors::ApiErrorResponse::PaymentNotFound)); + let store = state.clone().store; + let m_merchant_id = merchant_id.clone(); + let payment_intent_fut = tokio::spawn( + async move { + store + .find_payment_intent_by_payment_id_merchant_id( + &payment_id, + m_merchant_id.as_str(), + storage_scheme, + ) + .map(|x| x.change_context(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); - let mandate_details_fut = helpers::get_token_pm_type_mandate_details( - state, - request, - mandate_type.clone(), - merchant_account, - key_store, + let m_state = state.clone(); + let m_mandate_type = mandate_type.clone(); + let m_merchant_account = merchant_account.clone(); + let m_request = request.clone(); + let m_key_store = key_store.clone(); + + let mandate_details_fut = tokio::spawn( + async move { + helpers::get_token_pm_type_mandate_details( + &m_state, + &m_request, + m_mandate_type, + &m_merchant_account, + &m_key_store, + ) + .await + } + .in_current_span(), ); - let (mut payment_intent, mandate_details) = - futures::try_join!(payment_intent_fut, mandate_details_fut)?; + let (mut payment_intent, mandate_details) = tokio::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(mandate_details_fut) + )?; helpers::validate_customer_access(&payment_intent, auth_flow, request)?; @@ -112,76 +139,122 @@ impl // Stage 2 let attempt_id = payment_intent.active_attempt.get_id(); - let payment_attempt_fut = db - .find_payment_attempt_by_payment_id_merchant_id_attempt_id( - payment_intent.payment_id.as_str(), - merchant_id, - attempt_id.as_str(), - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - - let shipping_address_fut = helpers::create_or_find_address_for_payment_by_request( - db, - request.shipping.as_ref(), - payment_intent.shipping_address_id.as_deref(), - merchant_id, - payment_intent - .customer_id - .as_ref() - .or(customer_details.customer_id.as_ref()), - key_store, - &payment_intent.payment_id, - merchant_account.storage_scheme, + let store = state.clone().store; + let m_payment_id = payment_intent.payment_id.clone(); + let m_merchant_id = merchant_id.clone(); + + let payment_attempt_fut = tokio::spawn( + async move { + store + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + m_payment_id.as_str(), + m_merchant_id.as_str(), + attempt_id.as_str(), + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), ); - let billing_address_fut = helpers::create_or_find_address_for_payment_by_request( - db, - request.billing.as_ref(), - payment_intent.billing_address_id.as_deref(), - merchant_id, - payment_intent - .customer_id - .as_ref() - .or(customer_details.customer_id.as_ref()), - key_store, - &payment_intent.payment_id, - merchant_account.storage_scheme, + let m_merchant_id = merchant_id.clone(); + let m_request_shipping = request.shipping.clone(); + let m_payment_intent_shipping_address_id = payment_intent.shipping_address_id.clone(); + let m_payment_intent_payment_id = payment_intent.payment_id.clone(); + let m_customer_details_customer_id = customer_details.customer_id.clone(); + let m_payment_intent_customer_id = payment_intent.customer_id.clone(); + let store = state.clone().store; + let m_key_store = key_store.clone(); + + let shipping_address_fut = tokio::spawn( + async move { + helpers::create_or_find_address_for_payment_by_request( + store.as_ref(), + m_request_shipping.as_ref(), + m_payment_intent_shipping_address_id.as_deref(), + m_merchant_id.as_str(), + m_payment_intent_customer_id + .as_ref() + .or(m_customer_details_customer_id.as_ref()), + &m_key_store, + m_payment_intent_payment_id.as_ref(), + storage_scheme, + ) + .await + } + .in_current_span(), ); - let config_update_fut = request - .merchant_connector_details - .to_owned() - .async_map(|mcd| async { - helpers::insert_merchant_connector_creds_to_config( - db, - merchant_account.merchant_id.as_str(), - mcd, + let m_merchant_id = merchant_id.clone(); + let m_request_billing = request.billing.clone(); + let m_customer_details_customer_id = customer_details.customer_id.clone(); + let m_payment_intent_customer_id = payment_intent.customer_id.clone(); + let m_payment_intent_billing_address_id = payment_intent.billing_address_id.clone(); + let m_payment_intent_payment_id = payment_intent.payment_id.clone(); + let store = state.clone().store; + let m_key_store = key_store.clone(); + + let billing_address_fut = tokio::spawn( + async move { + helpers::create_or_find_address_for_payment_by_request( + store.as_ref(), + m_request_billing.as_ref(), + m_payment_intent_billing_address_id.as_deref(), + m_merchant_id.as_ref(), + m_payment_intent_customer_id + .as_ref() + .or(m_customer_details_customer_id.as_ref()), + &m_key_store, + m_payment_intent_payment_id.as_ref(), + storage_scheme, ) .await - }) - .map(|x| x.transpose()); + } + .in_current_span(), + ); + + let m_merchant_id = merchant_id.clone(); + let store = state.clone().store; + let m_request_merchant_connector_details = request.merchant_connector_details.clone(); + + let config_update_fut = tokio::spawn( + async move { + m_request_merchant_connector_details + .async_map(|mcd| async { + helpers::insert_merchant_connector_creds_to_config( + store.as_ref(), + m_merchant_id.as_str(), + mcd, + ) + .await + }) + .map(|x| x.transpose()) + .await + } + .in_current_span(), + ); let (mut payment_attempt, shipping_address, billing_address) = match payment_intent.status { api_models::enums::IntentStatus::RequiresCustomerAction | api_models::enums::IntentStatus::RequiresMerchantAction | api_models::enums::IntentStatus::RequiresPaymentMethod | api_models::enums::IntentStatus::RequiresConfirmation => { - let (payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut + let (payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(config_update_fut) )?; (payment_attempt, shipping_address, billing_address) } _ => { - let (mut payment_attempt, shipping_address, billing_address, _) = futures::try_join!( - payment_attempt_fut, - shipping_address_fut, - billing_address_fut, - config_update_fut + let (mut payment_attempt, shipping_address, billing_address, _) = tokio::try_join!( + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(shipping_address_fut), + utils::flatten_join_error(billing_address_fut), + utils::flatten_join_error(config_update_fut) )?; let attempt_type = helpers::get_attempt_type( @@ -193,11 +266,10 @@ impl (payment_intent, payment_attempt) = attempt_type .modify_payment_intent_and_payment_attempt( - // 3 request, payment_intent, payment_attempt, - db, + &*state.store, storage_scheme, ) .await?; @@ -445,7 +517,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -501,7 +573,7 @@ impl .payment_method_data .as_ref() .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, db).await + helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) .await .as_ref() @@ -537,76 +609,131 @@ impl .as_ref() .map(|surcharge_details| surcharge_details.final_amount) .unwrap_or(payment_data.payment_attempt.amount); - let payment_attempt_fut = db - .update_payment_attempt_with_attempt_id( - payment_data.payment_attempt, - storage::PaymentAttemptUpdate::ConfirmUpdate { - amount: payment_data.amount.into(), - currency: payment_data.currency, - status: attempt_status, - payment_method, - authentication_type, - browser_info, - connector, - payment_token, - payment_method_data: additional_pm_data, - payment_method_type, - payment_experience, - business_sub_label, - straight_through_algorithm, - error_code, - error_message, - amount_capturable: Some(authorized_amount), - updated_by: storage_scheme.to_string(), - merchant_connector_id, - }, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - - let payment_intent_fut = db - .update_payment_intent( - payment_data.payment_intent, - storage::PaymentIntentUpdate::Update { - amount: payment_data.amount.into(), - currency: payment_data.currency, - setup_future_usage, - status: intent_status, - customer_id, - shipping_address_id: shipping_address, - billing_address_id: billing_address, - return_url, - business_country, - business_label, - description, - statement_descriptor_name, - statement_descriptor_suffix, - order_details, - metadata, - payment_confirm_source: header_payload.payment_confirm_source, - updated_by: storage_scheme.to_string(), - }, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); - let customer_fut = Box::pin(async { - if let Some((updated_customer, customer)) = updated_customer.zip(customer) { - db.update_customer_by_customer_id_merchant_id( - customer.customer_id.to_owned(), - customer.merchant_id.to_owned(), - updated_customer, - key_store, + let m_payment_data_payment_attempt = payment_data.payment_attempt.clone(); + let m_browser_info = browser_info.clone(); + let m_connector = connector.clone(); + let m_payment_token = payment_token.clone(); + let m_additional_pm_data = additional_pm_data.clone(); + let m_business_sub_label = business_sub_label.clone(); + let m_straight_through_algorithm = straight_through_algorithm.clone(); + let m_error_code = error_code.clone(); + let m_error_message = error_message.clone(); + let m_db = state.clone().store; + + let payment_attempt_fut = tokio::spawn( + async move { + m_db.update_payment_attempt_with_attempt_id( + m_payment_data_payment_attempt, + storage::PaymentAttemptUpdate::ConfirmUpdate { + amount: payment_data.amount.into(), + currency: payment_data.currency, + status: attempt_status, + payment_method, + authentication_type, + browser_info: m_browser_info, + connector: m_connector, + payment_token: m_payment_token, + payment_method_data: m_additional_pm_data, + payment_method_type, + payment_experience, + business_sub_label: m_business_sub_label, + straight_through_algorithm: m_straight_through_algorithm, + error_code: m_error_code, + error_message: m_error_message, + amount_capturable: Some(authorized_amount), + updated_by: storage_scheme.to_string(), + merchant_connector_id, + }, + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); + + let m_payment_data_payment_intent = payment_data.payment_intent.clone(); + let m_customer_id = customer_id.clone(); + let m_shipping_address_id = shipping_address.clone(); + let m_billing_address_id = billing_address.clone(); + let m_return_url = return_url.clone(); + let m_business_label = business_label.clone(); + let m_description = description.clone(); + let m_statement_descriptor_name = statement_descriptor_name.clone(); + let m_statement_descriptor_suffix = statement_descriptor_suffix.clone(); + let m_order_details = order_details.clone(); + let m_metadata = metadata.clone(); + let m_db = state.clone().store; + let m_storage_scheme = storage_scheme.to_string(); + + let payment_intent_fut = tokio::spawn( + async move { + m_db.update_payment_intent( + m_payment_data_payment_intent, + storage::PaymentIntentUpdate::Update { + amount: payment_data.amount.into(), + currency: payment_data.currency, + setup_future_usage, + status: intent_status, + customer_id: m_customer_id, + shipping_address_id: m_shipping_address_id, + billing_address_id: m_billing_address_id, + return_url: m_return_url, + business_country, + business_label: m_business_label, + description: m_description, + statement_descriptor_name: m_statement_descriptor_name, + statement_descriptor_suffix: m_statement_descriptor_suffix, + order_details: m_order_details, + metadata: m_metadata, + payment_confirm_source: header_payload.payment_confirm_source, + updated_by: m_storage_scheme, + }, + storage_scheme, ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) .await - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to update CustomerConnector in customer")?; + } + .in_current_span(), + ); + + let customer_fut = + if let Some((updated_customer, customer)) = updated_customer.zip(customer) { + let m_customer_customer_id = customer.customer_id.to_owned(); + let m_customer_merchant_id = customer.merchant_id.to_owned(); + let m_key_store = key_store.clone(); + let m_updated_customer = updated_customer.clone(); + let m_db = state.clone().store; + tokio::spawn( + async move { + m_db.update_customer_by_customer_id_merchant_id( + m_customer_customer_id, + m_customer_merchant_id, + m_updated_customer, + &m_key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update CustomerConnector in customer")?; + + Ok::<_, error_stack::Report>(()) + } + .in_current_span(), + ) + } else { + tokio::spawn( + async move { Ok::<_, error_stack::Report>(()) } + .in_current_span(), + ) }; - Ok::<_, error_stack::Report>(()) - }); - let (payment_intent, payment_attempt, _) = - futures::try_join!(payment_intent_fut, payment_attempt_fut, customer_fut)?; + let (payment_intent, payment_attempt, _) = tokio::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(payment_attempt_fut), + utils::flatten_join_error(customer_fut) + )?; + payment_data.payment_intent = payment_intent; payment_data.payment_attempt = payment_attempt; diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index fad7212c61d3..974f5e6ab5b6 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -16,7 +16,7 @@ use crate::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, - utils::{self as core_utils}, + utils as core_utils, }, db::StorageInterface, routes::AppState, @@ -394,7 +394,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -443,7 +443,8 @@ impl .as_ref() .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::UpdateTrackers { @@ -466,7 +467,8 @@ impl let customer_id = payment_data.payment_intent.customer_id.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::ReturnUrlUpdate { diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 7e4fe0951b03..62f12cfbc90c 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -205,7 +205,7 @@ impl UpdateTracker, api: #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -225,7 +225,8 @@ impl UpdateTracker, api: let customer_id = payment_data.payment_intent.customer_id.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::ReturnUrlUpdate { diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index a6c2561aaeed..16d264c001ec 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -13,7 +13,6 @@ use crate::{ payment_methods::PaymentMethodRetrieve, payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, }, - db::StorageInterface, routes::AppState, services, types::{ @@ -164,7 +163,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: enums::MerchantStorageScheme, @@ -201,7 +200,8 @@ impl updated_by: storage_scheme.to_string(), }; - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, intent_status_update, @@ -210,7 +210,8 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt.clone(), attempt_status_update, diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index d6346a512ef1..b55b0c46f6ad 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use async_trait::async_trait; +use data_models::payments::payment_attempt::PaymentAttempt; use error_stack::ResultExt; use futures::FutureExt; use router_derive; use router_env::{instrument, tracing}; +use storage_impl::DataModelExt; +use tracing_futures::Instrument; use super::{Operation, PostUpdateTracker}; use crate::{ @@ -15,8 +18,7 @@ use crate::{ payments::{types::MultipleCaptureData, PaymentData}, utils as core_utils, }, - db::StorageInterface, - routes::metrics, + routes::{metrics, AppState}, services::RedirectForm, types::{ self, api, @@ -43,7 +45,7 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData< @@ -60,13 +62,13 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData .mandate_id .or_else(|| router_data.request.mandate_id.clone()); - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -77,7 +79,7 @@ impl PostUpdateTracker, types::PaymentsAuthorizeData impl PostUpdateTracker, types::PaymentsSyncData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: PaymentData, router_data: types::RouterData, @@ -86,8 +88,14 @@ impl PostUpdateTracker, types::PaymentsSyncData> for where F: 'b + Send, { - payment_response_update_tracker(db, payment_id, payment_data, router_data, storage_scheme) - .await + Box::pin(payment_response_update_tracker( + db, + payment_id, + payment_data, + router_data, + storage_scheme, + )) + .await } } @@ -97,7 +105,7 @@ impl PostUpdateTracker, types::PaymentsSessionData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -106,13 +114,13 @@ impl PostUpdateTracker, types::PaymentsSessionData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -125,7 +133,7 @@ impl PostUpdateTracker, types::PaymentsCaptureData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -134,13 +142,13 @@ impl PostUpdateTracker, types::PaymentsCaptureData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -151,7 +159,7 @@ impl PostUpdateTracker, types::PaymentsCaptureData> impl PostUpdateTracker, types::PaymentsCancelData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -161,13 +169,13 @@ impl PostUpdateTracker, types::PaymentsCancelData> f where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -180,7 +188,7 @@ impl PostUpdateTracker, types::PaymentsApproveData> { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -190,13 +198,13 @@ impl PostUpdateTracker, types::PaymentsApproveData> where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -207,7 +215,7 @@ impl PostUpdateTracker, types::PaymentsApproveData> impl PostUpdateTracker, types::PaymentsRejectData> for PaymentResponse { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -217,13 +225,13 @@ impl PostUpdateTracker, types::PaymentsRejectData> f where F: 'b + Send, { - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -236,7 +244,7 @@ impl PostUpdateTracker, types::SetupMandateRequestDa { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData< @@ -255,13 +263,13 @@ impl PostUpdateTracker, types::SetupMandateRequestDa // .map(api_models::payments::MandateIds::new) }); - payment_data = payment_response_update_tracker( + payment_data = Box::pin(payment_response_update_tracker( db, payment_id, payment_data, router_data, storage_scheme, - ) + )) .await?; Ok(payment_data) @@ -274,7 +282,7 @@ impl PostUpdateTracker, types::CompleteAuthorizeData { async fn update_tracker<'b>( &'b self, - db: &dyn StorageInterface, + db: &'b AppState, payment_id: &api::PaymentIdType, payment_data: PaymentData, response: types::RouterData, @@ -283,14 +291,20 @@ impl PostUpdateTracker, types::CompleteAuthorizeData where F: 'b + Send, { - payment_response_update_tracker(db, payment_id, payment_data, response, storage_scheme) - .await + Box::pin(payment_response_update_tracker( + db, + payment_id, + payment_data, + response, + storage_scheme, + )) + .await } } #[instrument(skip_all)] async fn payment_response_update_tracker( - db: &dyn StorageInterface, + state: &AppState, _payment_id: &api::PaymentIdType, mut payment_data: PaymentData, router_data: types::RouterData, @@ -524,7 +538,8 @@ async fn payment_response_update_tracker( payment_data.multiple_capture_data = match capture_update { Some((mut multiple_capture_data, capture_updates)) => { for (capture, capture_update) in capture_updates { - let updated_capture = db + let updated_capture = state + .store .update_capture_with_capture_id(capture, capture_update, storage_scheme) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; @@ -548,17 +563,43 @@ async fn payment_response_update_tracker( let payment_attempt = payment_data.payment_attempt.clone(); - payment_data.payment_attempt = match payment_attempt_update { - Some(payment_attempt_update) => db - .update_payment_attempt_with_attempt_id( - payment_attempt, - payment_attempt_update, - storage_scheme, + let m_db = state.clone().store; + let m_payment_attempt_update = payment_attempt_update.clone(); + let m_payment_attempt = payment_attempt.clone(); + + let payment_attempt = payment_attempt_update + .map(|payment_attempt_update| { + PaymentAttempt::from_storage_model( + payment_attempt_update + .to_storage_model() + .apply_changeset(payment_attempt.clone().to_storage_model()), ) + }) + .unwrap_or_else(|| payment_attempt); + + let payment_attempt_fut = tokio::spawn( + async move { + Box::pin(async move { + Ok::<_, error_stack::Report>( + match m_payment_attempt_update { + Some(payment_attempt_update) => m_db + .update_payment_attempt_with_attempt_id( + m_payment_attempt, + payment_attempt_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, + None => m_payment_attempt, + }, + ) + }) .await - .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?, - None => payment_attempt, - }; + } + .in_current_span(), + ); + + payment_data.payment_attempt = payment_attempt; let amount_captured = get_total_amount_captured( router_data.request, @@ -566,6 +607,7 @@ async fn payment_response_update_tracker( router_data.status, &payment_data, ); + let payment_intent_update = match &router_data.response { Err(_) => storage::PaymentIntentUpdate::PGStatusUpdate { status: payment_data @@ -583,25 +625,47 @@ async fn payment_response_update_tracker( }, }; - let payment_intent_fut = db - .update_payment_intent( - payment_data.payment_intent.clone(), - payment_intent_update, - storage_scheme, - ) - .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)); + let m_db = state.clone().store; + let m_payment_data_payment_intent = payment_data.payment_intent.clone(); + let m_payment_intent_update = payment_intent_update.clone(); + let payment_intent_fut = tokio::spawn( + async move { + m_db.update_payment_intent( + m_payment_data_payment_intent, + m_payment_intent_update, + storage_scheme, + ) + .map(|x| x.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)) + .await + } + .in_current_span(), + ); // When connector requires redirection for mandate creation it can update the connector mandate_id during Psync - let mandate_update_fut = mandate::update_connector_mandate_id( - db, - router_data.merchant_id, - payment_data.mandate_id.clone(), - router_data.response.clone(), + let m_db = state.clone().store; + let m_router_data_merchant_id = router_data.merchant_id.clone(); + let m_payment_data_mandate_id = payment_data.mandate_id.clone(); + let m_router_data_response = router_data.response.clone(); + let mandate_update_fut = tokio::spawn( + async move { + mandate::update_connector_mandate_id( + m_db.as_ref(), + m_router_data_merchant_id, + m_payment_data_mandate_id, + m_router_data_response, + ) + .await + } + .in_current_span(), ); - let (payment_intent, _) = futures::try_join!(payment_intent_fut, mandate_update_fut)?; - payment_data.payment_intent = payment_intent; + let (payment_intent, _, _) = futures::try_join!( + utils::flatten_join_error(payment_intent_fut), + utils::flatten_join_error(mandate_update_fut), + utils::flatten_join_error(payment_attempt_fut) + )?; + payment_data.payment_intent = payment_intent; Ok(payment_data) } diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 52677ab3cc8d..3abde60c2e9b 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -200,7 +200,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, _customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -217,7 +217,8 @@ impl { let metadata = payment_data.payment_intent.metadata.clone(); payment_data.payment_intent = match metadata { - Some(metadata) => db + Some(metadata) => state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 5578f6b3dc15..17f39d5150bb 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -174,7 +174,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: storage_enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 83e7131b2675..fb58aeb34e07 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -132,7 +132,7 @@ impl { async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, @@ -157,7 +157,7 @@ impl { async fn update_trackers<'b>( &'b self, - _db: &dyn StorageInterface, + _state: &'b AppState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 26bda6d6bee6..53a768f26810 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -422,7 +422,7 @@ impl #[instrument(skip_all)] async fn update_trackers<'b>( &'b self, - db: &dyn StorageInterface, + state: &'b AppState, mut payment_data: PaymentData, customer: Option, storage_scheme: storage_enums::MerchantStorageScheme, @@ -456,7 +456,7 @@ impl .payment_method_data .as_ref() .async_map(|payment_method_data| async { - helpers::get_additional_payment_data(payment_method_data, db).await + helpers::get_additional_payment_data(payment_method_data, &*state.store).await }) .await .as_ref() @@ -471,6 +471,7 @@ 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() @@ -479,7 +480,8 @@ impl .surcharge_details .as_ref() .map(|surcharge_details| surcharge_details.tax_on_surcharge_amount); - payment_data.payment_attempt = db + payment_data.payment_attempt = state + .store .update_payment_attempt_with_attempt_id( payment_data.payment_attempt, storage::PaymentAttemptUpdate::Update { @@ -540,7 +542,8 @@ impl let order_details = payment_data.payment_intent.order_details.clone(); let metadata = payment_data.payment_intent.metadata.clone(); - payment_data.payment_intent = db + payment_data.payment_intent = state + .store .update_payment_intent( payment_data.payment_intent, storage::PaymentIntentUpdate::Update { diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index ba4d7f6549e7..db53a3b56a15 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -79,29 +79,35 @@ pub async fn payments_incoming_webhook_flow< .perform_locking_action(&state, merchant_account.merchant_id.to_string()) .await?; - let response = - payments::payments_core::( - state.clone(), - merchant_account.clone(), - key_store, - payments::operations::PaymentStatus, - api::PaymentsRetrieveRequest { - resource_id: id, - merchant_id: Some(merchant_account.merchant_id.clone()), - force_sync: true, - connector: None, - param: None, - merchant_connector_details: None, - client_secret: None, - expand_attempts: None, - expand_captures: None, - }, - services::AuthFlow::Merchant, - consume_or_trigger_flow, - None, - HeaderPayload::default(), - ) - .await; + let response = Box::pin(payments::payments_core::< + api::PSync, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( + state.clone(), + merchant_account.clone(), + key_store, + payments::operations::PaymentStatus, + api::PaymentsRetrieveRequest { + resource_id: id, + merchant_id: Some(merchant_account.merchant_id.clone()), + force_sync: true, + connector: None, + param: None, + merchant_connector_details: None, + client_secret: None, + expand_attempts: None, + expand_captures: None, + }, + services::AuthFlow::Merchant, + consume_or_trigger_flow, + None, + HeaderPayload::default(), + )) + .await; lock_action .free_lock_action(&state, merchant_account.merchant_id.to_owned()) @@ -572,7 +578,14 @@ async fn bank_transfer_webhook_flow( + Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Ctx, + >( state.clone(), merchant_account.to_owned(), key_store, @@ -582,7 +595,7 @@ async fn bank_transfer_webhook_flow RouterResponse { - let (application_response, _webhooks_response_tracker) = webhooks_core::( + let (application_response, _webhooks_response_tracker) = Box::pin(webhooks_core::( state, req, merchant_account, key_store, connector_name_or_mca_id, body, - ) + )) .await?; Ok(application_response) @@ -1089,18 +1102,18 @@ pub async fn webhooks_core payments_incoming_webhook_flow::( + api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow::( state.clone(), merchant_account, business_profile, key_store, webhook_details, source_verified, - ) + )) .await .attach_printable("Incoming webhook flow for payments failed")?, - api::WebhookFlow::Refund => refunds_incoming_webhook_flow::( + api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow::( state.clone(), merchant_account, business_profile, @@ -1109,7 +1122,7 @@ pub async fn webhooks_core bank_transfer_webhook_flow::( + api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow::( state.clone(), merchant_account, business_profile, key_store, webhook_details, source_verified, - ) + )) .await .attach_printable("Incoming bank-transfer webhook flow failed")?, diff --git a/crates/router/src/db/address.rs b/crates/router/src/db/address.rs index 9244fc022d9e..689d1f9c7891 100644 --- a/crates/router/src/db/address.rs +++ b/crates/router/src/db/address.rs @@ -339,7 +339,7 @@ mod storage { MerchantStorageScheme::RedisKv => { let key = format!("mid_{}_pid_{}", merchant_id, payment_id); let field = format!("add_{}", address_id); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -350,7 +350,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } }?; diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index c9b9f8ac55f5..8ac8bd106eff 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -310,7 +310,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -321,7 +321,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -490,7 +490,7 @@ mod storage { let pattern = db_utils::generate_hscan_pattern_for_refund(&lookup.sk_id); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -501,7 +501,7 @@ mod storage { .try_into_scan() }, database_call, - ) + )) .await } } @@ -581,7 +581,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -592,7 +592,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -626,7 +626,7 @@ mod storage { .await?; let key = &lookup.pk_id; - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -637,7 +637,7 @@ mod storage { .try_into_hget() }, database_call, - ) + )) .await } } @@ -664,7 +664,7 @@ mod storage { enums::MerchantStorageScheme::PostgresOnly => database_call().await, enums::MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); - db_utils::try_redis_get_else_try_database_get( + Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -675,7 +675,7 @@ mod storage { .try_into_scan() }, database_call, - ) + )) .await } } diff --git a/crates/router/src/db/reverse_lookup.rs b/crates/router/src/db/reverse_lookup.rs index 4a4056032b18..445e171fa277 100644 --- a/crates/router/src/db/reverse_lookup.rs +++ b/crates/router/src/db/reverse_lookup.rs @@ -150,7 +150,11 @@ mod storage { .try_into_get() }; - db_utils::try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(db_utils::try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index e106eb06a766..a3ed0b35c785 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -189,7 +189,7 @@ pub async fn start_server(conf: settings::Settings) -> ApplicationResult errors::ApplicationError::ApiClientError(error.current_context().clone()) })?, ); - let state = routes::AppState::new(conf, tx, api_client).await; + let state = Box::pin(routes::AppState::new(conf, tx, api_client)).await; let request_body_limit = server.request_body_limit; let server = actix_web::HttpServer::new(move || mk_app(state.clone(), request_body_limit)) .bind((server.host.as_str(), server.port))? diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index a8eda22402c3..eef8cacc5f92 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -30,7 +30,7 @@ pub async fn merchant_account_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::MerchantsAccountCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,7 +38,7 @@ pub async fn merchant_account_create( |state, _, req| create_merchant_account(state, req), &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Account - Retrieve @@ -131,7 +131,7 @@ pub async fn update_merchant_account( ) -> HttpResponse { let flow = Flow::MerchantsAccountUpdate; let merchant_id = mid.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -145,7 +145,7 @@ pub async fn update_merchant_account( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } @@ -210,7 +210,7 @@ pub async fn payment_connector_create( ) -> HttpResponse { let flow = Flow::MerchantConnectorsCreate; let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -224,7 +224,7 @@ pub async fn payment_connector_create( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Merchant Connector - Retrieve @@ -450,7 +450,7 @@ pub async fn business_profile_create( let payload = json_payload.into_inner(); let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -464,7 +464,7 @@ pub async fn business_profile_create( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::BusinessProfileRetrieve))] diff --git a/crates/router/src/routes/api_keys.rs b/crates/router/src/routes/api_keys.rs index 1f71f1dc2800..7299aa696390 100644 --- a/crates/router/src/routes/api_keys.rs +++ b/crates/router/src/routes/api_keys.rs @@ -36,7 +36,7 @@ pub async fn api_key_create( let payload = json_payload.into_inner(); let merchant_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -61,7 +61,7 @@ pub async fn api_key_create( req.headers(), ), api_locking::LockAction::NotApplicable, - ) + )) .await } /// API Key - Retrieve diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 7f5c720be607..15b6df733489 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -112,56 +112,59 @@ impl AppState { shut_down_signal: oneshot::Sender<()>, api_client: Box, ) -> Self { - #[cfg(feature = "kms")] - let kms_client = kms::get_kms_client(&conf.kms).await; - let testable = storage_impl == StorageImpl::PostgresqlTest; - let store: Box = match storage_impl { - StorageImpl::Postgresql | StorageImpl::PostgresqlTest => Box::new( + Box::pin(async move { + #[cfg(feature = "kms")] + let kms_client = kms::get_kms_client(&conf.kms).await; + let testable = storage_impl == StorageImpl::PostgresqlTest; + let store: Box = match storage_impl { + StorageImpl::Postgresql | StorageImpl::PostgresqlTest => Box::new( + #[allow(clippy::expect_used)] + get_store(&conf, shut_down_signal, testable) + .await + .expect("Failed to create store"), + ), #[allow(clippy::expect_used)] - get_store(&conf, shut_down_signal, testable) - .await - .expect("Failed to create store"), - ), - #[allow(clippy::expect_used)] - StorageImpl::Mock => Box::new( - MockDb::new(&conf.redis) - .await - .expect("Failed to create mock store"), - ), - }; + StorageImpl::Mock => Box::new( + MockDb::new(&conf.redis) + .await + .expect("Failed to create mock store"), + ), + }; + + #[cfg(feature = "olap")] + let pool = crate::analytics::AnalyticsProvider::from_conf( + &conf.analytics, + #[cfg(feature = "kms")] + kms_client, + ) + .await; - #[cfg(feature = "olap")] - let pool = crate::analytics::AnalyticsProvider::from_conf( - &conf.analytics, #[cfg(feature = "kms")] - kms_client, - ) - .await; - - #[cfg(feature = "kms")] - #[allow(clippy::expect_used)] - let kms_secrets = settings::ActiveKmsSecrets { - jwekey: conf.jwekey.clone().into(), - } - .decrypt_inner(kms_client) - .await - .expect("Failed while performing KMS decryption"); - - #[cfg(feature = "email")] - let email_client = Arc::new(AwsSes::new(&conf.email).await); - Self { - flow_name: String::from("default"), - store, - conf: Arc::new(conf), + #[allow(clippy::expect_used)] + let kms_secrets = settings::ActiveKmsSecrets { + jwekey: conf.jwekey.clone().into(), + } + .decrypt_inner(kms_client) + .await + .expect("Failed while performing KMS decryption"); + #[cfg(feature = "email")] - email_client, - #[cfg(feature = "kms")] - kms_secrets: Arc::new(kms_secrets), - api_client, - event_handler: Box::::default(), - #[cfg(feature = "olap")] - pool, - } + let email_client = Arc::new(AwsSes::new(&conf.email).await); + Self { + flow_name: String::from("default"), + store, + conf: Arc::new(conf), + #[cfg(feature = "email")] + email_client, + #[cfg(feature = "kms")] + kms_secrets: Arc::new(kms_secrets), + api_client, + event_handler: Box::::default(), + #[cfg(feature = "olap")] + pool, + } + }) + .await } pub async fn new( @@ -169,7 +172,13 @@ impl AppState { shut_down_signal: oneshot::Sender<()>, api_client: Box, ) -> Self { - Self::with_storage(conf, StorageImpl::Postgresql, shut_down_signal, api_client).await + Box::pin(Self::with_storage( + conf, + StorageImpl::Postgresql, + shut_down_signal, + api_client, + )) + .await } } diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index ff2ffc2a3fe3..cfc37cbdbb2a 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -30,7 +30,7 @@ pub async fn customers_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::CustomersCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,7 +38,7 @@ pub async fn customers_create( |state, auth, req| create_customer(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Retrieve Customer @@ -142,7 +142,7 @@ pub async fn customers_update( let flow = Flow::CustomersUpdate; let customer_id = path.into_inner(); json_payload.customer_id = customer_id; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -150,7 +150,7 @@ pub async fn customers_update( |state, auth, req| update_customer(state, auth.merchant_account, req, auth.key_store), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Delete Customer @@ -179,7 +179,7 @@ pub async fn customers_delete( customer_id: path.into_inner(), }) .into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -187,7 +187,7 @@ pub async fn customers_delete( |state, auth, req| delete_customer(state, auth.merchant_account, req, auth.key_store), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::CustomersGetMandates))] diff --git a/crates/router/src/routes/disputes.rs b/crates/router/src/routes/disputes.rs index d570a5319687..aaeb118645db 100644 --- a/crates/router/src/routes/disputes.rs +++ b/crates/router/src/routes/disputes.rs @@ -117,7 +117,7 @@ pub async fn accept_dispute( let dispute_id = dispute_types::DisputeId { dispute_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -127,7 +127,7 @@ pub async fn accept_dispute( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Disputes - Submit Dispute Evidence @@ -150,7 +150,7 @@ pub async fn submit_dispute_evidence( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::DisputesEvidenceSubmit; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -160,7 +160,7 @@ pub async fn submit_dispute_evidence( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Disputes - Attach Evidence to Dispute @@ -191,7 +191,7 @@ pub async fn attach_dispute_evidence( Ok(valid_request) => valid_request, Err(err) => return api::log_and_return_error_response(err), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -201,7 +201,7 @@ pub async fn attach_dispute_evidence( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Diputes - Retrieve Dispute @@ -229,7 +229,7 @@ pub async fn retrieve_dispute_evidence( let dispute_id = dispute_types::DisputeId { dispute_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -237,6 +237,6 @@ pub async fn retrieve_dispute_evidence( |state, auth, req| disputes::retrieve_dispute_evidence(state, auth.merchant_account, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/files.rs b/crates/router/src/routes/files.rs index 4a327ba0807d..bde221ebc161 100644 --- a/crates/router/src/routes/files.rs +++ b/crates/router/src/routes/files.rs @@ -39,7 +39,7 @@ pub async fn files_create( Ok(valid_request) => valid_request, Err(err) => return api::log_and_return_error_response(err), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -47,7 +47,7 @@ pub async fn files_create( |state, auth, req| files_create_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Files - Delete @@ -77,7 +77,7 @@ pub async fn files_delete( let file_id = files::FileId { file_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -85,7 +85,7 @@ pub async fn files_delete( |state, auth, req| files_delete_core(state, auth.merchant_account, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Files - Retrieve @@ -115,7 +115,7 @@ pub async fn files_retrieve( let file_id = files::FileId { file_id: path.into_inner(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -123,6 +123,6 @@ pub async fn files_retrieve( |state, auth, req| files_retrieve_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index b664ee4429d4..7d6bf1a05f09 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -62,7 +62,7 @@ pub async fn initiate_payment_link( payment_id, merchant_id: merchant_id.clone(), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -77,6 +77,6 @@ pub async fn initiate_payment_link( }, &crate::services::authentication::MerchantIdAuth(merchant_id), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index faaf757fd7e7..83d4c7f96611 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -34,7 +34,7 @@ pub async fn create_payment_method_api( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PaymentMethodsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -44,7 +44,7 @@ pub async fn create_payment_method_api( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Merchant @@ -84,7 +84,7 @@ pub async fn list_payment_method_api( Err(e) => return api::log_and_return_error_response(e), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -94,7 +94,7 @@ pub async fn list_payment_method_api( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Customer @@ -135,7 +135,7 @@ pub async fn list_customer_payment_method_api( Err(e) => return api::log_and_return_error_response(e), }; let customer_id = customer_id.into_inner().0; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -151,7 +151,7 @@ pub async fn list_customer_payment_method_api( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// List payment methods for a Customer @@ -191,7 +191,7 @@ pub async fn list_customer_payment_method_api_client( Ok((auth, _auth_flow)) => (auth, _auth_flow), Err(e) => return api::log_and_return_error_response(e), }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -207,7 +207,7 @@ pub async fn list_customer_payment_method_api_client( }, &*auth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Retrieve @@ -239,7 +239,7 @@ pub async fn payment_method_retrieve_api( }) .into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -247,7 +247,7 @@ pub async fn payment_method_retrieve_api( |state, _auth, pm| cards::retrieve_payment_method(state, pm), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Update @@ -278,7 +278,7 @@ pub async fn payment_method_update_api( let flow = Flow::PaymentMethodsUpdate; let payment_method_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -294,7 +294,7 @@ pub async fn payment_method_update_api( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payment Method - Delete @@ -324,7 +324,7 @@ pub async fn payment_method_delete_api( let pm = PaymentMethodId { payment_method_id: payment_method_id.into_inner().0, }; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -332,7 +332,7 @@ pub async fn payment_method_delete_api( |state, auth, req| cards::delete_payment_method(state, auth.merchant_account, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[cfg(test)] diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index ed36721da445..b05fae65338a 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -102,7 +102,7 @@ pub async fn payments_create( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -123,7 +123,7 @@ pub async fn payments_create( _ => auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), }, locking_action, - ) + )) .await } // /// Payments - Redirect @@ -160,7 +160,7 @@ pub async fn payments_start( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -187,7 +187,7 @@ pub async fn payments_start( }, &auth::MerchantIdAuth(merchant_id), locking_action, - ) + )) .await } /// Payments - Retrieve @@ -234,7 +234,7 @@ pub async fn payments_retrieve( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -258,7 +258,7 @@ pub async fn payments_retrieve( req.headers(), ), locking_action, - ) + )) .await } /// Payments - Retrieve with gateway credentials @@ -300,7 +300,7 @@ pub async fn payments_retrieve_with_gateway_creds( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -320,7 +320,7 @@ pub async fn payments_retrieve_with_gateway_creds( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Update @@ -367,7 +367,7 @@ pub async fn payments_update( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -385,7 +385,7 @@ pub async fn payments_update( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Confirm @@ -443,7 +443,7 @@ pub async fn payments_confirm( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -461,7 +461,7 @@ pub async fn payments_confirm( }, &*auth_type, locking_action, - ) + )) .await } /// Payments - Capture @@ -498,7 +498,7 @@ pub async fn payments_capture( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -525,7 +525,7 @@ pub async fn payments_capture( }, &auth::ApiKeyAuth, locking_action, - ) + )) .await } /// Payments - Session token @@ -554,7 +554,7 @@ pub async fn payments_connector_session( let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -581,7 +581,7 @@ pub async fn payments_connector_session( }, &auth::PublishableKeyAuth, locking_action, - ) + )) .await } // /// Payments - Redirect response @@ -772,7 +772,7 @@ pub async fn payments_cancel( let payment_id = path.into_inner(); payload.payment_id = payment_id; let locking_action = payload.get_locking_input(flow.clone()); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -792,7 +792,7 @@ pub async fn payments_cancel( }, &auth::ApiKeyAuth, locking_action, - ) + )) .await } /// Payments - List diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 15cf59aaf32d..cc47263a0c56 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -33,7 +33,7 @@ pub async fn payouts_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -41,7 +41,7 @@ pub async fn payouts_create( |state, auth, req| payouts_create_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Retrieve @@ -72,7 +72,7 @@ pub async fn payouts_retrieve( force_sync: query_params.force_sync, }; let flow = Flow::PayoutsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -80,7 +80,7 @@ pub async fn payouts_retrieve( |state, auth, req| payouts_retrieve_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Update @@ -111,7 +111,7 @@ pub async fn payouts_update( let payout_id = path.into_inner(); let mut payout_update_payload = json_payload.into_inner(); payout_update_payload.payout_id = Some(payout_id); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -119,7 +119,7 @@ pub async fn payouts_update( |state, auth, req| payouts_update_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Cancel @@ -150,7 +150,7 @@ pub async fn payouts_cancel( let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -158,7 +158,7 @@ pub async fn payouts_cancel( |state, auth, req| payouts_cancel_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Payouts - Fulfill @@ -189,7 +189,7 @@ pub async fn payouts_fulfill( let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -197,7 +197,7 @@ pub async fn payouts_fulfill( |state, auth, req| payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req), &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } #[instrument(skip_all, fields(flow = ?Flow::PayoutsAccounts))] diff --git a/crates/router/src/routes/refunds.rs b/crates/router/src/routes/refunds.rs index d1f5cb56fe23..d370af6b8d7a 100644 --- a/crates/router/src/routes/refunds.rs +++ b/crates/router/src/routes/refunds.rs @@ -31,7 +31,7 @@ pub async fn refunds_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsCreate; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -39,7 +39,7 @@ pub async fn refunds_create( |state, auth, req| refund_create_core(state, auth.merchant_account, auth.key_store, req), auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Retrieve (GET) @@ -74,7 +74,7 @@ pub async fn refunds_retrieve( }; let flow = Flow::RefundsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -90,7 +90,7 @@ pub async fn refunds_retrieve( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Retrieve (POST) @@ -115,7 +115,7 @@ pub async fn refunds_retrieve_with_body( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::RefundsRetrieve; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -131,7 +131,7 @@ pub async fn refunds_retrieve_with_body( }, &auth::ApiKeyAuth, api_locking::LockAction::NotApplicable, - ) + )) .await } /// Refunds - Update diff --git a/crates/router/src/routes/verification.rs b/crates/router/src/routes/verification.rs index 2ad061848c92..d0525bb272e8 100644 --- a/crates/router/src/routes/verification.rs +++ b/crates/router/src/routes/verification.rs @@ -18,7 +18,7 @@ pub async fn apple_pay_merchant_registration( let flow = Flow::Verification; let merchant_id = path.into_inner(); let kms_conf = &state.clone().conf.kms; - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -34,7 +34,7 @@ pub async fn apple_pay_merchant_registration( }, auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, req.headers()), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/routes/webhooks.rs b/crates/router/src/routes/webhooks.rs index 5c90e46bb90b..63f2328ec6ce 100644 --- a/crates/router/src/routes/webhooks.rs +++ b/crates/router/src/routes/webhooks.rs @@ -21,7 +21,7 @@ pub async fn receive_incoming_webhook( let flow = Flow::IncomingWebhookReceive; let (merchant_id, connector_id_or_name) = path.into_inner(); - api::server_wrap( + Box::pin(api::server_wrap( flow, state, &req, @@ -38,6 +38,6 @@ pub async fn receive_incoming_webhook( }, &auth::MerchantIdAuth(merchant_id), api_locking::LockAction::NotApplicable, - ) + )) .await } diff --git a/crates/router/src/types/domain/customer.rs b/crates/router/src/types/domain/customer.rs index 3810523b413f..fe575851dc49 100644 --- a/crates/router/src/types/domain/customer.rs +++ b/crates/router/src/types/domain/customer.rs @@ -99,7 +99,7 @@ impl super::behaviour::Conversion for Customer { } } -#[derive(Debug)] +#[derive(Clone, Debug)] pub enum CustomerUpdate { Update { name: crypto::OptionalEncryptableName, diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index aadb714e8ce2..4933b4d700d3 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -753,9 +753,11 @@ where if let services::ApplicationResponse::JsonWithHeaders((payments_response_json, _)) = payments_response { + let m_state = state.clone(); + Box::pin( webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook( - state.clone(), + m_state, merchant_account, business_profile, event_type, @@ -772,3 +774,16 @@ where Ok(()) } + +type Handle = tokio::task::JoinHandle>; + +pub async fn flatten_join_error(handle: Handle) -> RouterResult { + match handle.await { + Ok(Ok(t)) => Ok(t), + Ok(Err(err)) => Err(err), + Err(err) => Err(err) + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Join Error"), + } +} diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index f41b300c5127..00e7357d896f 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -61,7 +61,13 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { .await?; let (mut payment_data, _, customer, _, _) = - payment_flows::payments_operation_core::( + Box::pin(payment_flows::payments_operation_core::< + api::PSync, + _, + _, + _, + Oss, + >( state, merchant_account.clone(), key_store, @@ -71,7 +77,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { services::AuthFlow::Client, None, api::HeaderPayload::default(), - ) + )) .await?; let terminal_status = [ @@ -169,7 +175,11 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { // Trigger the outgoing webhook to notify the merchant about failed payment let operation = operations::PaymentStatus; - utils::trigger_payments_webhook::<_, api_models::payments::PaymentsRequest, _>( + Box::pin(utils::trigger_payments_webhook::< + _, + api_models::payments::PaymentsRequest, + _, + >( merchant_account, business_profile, payment_data, @@ -177,7 +187,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { customer, state, operation, - ) + )) .await .map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error)) .ok(); diff --git a/crates/router/src/workflows/refund_router.rs b/crates/router/src/workflows/refund_router.rs index 8ca3551cfc0f..934c208f9115 100644 --- a/crates/router/src/workflows/refund_router.rs +++ b/crates/router/src/workflows/refund_router.rs @@ -13,7 +13,7 @@ impl ProcessTrackerWorkflow for RefundWorkflowRouter { state: &'a AppState, process: storage::ProcessTracker, ) -> Result<(), errors::ProcessTrackerError> { - Ok(refund_flow::start_refund_workflow(state, &process).await?) + Ok(Box::pin(refund_flow::start_refund_workflow(state, &process)).await?) } async fn error_handler<'a>( diff --git a/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index e1fd3a0f0279..4de45c7132a8 100644 --- a/crates/router/tests/cache.rs +++ b/crates/router/tests/cache.rs @@ -7,10 +7,14 @@ mod utils; #[actix_web::test] async fn invalidate_existing_cache_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; let cache_key = "cacheKey".to_string(); let cache_key_value = "val".to_string(); @@ -53,7 +57,7 @@ async fn invalidate_existing_cache_success() { #[actix_web::test] async fn invalidate_non_existing_cache_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let cache_key = "cacheKey".to_string(); let api_key = ("api-key", "test_admin"); let client = awc::Client::default(); diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 1f450a19e776..67a0625968fb 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -80,7 +80,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn create_connector_customer( @@ -104,7 +104,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn create_connector_pm_token( @@ -128,7 +128,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// For initiating payments when `CaptureMethod` is set to `Automatic` @@ -156,7 +156,7 @@ pub trait ConnectorActions: Connector { ) .await; integration.execute_pretasks(&mut request, &state).await?; - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn sync_payment( @@ -169,7 +169,7 @@ pub trait ConnectorActions: Connector { payment_data.unwrap_or_else(|| PaymentSyncType::default().0), payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// will retry the psync till the given status matches or retry max 3 times @@ -207,7 +207,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn authorize_and_capture_payment( @@ -243,7 +243,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn authorize_and_void_payment( @@ -280,7 +280,7 @@ pub trait ConnectorActions: Connector { }, payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } async fn capture_payment_and_refund( @@ -400,7 +400,7 @@ pub trait ConnectorActions: Connector { }), payment_info, ); - call_connector(request, integration).await + Box::pin(call_connector(request, integration)).await } /// will retry the rsync till the given status matches or retry max 3 times diff --git a/crates/router/tests/customers.rs b/crates/router/tests/customers.rs index aa17635388fd..065f98fe6609 100644 --- a/crates/router/tests/customers.rs +++ b/crates/router/tests/customers.rs @@ -10,7 +10,7 @@ mod utils; #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn customer_success() { - utils::setup().await; + Box::pin(utils::setup()).await; let customer_id = format!("customer_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -79,7 +79,7 @@ async fn customer_success() { #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn customer_failure() { - utils::setup().await; + Box::pin(utils::setup()).await; let customer_id = format!("customer_{}", uuid::Uuid::new_v4()); let api_key = ("api-key", "MySecretApiKey"); diff --git a/crates/router/tests/integration_demo.rs b/crates/router/tests/integration_demo.rs index 16e7ead0a383..5bdf9a5f525e 100644 --- a/crates/router/tests/integration_demo.rs +++ b/crates/router/tests/integration_demo.rs @@ -10,7 +10,7 @@ use utils::{mk_service, ApiKey, AppClient, MerchantId, PaymentId, Status}; /// 1) Create Merchant account #[actix_web::test] async fn create_merchant_account() { - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); @@ -59,7 +59,7 @@ async fn create_merchant_account() { #[actix_web::test] async fn partial_refund() { let authentication = ConnectorAuthentication::new(); - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); @@ -125,7 +125,7 @@ async fn partial_refund() { #[actix_web::test] async fn exceed_refund() { let authentication = ConnectorAuthentication::new(); - let server = mk_service().await; + let server = Box::pin(mk_service()).await; let client = AppClient::guest(); let admin_client = client.admin("test_admin"); diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index d2d6c48507e5..9d48aaddd451 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -24,7 +24,7 @@ use uuid::Uuid; #[ignore] // verify the API-KEY/merchant id has stripe as first choice async fn payments_create_stripe() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -93,7 +93,7 @@ async fn payments_create_stripe() { #[ignore] // verify the API-KEY/merchant id has adyen as first choice async fn payments_create_adyen() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "321"); @@ -162,7 +162,7 @@ async fn payments_create_adyen() { // verify the API-KEY/merchant id has stripe as first choice #[ignore] async fn payments_create_fail() { - utils::setup().await; + Box::pin(utils::setup()).await; let payment_id = format!("test_{}", uuid::Uuid::new_v4()); let api_key = ("API-KEY", "MySecretApiKey"); @@ -221,7 +221,7 @@ async fn payments_create_fail() { #[actix_web::test] #[ignore] async fn payments_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; @@ -360,20 +360,26 @@ async fn payments_create_core() { }; let expected_response = services::ApplicationResponse::JsonWithHeaders((expected_response, vec![])); - let actual_response = - payments::payments_core::( - state, - merchant_account, - key_store, - payments::PaymentCreate, - req, - services::AuthFlow::Merchant, - payments::CallConnectorAction::Trigger, - None, - api::HeaderPayload::default(), - ) - .await - .unwrap(); + let actual_response = Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + merchant_account, + key_store, + payments::PaymentCreate, + req, + services::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + api::HeaderPayload::default(), + )) + .await + .unwrap(); assert_eq!(expected_response, actual_response); } @@ -531,19 +537,25 @@ async fn payments_create_core_adyen_no_redirect() { }, vec![], )); - let actual_response = - payments::payments_core::( - state, - merchant_account, - key_store, - payments::PaymentCreate, - req, - services::AuthFlow::Merchant, - payments::CallConnectorAction::Trigger, - None, - api::HeaderPayload::default(), - ) - .await - .unwrap(); + let actual_response = Box::pin(payments::payments_core::< + api::Authorize, + api::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + merchant_account, + key_store, + payments::PaymentCreate, + req, + services::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + api::HeaderPayload::default(), + )) + .await + .unwrap(); assert_eq!(expected_response, actual_response); } diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index ed8827a910be..5d4ca844061f 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -120,7 +120,7 @@ async fn payments_create_core() { }; let expected_response = services::ApplicationResponse::JsonWithHeaders((expected_response, vec![])); - let actual_response = router::core::payments::payments_core::< + let actual_response = Box::pin(router::core::payments::payments_core::< api::Authorize, api::PaymentsResponse, _, @@ -137,7 +137,7 @@ async fn payments_create_core() { payments::CallConnectorAction::Trigger, None, api::HeaderPayload::default(), - ) + )) .await .unwrap(); assert_eq!(expected_response, actual_response); @@ -299,7 +299,7 @@ async fn payments_create_core_adyen_no_redirect() { }, vec![], )); - let actual_response = router::core::payments::payments_core::< + let actual_response = Box::pin(router::core::payments::payments_core::< api::Authorize, api::PaymentsResponse, _, @@ -316,7 +316,7 @@ async fn payments_create_core_adyen_no_redirect() { payments::CallConnectorAction::Trigger, None, api::HeaderPayload::default(), - ) + )) .await .unwrap(); assert_eq!(expected_response, actual_response); diff --git a/crates/router/tests/payouts.rs b/crates/router/tests/payouts.rs index 566930cd4e31..ab0bc891a7cc 100644 --- a/crates/router/tests/payouts.rs +++ b/crates/router/tests/payouts.rs @@ -4,7 +4,7 @@ mod utils; #[actix_web::test] async fn payouts_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; diff --git a/crates/router/tests/refunds.rs b/crates/router/tests/refunds.rs index c9e08d223503..6b9dfd5ed4a2 100644 --- a/crates/router/tests/refunds.rs +++ b/crates/router/tests/refunds.rs @@ -11,7 +11,7 @@ mod utils; #[actix_web::test] // verify the API-KEY/merchant id has stripe as first choice async fn refund_create_fail_stripe() { - let app = mk_service().await; + let app = Box::pin(mk_service()).await; let client = AppClient::guest(); let user_client = client.user("321"); @@ -25,7 +25,7 @@ async fn refund_create_fail_stripe() { #[actix_web::test] // verify the API-KEY/merchant id has adyen as first choice async fn refund_create_fail_adyen() { - let app = mk_service().await; + let app = Box::pin(mk_service()).await; let client = AppClient::guest(); let user_client = client.user("321"); @@ -39,7 +39,7 @@ async fn refund_create_fail_adyen() { #[actix_web::test] #[ignore] async fn refunds_todo() { - utils::setup().await; + Box::pin(utils::setup()).await; let client = awc::Client::default(); let mut response; diff --git a/crates/router/tests/services.rs b/crates/router/tests/services.rs index 64f1c3d8ee1b..eff7fe7f8738 100644 --- a/crates/router/tests/services.rs +++ b/crates/router/tests/services.rs @@ -10,8 +10,12 @@ async fn get_redis_conn_failure() { // Arrange utils::setup().await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; let _ = state.store.get_redis_conn().map(|conn| { conn.is_redis_available @@ -28,10 +32,14 @@ async fn get_redis_conn_failure() { #[tokio::test] async fn get_redis_conn_success() { // Arrange - utils::setup().await; + Box::pin(utils::setup()).await; let (tx, _) = tokio::sync::oneshot::channel(); - let state = - routes::AppState::new(Settings::default(), tx, Box::new(services::MockApiClient)).await; + let state = Box::pin(routes::AppState::new( + Settings::default(), + tx, + Box::new(services::MockApiClient), + )) + .await; // Act let result = state.store.get_redis_conn(); diff --git a/crates/router/tests/utils.rs b/crates/router/tests/utils.rs index 274c011df7a0..6cddbc043662 100644 --- a/crates/router/tests/utils.rs +++ b/crates/router/tests/utils.rs @@ -20,7 +20,7 @@ static SERVER: OnceCell = OnceCell::const_new(); async fn spawn_server() -> bool { let conf = Settings::new().expect("invalid settings"); - let server = router::start_server(conf) + let server = Box::pin(router::start_server(conf)) .await .expect("failed to create server"); @@ -29,7 +29,7 @@ async fn spawn_server() -> bool { } pub async fn setup() { - SERVER.get_or_init(spawn_server).await; + Box::pin(SERVER.get_or_init(spawn_server)).await; } const STRIPE_MOCK: &str = "http://localhost:12111/"; diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 31115e91589f..77589cc7d782 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -26,7 +26,7 @@ router_env = { version = "0.1.0", path = "../router_env" } # Third party crates actix-web = "4.3.1" -async-bb8-diesel = "0.1.0" +async-bb8-diesel = { git = "https://github.com/jarnura/async-bb8-diesel", rev = "53b4ab901aab7635c8215fd1c2d542c8db443094" } async-trait = "0.1.72" bb8 = "0.8.1" bytes = "1.4.0" diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 00d8703940c7..dc0dea4bb59c 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use data_models::errors::{StorageError, StorageResult}; -use diesel_models::{self as store}; +use diesel_models as store; use error_stack::ResultExt; use masking::StrongSecret; use redis::{kv_store::RedisConnInterface, RedisStore}; diff --git a/crates/storage_impl/src/lookup.rs b/crates/storage_impl/src/lookup.rs index dbfd77a8d6a0..bd045fedd379 100644 --- a/crates/storage_impl/src/lookup.rs +++ b/crates/storage_impl/src/lookup.rs @@ -135,7 +135,11 @@ impl ReverseLookupInterface for KVRouterStore { .try_into_get() }; - try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index d34230e2cb49..3d00e2f2bf7a 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -557,12 +557,12 @@ impl PaymentAttemptInterface for KVRouterStore { .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper(self, KvOperation::::HGet(&lookup.sk_id), key).await?.try_into_hget() }, || async {self.router_store.find_payment_attempt_by_connector_transaction_id_payment_id_merchant_id(connector_transaction_id, payment_id, merchant_id, storage_scheme).await}, - ) + )) .await } } @@ -607,7 +607,11 @@ impl PaymentAttemptInterface for KVRouterStore { )) }) }; - try_redis_get_else_try_database_get(redis_fut, database_call).await + Box::pin(try_redis_get_else_try_database_get( + redis_fut, + database_call, + )) + .await } } } @@ -635,7 +639,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -654,7 +658,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -682,7 +686,7 @@ impl PaymentAttemptInterface for KVRouterStore { MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); let field = format!("pa_{attempt_id}"); - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper(self, KvOperation::::HGet(&field), key) .await? @@ -698,7 +702,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -726,7 +730,7 @@ impl PaymentAttemptInterface for KVRouterStore { .get_lookup_by_lookup_id(&lookup_id, storage_scheme) .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -745,7 +749,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } @@ -774,7 +778,7 @@ impl PaymentAttemptInterface for KVRouterStore { .await?; let key = &lookup.pk_id; - try_redis_get_else_try_database_get( + Box::pin(try_redis_get_else_try_database_get( async { kv_wrapper( self, @@ -793,7 +797,7 @@ impl PaymentAttemptInterface for KVRouterStore { ) .await }, - ) + )) .await } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 2dc5cdd1c026..c3b3d22ffe35 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -39,7 +39,7 @@ use crate::connection; use crate::{ diesel_error_to_data_error, redis::kv_store::{kv_wrapper, KvOperation}, - utils::{pg_connection_read, pg_connection_write}, + utils::{self, pg_connection_read, pg_connection_write}, DataModelExt, DatabaseStore, KVRouterStore, }; @@ -206,7 +206,7 @@ impl PaymentIntentInterface for KVRouterStore { MerchantStorageScheme::RedisKv => { let key = format!("mid_{merchant_id}_pid_{payment_id}"); let field = format!("pi_{payment_id}"); - crate::utils::try_redis_get_else_try_database_get( + Box::pin(utils::try_redis_get_else_try_database_get( async { kv_wrapper::( self, @@ -217,7 +217,7 @@ impl PaymentIntentInterface for KVRouterStore { .try_into_hget() }, database_call, - ) + )) .await } }