From 906d754944e78d75fc0e1f034abaebcf3045c1fe Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 27 Dec 2024 00:26:32 +0000 Subject: [PATCH 01/30] chore(version): 2024.12.27.0 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bc0977570e..b7df3539fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to HyperSwitch will be documented here. - - - + ## 2024.12.26.1 ### Bug Fixes From d19c1a19638d72ad9df12205bf9411c292d1ed6f Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:23:47 +0530 Subject: [PATCH 02/30] refactor(payment_methods): update `connector_mandate_details` for card metadata changes (#6848) --- .../payments/operations/payment_response.rs | 3 + .../router/src/core/payments/tokenization.rs | 71 ++++++++++++++++++- 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index 91fdfae63fa..fbc2d2ee461 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -200,6 +200,7 @@ impl PostUpdateTracker, types::PaymentsAuthor payment_method_billing_address, business_profile, connector_mandate_reference_id.clone(), + merchant_connector_id.clone(), )); let is_connector_mandate = resp.request.customer_acceptance.is_some() @@ -315,6 +316,7 @@ impl PostUpdateTracker, types::PaymentsAuthor payment_method_billing_address.as_ref(), &business_profile, connector_mandate_reference_id, + merchant_connector_id.clone(), )) .await; @@ -1099,6 +1101,7 @@ impl PostUpdateTracker, types::SetupMandateRequestDa payment_method_billing_address, business_profile, connector_mandate_reference_id, + merchant_connector_id.clone(), )) .await?; diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs index 94f221dd722..84f848ef0ab 100644 --- a/crates/router/src/core/payments/tokenization.rs +++ b/crates/router/src/core/payments/tokenization.rs @@ -83,6 +83,7 @@ pub async fn save_payment_method( payment_method_billing_address: Option<&hyperswitch_domain_models::address::Address>, business_profile: &domain::Profile, mut original_connector_mandate_reference_id: Option, + merchant_connector_id: Option, ) -> RouterResult where FData: mandate::MandateBehaviour + Clone, @@ -458,7 +459,41 @@ where resp.payment_method_id = payment_method_id; let existing_pm = match payment_method { - Ok(pm) => Ok(pm), + Ok(pm) => { + let mandate_details = pm + .connector_mandate_details + .clone() + .map(|val| { + val.parse_value::( + "PaymentsMandateReference", + ) + }) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize to Payment Mandate Reference ")?; + if let Some((mandate_details, merchant_connector_id)) = + mandate_details.zip(merchant_connector_id) + { + let connector_mandate_details = + update_connector_mandate_details_status( + merchant_connector_id, + mandate_details, + ConnectorMandateStatus::Inactive, + )?; + payment_methods::cards::update_payment_method_connector_mandate_details( + state, + key_store, + db, + pm.clone(), + connector_mandate_details, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to add payment method in db")?; + } + Ok(pm) + } Err(err) => { if err.current_context().is_db_not_found() { payment_methods::cards::create_payment_method( @@ -1260,3 +1295,37 @@ pub fn update_connector_mandate_details( Ok(connector_mandate_details) } + +#[cfg(feature = "v1")] +pub fn update_connector_mandate_details_status( + merchant_connector_id: id_type::MerchantConnectorAccountId, + mut payment_mandate_reference: diesel_models::PaymentsMandateReference, + status: ConnectorMandateStatus, +) -> RouterResult> { + let mandate_reference = { + payment_mandate_reference + .entry(merchant_connector_id) + .and_modify(|pm| { + let update_rec = diesel_models::PaymentsMandateReferenceRecord { + connector_mandate_id: pm.connector_mandate_id.clone(), + payment_method_type: pm.payment_method_type, + original_payment_authorized_amount: pm.original_payment_authorized_amount, + original_payment_authorized_currency: pm.original_payment_authorized_currency, + mandate_metadata: pm.mandate_metadata.clone(), + connector_mandate_status: Some(status), + connector_mandate_request_reference_id: pm + .connector_mandate_request_reference_id + .clone(), + }; + *pm = update_rec + }); + Some(payment_mandate_reference) + }; + let connector_mandate_details = mandate_reference + .map(|mandate| mandate.encode_to_value()) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unable to serialize customer acceptance to value")?; + + Ok(connector_mandate_details) +} From cb12e3da1cc836a5ba8a98b998ba4ba7e47818af Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Fri, 27 Dec 2024 11:49:55 +0530 Subject: [PATCH 03/30] fix(router): rename `management_url` to `management_u_r_l` in the apple pay session response (#6945) --- api-reference-v2/openapi_spec.json | 4 ++-- api-reference/openapi_spec.json | 4 ++-- crates/api_models/src/payments.rs | 2 +- crates/router/src/core/payments/transformers.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 636723d808a..f1caede89cc 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3438,7 +3438,7 @@ "required": [ "payment_description", "regular_billing", - "management_url" + "management_u_r_l" ], "properties": { "payment_description": { @@ -3453,7 +3453,7 @@ "description": "A localized billing agreement that the payment sheet displays to the user before the user authorizes the payment", "nullable": true }, - "management_url": { + "management_u_r_l": { "type": "string", "description": "A URL to a web page where the user can update or delete the payment method for the recurring payment", "example": "https://hyperswitch.io" diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 186aa6dc9c3..df7e96431db 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -6052,7 +6052,7 @@ "required": [ "payment_description", "regular_billing", - "management_url" + "management_u_r_l" ], "properties": { "payment_description": { @@ -6067,7 +6067,7 @@ "description": "A localized billing agreement that the payment sheet displays to the user before the user authorizes the payment", "nullable": true }, - "management_url": { + "management_u_r_l": { "type": "string", "description": "A URL to a web page where the user can update or delete the payment method for the recurring payment", "example": "https://hyperswitch.io" diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index a1718944095..816c085b8fa 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6295,7 +6295,7 @@ pub struct ApplePayRecurringPaymentRequest { pub billing_agreement: Option, /// A URL to a web page where the user can update or delete the payment method for the recurring payment #[schema(value_type = String, example = "https://hyperswitch.io")] - pub management_url: common_utils::types::Url, + pub management_u_r_l: common_utils::types::Url, } #[derive(Debug, Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize, ToSchema)] diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index e532abaa7ea..79bcfb6a497 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -3480,7 +3480,7 @@ impl .recurring_payment_interval_count, }, billing_agreement: apple_pay_recurring_details.billing_agreement, - management_url: apple_pay_recurring_details.management_url, + management_u_r_l: apple_pay_recurring_details.management_url, } } } From e393a036fbde109d367e488807a53e919a12db90 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Fri, 27 Dec 2024 13:39:42 +0530 Subject: [PATCH 04/30] feat(connector): [Fiuu] Consume error message thrown by connector for Psync flow and make extraP from response struct Secret (#6934) --- .../hyperswitch_connectors/src/connectors/fiuu.rs | 4 ++-- .../src/connectors/fiuu/transformers.rs | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu.rs b/crates/hyperswitch_connectors/src/connectors/fiuu.rs index 7805281c4be..3ea8793d9f5 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu.rs @@ -908,7 +908,7 @@ impl webhooks::IncomingWebhook for Fiuu { serde_urlencoded::from_bytes::(request.body) .change_context(errors::ConnectorError::WebhookResourceObjectNotFound)?; let mandate_reference = webhook_payment_response.extra_parameters.as_ref().and_then(|extra_p| { - let mandate_token: Result = serde_json::from_str(extra_p); + let mandate_token: Result = serde_json::from_str(&extra_p.clone().expose()); match mandate_token { Ok(token) => { token.token.as_ref().map(|token| hyperswitch_domain_models::router_flow_types::ConnectorMandateDetails { @@ -918,7 +918,7 @@ impl webhooks::IncomingWebhook for Fiuu { Err(err) => { router_env::logger::warn!( "Failed to convert 'extraP' from fiuu webhook response to fiuu::ExtraParameters. \ - Input: '{}', Error: {}", + Input: '{:?}', Error: {}", extra_p, err ); diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 143cd1aa047..14ce13f971b 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs @@ -1168,9 +1168,9 @@ impl TryFrom> for PaymentsSy let error_response = if status == enums::AttemptStatus::Failure { Some(ErrorResponse { status_code: item.http_code, - code: response.stat_code.to_string(), - message: response.stat_name.clone().to_string(), - reason: Some(response.stat_name.clone().to_string()), + code: response.error_code.clone(), + message: response.error_desc.clone(), + reason: Some(response.error_desc), attempt_status: Some(enums::AttemptStatus::Failure), connector_transaction_id: None, }) @@ -1199,7 +1199,7 @@ impl TryFrom> for PaymentsSy status: response.status, })?; let mandate_reference = response.extra_parameters.as_ref().and_then(|extra_p| { - let mandate_token: Result = serde_json::from_str(extra_p); + let mandate_token: Result = serde_json::from_str(&extra_p.clone().expose()); match mandate_token { Ok(token) => { token.token.as_ref().map(|token| MandateReference { @@ -1212,7 +1212,7 @@ impl TryFrom> for PaymentsSy Err(err) => { router_env::logger::warn!( "Failed to convert 'extraP' from fiuu webhook response to fiuu::ExtraParameters. \ - Input: '{}', Error: {}", + Input: '{:?}', Error: {}", extra_p, err ); @@ -1228,7 +1228,7 @@ impl TryFrom> for PaymentsSy .clone() .unwrap_or(consts::NO_ERROR_CODE.to_owned()), message: response - .error_code + .error_desc .clone() .unwrap_or(consts::NO_ERROR_MESSAGE.to_owned()), reason: response.error_desc.clone(), @@ -1697,7 +1697,7 @@ pub struct FiuuWebhooksPaymentResponse { pub error_desc: Option, pub error_code: Option, #[serde(rename = "extraP")] - pub extra_parameters: Option, + pub extra_parameters: Option>, } #[derive(Debug, Deserialize, Serialize, Clone)] From 3eb2eb1cf5b9855432b49994b433e6f7fa404212 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 00:27:56 +0000 Subject: [PATCH 05/30] chore(version): 2024.12.30.0 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7df3539fda..b238cf58c2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.30.0 + +### Features + +- **connector:** [Fiuu] Consume error message thrown by connector for Psync flow and make extraP from response struct Secret ([#6934](https://github.com/juspay/hyperswitch/pull/6934)) ([`e393a03`](https://github.com/juspay/hyperswitch/commit/e393a036fbde109d367e488807a53e919a12db90)) + +### Bug Fixes + +- **router:** Rename `management_url` to `management_u_r_l` in the apple pay session response ([#6945](https://github.com/juspay/hyperswitch/pull/6945)) ([`cb12e3d`](https://github.com/juspay/hyperswitch/commit/cb12e3da1cc836a5ba8a98b998ba4ba7e47818af)) + +### Refactors + +- **payment_methods:** Update `connector_mandate_details` for card metadata changes ([#6848](https://github.com/juspay/hyperswitch/pull/6848)) ([`d19c1a1`](https://github.com/juspay/hyperswitch/commit/d19c1a19638d72ad9df12205bf9411c292d1ed6f)) + +**Full Changelog:** [`2024.12.27.0...2024.12.30.0`](https://github.com/juspay/hyperswitch/compare/2024.12.27.0...2024.12.30.0) + +- - - + ## 2024.12.26.1 From 227c274ece0e593df1fa9555da29e37c689c9452 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:39:16 +0530 Subject: [PATCH 06/30] feat(users): Add email domain based restriction for dashboard entry APIs (#6940) --- crates/api_models/src/user.rs | 18 +- .../src/query/user_authentication_method.rs | 14 ++ crates/diesel_models/src/schema.rs | 2 + crates/diesel_models/src/schema_v2.rs | 2 + .../src/user_authentication_method.rs | 13 ++ crates/router/src/core/errors/user.rs | 8 + crates/router/src/core/user.rs | 215 ++++++++++++++---- crates/router/src/db/kafka_store.rs | 12 + .../src/db/user_authentication_method.rs | 49 +++- crates/router/src/types/domain/user.rs | 9 + .../domain/user/user_authentication_method.rs | 1 + crates/router/src/utils/user.rs | 22 +- .../down.sql | 3 + .../up.sql | 6 + 14 files changed, 322 insertions(+), 52 deletions(-) create mode 100644 migrations/2024-12-11-092624_add-email-domain-in-auth-methods/down.sql create mode 100644 migrations/2024-12-11-092624_add-email-domain-in-auth-methods/up.sql diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 7b5911cf1a8..ea979bfe735 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -305,18 +305,26 @@ pub struct CreateUserAuthenticationMethodRequest { pub owner_type: common_enums::Owner, pub auth_method: AuthConfig, pub allow_signup: bool, + pub email_domain: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct UpdateUserAuthenticationMethodRequest { - pub id: String, - // TODO: When adding more fields make config and new fields option - pub auth_method: AuthConfig, +#[serde(rename_all = "snake_case")] +pub enum UpdateUserAuthenticationMethodRequest { + AuthMethod { + id: String, + auth_config: AuthConfig, + }, + EmailDomain { + owner_id: String, + email_domain: String, + }, } #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct GetUserAuthenticationMethodsRequest { - pub auth_id: String, + pub auth_id: Option, + pub email_domain: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/diesel_models/src/query/user_authentication_method.rs b/crates/diesel_models/src/query/user_authentication_method.rs index 14a28269cec..b9ed95e37c0 100644 --- a/crates/diesel_models/src/query/user_authentication_method.rs +++ b/crates/diesel_models/src/query/user_authentication_method.rs @@ -64,4 +64,18 @@ impl UserAuthenticationMethod { ) .await } + + pub async fn list_user_authentication_methods_for_email_domain( + conn: &PgPooledConn, + email_domain: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::email_domain.eq(email_domain.to_owned()), + None, + None, + Some(dsl::last_modified_at.asc()), + ) + .await + } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 95bb714cb71..37a39cb731a 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1405,6 +1405,8 @@ diesel::table! { allow_signup -> Bool, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + email_domain -> Varchar, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 8bbb4baf9d7..3e27f29427b 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1352,6 +1352,8 @@ diesel::table! { allow_signup -> Bool, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + email_domain -> Varchar, } } diff --git a/crates/diesel_models/src/user_authentication_method.rs b/crates/diesel_models/src/user_authentication_method.rs index 76e1abe7575..7f3799aa7bf 100644 --- a/crates/diesel_models/src/user_authentication_method.rs +++ b/crates/diesel_models/src/user_authentication_method.rs @@ -17,6 +17,7 @@ pub struct UserAuthenticationMethod { pub allow_signup: bool, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, + pub email_domain: String, } #[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -32,6 +33,7 @@ pub struct UserAuthenticationMethodNew { pub allow_signup: bool, pub created_at: PrimitiveDateTime, pub last_modified_at: PrimitiveDateTime, + pub email_domain: String, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] @@ -40,6 +42,7 @@ pub struct OrgAuthenticationMethodUpdateInternal { pub private_config: Option, pub public_config: Option, pub last_modified_at: PrimitiveDateTime, + pub email_domain: Option, } pub enum UserAuthenticationMethodUpdate { @@ -47,6 +50,9 @@ pub enum UserAuthenticationMethodUpdate { private_config: Option, public_config: Option, }, + EmailDomain { + email_domain: String, + }, } impl From for OrgAuthenticationMethodUpdateInternal { @@ -60,6 +66,13 @@ impl From for OrgAuthenticationMethodUpdateInter private_config, public_config, last_modified_at, + email_domain: None, + }, + UserAuthenticationMethodUpdate::EmailDomain { email_domain } => Self { + private_config: None, + public_config: None, + last_modified_at, + email_domain: Some(email_domain), }, } } diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index fa5f185ab82..6af269d916b 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -108,6 +108,8 @@ pub enum UserErrors { InvalidThemeLineage(String), #[error("Missing required field: email_config")] MissingEmailConfig, + #[error("Invalid Auth Method Operation: {0}")] + InvalidAuthMethodOperationWithMessage(String), } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -280,6 +282,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 56, self.get_error_message(), None)) } + Self::InvalidAuthMethodOperationWithMessage(_) => { + AER::BadRequest(ApiError::new(sub_code, 57, self.get_error_message(), None)) + } } } } @@ -347,6 +352,9 @@ impl UserErrors { format!("Invalid field: {} in lineage", field_name) } Self::MissingEmailConfig => "Missing required field: email_config".to_string(), + Self::InvalidAuthMethodOperationWithMessage(operation) => { + format!("Invalid Auth Method Operation: {}", operation) + } } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 19f257ef4f3..a7c60f33ff7 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -7,7 +7,7 @@ use api_models::{ payments::RedirectionResponse, user::{self as user_api, InviteMultipleUserResponse, NameIdUnit}, }; -use common_enums::EntityType; +use common_enums::{EntityType, UserAuthType}; use common_utils::{type_name, types::keymanager::Identifier}; #[cfg(feature = "email")] use diesel_models::user_role::UserRoleUpdate; @@ -22,6 +22,7 @@ use masking::{ExposeInterface, PeekInterface, Secret}; #[cfg(feature = "email")] use router_env::env; use router_env::logger; +use storage_impl::errors::StorageError; #[cfg(not(feature = "email"))] use user_api::dashboard_metadata::SetMetaDataRequest; @@ -152,6 +153,14 @@ pub async fn signup_token_only_flow( state: SessionState, request: user_api::SignUpRequest, ) -> UserResponse { + let user_email = domain::UserEmail::from_pii_email(request.email.clone())?; + utils::user::validate_email_domain_auth_type_using_db( + &state, + &user_email, + UserAuthType::Password, + ) + .await?; + let new_user = domain::NewUser::try_from(request)?; new_user .get_new_merchant() @@ -187,9 +196,18 @@ pub async fn signin_token_only_flow( state: SessionState, request: user_api::SignInRequest, ) -> UserResponse { + let user_email = domain::UserEmail::from_pii_email(request.email)?; + + utils::user::validate_email_domain_auth_type_using_db( + &state, + &user_email, + UserAuthType::Password, + ) + .await?; + let user_from_db: domain::UserFromStorage = state .global_store - .find_user_by_email(&domain::UserEmail::from_pii_email(request.email)?) + .find_user_by_email(&user_email) .await .to_not_found_response(UserErrors::InvalidCredentials)? .into(); @@ -215,10 +233,16 @@ pub async fn connect_account( auth_id: Option, theme_id: Option, ) -> UserResponse { - let find_user = state - .global_store - .find_user_by_email(&domain::UserEmail::from_pii_email(request.email.clone())?) - .await; + let user_email = domain::UserEmail::from_pii_email(request.email.clone())?; + + utils::user::validate_email_domain_auth_type_using_db( + &state, + &user_email, + UserAuthType::MagicLink, + ) + .await?; + + let find_user = state.global_store.find_user_by_email(&user_email).await; if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); @@ -412,6 +436,13 @@ pub async fn forgot_password( ) -> UserResponse<()> { let user_email = domain::UserEmail::from_pii_email(request.email)?; + utils::user::validate_email_domain_auth_type_using_db( + &state, + &user_email, + UserAuthType::Password, + ) + .await?; + let user_from_db = state .global_store .find_user_by_email(&user_email) @@ -1757,7 +1788,15 @@ pub async fn send_verification_mail( auth_id: Option, theme_id: Option, ) -> UserResponse<()> { - let user_email = domain::UserEmail::try_from(req.email)?; + let user_email = domain::UserEmail::from_pii_email(req.email)?; + + utils::user::validate_email_domain_auth_type_using_db( + &state, + &user_email, + UserAuthType::MagicLink, + ) + .await?; + let user = state .global_store .find_user_by_email(&user_email) @@ -2317,10 +2356,30 @@ pub async fn create_user_authentication_method( .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get list of auth methods for the owner id")?; - let auth_id = auth_methods - .first() - .map(|auth_method| auth_method.auth_id.clone()) - .unwrap_or(uuid::Uuid::new_v4().to_string()); + let (auth_id, email_domain) = if let Some(auth_method) = auth_methods.first() { + let email_domain = match req.email_domain { + Some(email_domain) => { + if email_domain != auth_method.email_domain { + return Err(report!(UserErrors::InvalidAuthMethodOperationWithMessage( + "Email domain mismatch".to_string() + ))); + } + + email_domain + } + None => auth_method.email_domain.clone(), + }; + + (auth_method.auth_id.clone(), email_domain) + } else { + let email_domain = + req.email_domain + .ok_or(UserErrors::InvalidAuthMethodOperationWithMessage( + "Email domain not found".to_string(), + ))?; + + (uuid::Uuid::new_v4().to_string(), email_domain) + }; for db_auth_method in auth_methods { let is_type_same = db_auth_method.auth_type == (&req.auth_method).foreign_into(); @@ -2360,6 +2419,7 @@ pub async fn create_user_authentication_method( allow_signup: req.allow_signup, created_at: now, last_modified_at: now, + email_domain, }) .await .to_duplicate_response(UserErrors::UserAuthMethodAlreadyExists)?; @@ -2383,25 +2443,71 @@ pub async fn update_user_authentication_method( .change_context(UserErrors::InternalServerError) .attach_printable("Failed to decode DEK")?; - let (private_config, public_config) = utils::user::construct_public_and_private_db_configs( - &state, - &req.auth_method, - &user_auth_encryption_key, - req.id.clone(), - ) - .await?; + match req { + user_api::UpdateUserAuthenticationMethodRequest::AuthMethod { + id, + auth_config: auth_method, + } => { + let (private_config, public_config) = + utils::user::construct_public_and_private_db_configs( + &state, + &auth_method, + &user_auth_encryption_key, + id.clone(), + ) + .await?; + + state + .store + .update_user_authentication_method( + &id, + UserAuthenticationMethodUpdate::UpdateConfig { + private_config, + public_config, + }, + ) + .await + .map_err(|error| { + let user_error = match error.current_context() { + StorageError::ValueNotFound(_) => { + UserErrors::InvalidAuthMethodOperationWithMessage( + "Auth method not found".to_string(), + ) + } + StorageError::DuplicateValue { .. } => { + UserErrors::UserAuthMethodAlreadyExists + } + _ => UserErrors::InternalServerError, + }; + error.change_context(user_error) + })?; + } + user_api::UpdateUserAuthenticationMethodRequest::EmailDomain { + owner_id, + email_domain, + } => { + let auth_methods = state + .store + .list_user_authentication_methods_for_owner_id(&owner_id) + .await + .change_context(UserErrors::InternalServerError)?; + + futures::future::try_join_all(auth_methods.iter().map(|auth_method| async { + state + .store + .update_user_authentication_method( + &auth_method.id, + UserAuthenticationMethodUpdate::EmailDomain { + email_domain: email_domain.clone(), + }, + ) + .await + .to_duplicate_response(UserErrors::UserAuthMethodAlreadyExists) + })) + .await?; + } + } - state - .store - .update_user_authentication_method( - &req.id, - UserAuthenticationMethodUpdate::UpdateConfig { - private_config, - public_config, - }, - ) - .await - .change_context(UserErrors::InvalidUserAuthMethodOperation)?; Ok(ApplicationResponse::StatusOk) } @@ -2409,18 +2515,28 @@ pub async fn list_user_authentication_methods( state: SessionState, req: user_api::GetUserAuthenticationMethodsRequest, ) -> UserResponse> { - let user_authentication_methods = state - .store - .list_user_authentication_methods_for_auth_id(&req.auth_id) - .await - .change_context(UserErrors::InternalServerError)?; + let user_authentication_methods = match (req.auth_id, req.email_domain) { + (Some(auth_id), None) => state + .store + .list_user_authentication_methods_for_auth_id(&auth_id) + .await + .change_context(UserErrors::InternalServerError)?, + (None, Some(email_domain)) => state + .store + .list_user_authentication_methods_for_email_domain(&email_domain) + .await + .change_context(UserErrors::InternalServerError)?, + (Some(_), Some(_)) | (None, None) => { + return Err(UserErrors::InvalidUserAuthMethodOperation.into()); + } + }; Ok(ApplicationResponse::Json( user_authentication_methods .into_iter() .map(|auth_method| { let auth_name = match (auth_method.auth_type, auth_method.public_config) { - (common_enums::UserAuthType::OpenIdConnect, config) => { + (UserAuthType::OpenIdConnect, config) => { let open_id_public_config: Option = config .map(|config| { @@ -2546,6 +2662,13 @@ pub async fn sso_sign( ) .await?; + utils::user::validate_email_domain_auth_type_using_db( + &state, + &email, + UserAuthType::OpenIdConnect, + ) + .await?; + // TODO: Use config to handle not found error let user_from_db: domain::UserFromStorage = state .global_store @@ -2594,14 +2717,20 @@ pub async fn terminate_auth_select( .change_context(UserErrors::InternalServerError)? .into(); - let user_authentication_method = if let Some(id) = &req.id { - state - .store - .get_user_authentication_method_by_id(id) - .await - .to_not_found_response(UserErrors::InvalidUserAuthMethodOperation)? - } else { - DEFAULT_USER_AUTH_METHOD.clone() + let user_email = domain::UserEmail::from_pii_email(user_from_db.get_email())?; + let auth_methods = state + .store + .list_user_authentication_methods_for_email_domain(user_email.extract_domain()?) + .await + .change_context(UserErrors::InternalServerError)?; + + let user_authentication_method = match (req.id, auth_methods.is_empty()) { + (Some(id), _) => auth_methods + .into_iter() + .find(|auth_method| auth_method.id == id) + .ok_or(UserErrors::InvalidUserAuthMethodOperation)?, + (None, true) => DEFAULT_USER_AUTH_METHOD.clone(), + (None, false) => return Err(UserErrors::InvalidUserAuthMethodOperation.into()), }; let current_flow = domain::CurrentFlow::new(user_token, domain::SPTFlow::AuthSelect.into())?; diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 525c5f12dc4..8eec7f04169 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3816,6 +3816,18 @@ impl UserAuthenticationMethodInterface for KafkaStore { .update_user_authentication_method(id, user_authentication_method_update) .await } + + async fn list_user_authentication_methods_for_email_domain( + &self, + email_domain: &str, + ) -> CustomResult< + Vec, + errors::StorageError, + > { + self.diesel_store + .list_user_authentication_methods_for_email_domain(email_domain) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/user_authentication_method.rs b/crates/router/src/db/user_authentication_method.rs index a02e7bdb11f..cd918fe2060 100644 --- a/crates/router/src/db/user_authentication_method.rs +++ b/crates/router/src/db/user_authentication_method.rs @@ -36,6 +36,11 @@ pub trait UserAuthenticationMethodInterface { id: &str, user_authentication_method_update: storage::UserAuthenticationMethodUpdate, ) -> CustomResult; + + async fn list_user_authentication_methods_for_email_domain( + &self, + email_domain: &str, + ) -> CustomResult, errors::StorageError>; } #[async_trait::async_trait] @@ -57,7 +62,7 @@ impl UserAuthenticationMethodInterface for Store { &self, id: &str, ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; + let conn = connection::pg_connection_read(self).await?; storage::UserAuthenticationMethod::get_user_authentication_method_by_id(&conn, id) .await .map_err(|error| report!(errors::StorageError::from(error))) @@ -68,7 +73,7 @@ impl UserAuthenticationMethodInterface for Store { &self, auth_id: &str, ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_connection_write(self).await?; + let conn = connection::pg_connection_read(self).await?; storage::UserAuthenticationMethod::list_user_authentication_methods_for_auth_id( &conn, auth_id, ) @@ -81,7 +86,7 @@ impl UserAuthenticationMethodInterface for Store { &self, owner_id: &str, ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_connection_write(self).await?; + let conn = connection::pg_connection_read(self).await?; storage::UserAuthenticationMethod::list_user_authentication_methods_for_owner_id( &conn, owner_id, ) @@ -104,6 +109,20 @@ impl UserAuthenticationMethodInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error))) } + + #[instrument(skip_all)] + async fn list_user_authentication_methods_for_email_domain( + &self, + email_domain: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::UserAuthenticationMethod::list_user_authentication_methods_for_email_domain( + &conn, + email_domain, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } } #[async_trait::async_trait] @@ -130,6 +149,7 @@ impl UserAuthenticationMethodInterface for MockDb { allow_signup: user_authentication_method.allow_signup, created_at: user_authentication_method.created_at, last_modified_at: user_authentication_method.last_modified_at, + email_domain: user_authentication_method.email_domain, }; user_authentication_methods.push(user_authentication_method.clone()); @@ -222,6 +242,13 @@ impl UserAuthenticationMethodInterface for MockDb { last_modified_at: common_utils::date_time::now(), ..auth_method_inner.to_owned() }, + storage::UserAuthenticationMethodUpdate::EmailDomain { email_domain } => { + storage::UserAuthenticationMethod { + email_domain: email_domain.to_owned(), + last_modified_at: common_utils::date_time::now(), + ..auth_method_inner.to_owned() + } + } }; auth_method_inner.to_owned() }) @@ -232,4 +259,20 @@ impl UserAuthenticationMethodInterface for MockDb { .into(), ) } + + #[instrument(skip_all)] + async fn list_user_authentication_methods_for_email_domain( + &self, + email_domain: &str, + ) -> CustomResult, errors::StorageError> { + let user_authentication_methods = self.user_authentication_methods.lock().await; + + let user_authentication_methods_list: Vec<_> = user_authentication_methods + .iter() + .filter(|auth_method_inner| auth_method_inner.email_domain == email_domain) + .cloned() + .collect(); + + Ok(user_authentication_methods_list) + } } diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 6efaac7bfed..569e7f9a99f 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -138,6 +138,15 @@ impl UserEmail { pub fn get_secret(self) -> Secret { (*self.0).clone() } + + pub fn extract_domain(&self) -> UserResult<&str> { + let (_username, domain) = self + .peek() + .split_once('@') + .ok_or(UserErrors::InternalServerError)?; + + Ok(domain) + } } impl TryFrom for UserEmail { diff --git a/crates/router/src/types/domain/user/user_authentication_method.rs b/crates/router/src/types/domain/user/user_authentication_method.rs index 570e144961a..29c588f15e1 100644 --- a/crates/router/src/types/domain/user/user_authentication_method.rs +++ b/crates/router/src/types/domain/user/user_authentication_method.rs @@ -14,4 +14,5 @@ pub static DEFAULT_USER_AUTH_METHOD: Lazy = allow_signup: true, created_at: common_utils::date_time::now(), last_modified_at: common_utils::date_time::now(), + email_domain: String::from("hyperswitch"), }); diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index abd59684243..b8ffcf836d7 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -5,7 +5,7 @@ use common_enums::UserAuthType; use common_utils::{ encryption::Encryption, errors::CustomResult, id_type, type_name, types::keymanager::Identifier, }; -use diesel_models::{organization, organization::OrganizationBridge}; +use diesel_models::organization::{self, OrganizationBridge}; use error_stack::ResultExt; use masking::{ExposeInterface, Secret}; use redis_interface::RedisConnectionPool; @@ -312,3 +312,23 @@ pub fn create_merchant_account_request_for_org( pm_collect_link_config: None, }) } + +pub async fn validate_email_domain_auth_type_using_db( + state: &SessionState, + email: &domain::UserEmail, + required_auth_type: UserAuthType, +) -> UserResult<()> { + let domain = email.extract_domain()?; + let user_auth_methods = state + .store + .list_user_authentication_methods_for_email_domain(domain) + .await + .change_context(UserErrors::InternalServerError)?; + + (user_auth_methods.is_empty() + || user_auth_methods + .iter() + .any(|auth_method| auth_method.auth_type == required_auth_type)) + .then_some(()) + .ok_or(UserErrors::InvalidUserAuthMethodOperation.into()) +} diff --git a/migrations/2024-12-11-092624_add-email-domain-in-auth-methods/down.sql b/migrations/2024-12-11-092624_add-email-domain-in-auth-methods/down.sql new file mode 100644 index 00000000000..9f3560069c7 --- /dev/null +++ b/migrations/2024-12-11-092624_add-email-domain-in-auth-methods/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +DROP INDEX email_domain_index; +ALTER TABLE user_authentication_methods DROP COLUMN email_domain; diff --git a/migrations/2024-12-11-092624_add-email-domain-in-auth-methods/up.sql b/migrations/2024-12-11-092624_add-email-domain-in-auth-methods/up.sql new file mode 100644 index 00000000000..831f10162ca --- /dev/null +++ b/migrations/2024-12-11-092624_add-email-domain-in-auth-methods/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +ALTER TABLE user_authentication_methods ADD COLUMN email_domain VARCHAR(64); +UPDATE user_authentication_methods SET email_domain = auth_id WHERE email_domain IS NULL; +ALTER TABLE user_authentication_methods ALTER COLUMN email_domain SET NOT NULL; + +CREATE INDEX email_domain_index ON user_authentication_methods (email_domain); From 849fbbf71b6332d30c3a955d2cc33b16dcd3e727 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Mon, 30 Dec 2024 12:57:09 +0530 Subject: [PATCH 07/30] refactor(dynamic_routing): add non_deterministic value in SuccessBasedRoutingConclusiveState type (#6878) --- .../down.sql | 2 ++ .../up.sql | 12 ++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/down.sql create mode 100644 migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/up.sql diff --git a/migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/down.sql b/migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/down.sql new file mode 100644 index 00000000000..2a3866c86d4 --- /dev/null +++ b/migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; diff --git a/migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/up.sql b/migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/up.sql new file mode 100644 index 00000000000..9e4a259c7ae --- /dev/null +++ b/migrations/2024-12-18-124527_add_new_value_in_success_based_routing_conclusive_state/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_enum + WHERE enumlabel = 'non_deterministic' + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'SuccessBasedRoutingConclusiveState') + ) THEN + ALTER TYPE "SuccessBasedRoutingConclusiveState" ADD VALUE 'non_deterministic'; + END IF; +END $$; From 8092c1fd8fbe115ce7b3f4329758800e506bccae Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Mon, 30 Dec 2024 12:58:00 +0530 Subject: [PATCH 08/30] refactor(dynamic_routing): remove `tenant-id` prefixed in `id` field of dynamic routing grpc requests (#6949) --- crates/router/src/core/payments/routing.rs | 7 +------ crates/router/src/core/routing.rs | 6 +----- crates/router/src/core/routing/helpers.rs | 17 ++--------------- 3 files changed, 4 insertions(+), 26 deletions(-) diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 0a57819816f..8f3f40edf06 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -1347,14 +1347,9 @@ pub async fn perform_success_based_routing( .ok_or(errors::RoutingError::SuccessBasedRoutingParamsNotFoundError)?, ); - let tenant_business_profile_id = routing::helpers::generate_tenant_business_profile_id( - &state.tenant.redis_key_prefix, - business_profile.get_id().get_string_repr(), - ); - let success_based_connectors: CalSuccessRateResponse = client .calculate_success_rate( - tenant_business_profile_id, + business_profile.get_id().get_string_repr().into(), success_based_routing_configs, success_based_routing_config_params, routable_connectors, diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 5fc54fe96e5..717dfd0c6eb 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -1397,10 +1397,6 @@ pub async fn success_based_routing_update_configs( router_env::metric_attributes!(("profile_id", profile_id.clone())), ); - let prefix_of_dynamic_routing_keys = helpers::generate_tenant_business_profile_id( - &state.tenant.redis_key_prefix, - profile_id.get_string_repr(), - ); state .grpc_client .dynamic_routing @@ -1409,7 +1405,7 @@ pub async fn success_based_routing_update_configs( .async_map(|sr_client| async { sr_client .invalidate_success_rate_routing_keys( - prefix_of_dynamic_routing_keys, + profile_id.get_string_repr().into(), state.get_grpc_headers(), ) .await diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 489c3122a8e..0d66c3b6f17 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -699,11 +699,6 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("unable to retrieve success_rate based dynamic routing configs")?; - let tenant_business_profile_id = generate_tenant_business_profile_id( - &state.tenant.redis_key_prefix, - business_profile.get_id().get_string_repr(), - ); - let success_based_routing_config_params = success_based_routing_config_params_interpolator .get_string_val( success_based_routing_configs @@ -715,7 +710,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( let success_based_connectors = client .calculate_success_rate( - tenant_business_profile_id.clone(), + business_profile.get_id().get_string_repr().into(), success_based_routing_configs.clone(), success_based_routing_config_params.clone(), routable_connectors.clone(), @@ -841,7 +836,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( client .update_success_rate( - tenant_business_profile_id, + business_profile.get_id().get_string_repr().into(), success_based_routing_configs, success_based_routing_config_params, vec![routing_types::RoutableConnectorChoiceWithStatus::new( @@ -936,14 +931,6 @@ fn get_success_based_metrics_outcome_for_payment( } } -/// generates cache key with tenant's redis key prefix and profile_id -pub fn generate_tenant_business_profile_id( - redis_key_prefix: &str, - business_profile_id: &str, -) -> String { - format!("{}:{}", redis_key_prefix, business_profile_id) -} - #[cfg(all(feature = "v1", feature = "dynamic_routing"))] pub async fn disable_dynamic_routing_algorithm( state: &SessionState, From 7d00583a8076ef2f996345549b5e81c7f90361dc Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 00:26:29 +0000 Subject: [PATCH 09/30] chore(version): 2024.12.31.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b238cf58c2a..3de8bcfeb93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.31.0 + +### Features + +- **users:** Add email domain based restriction for dashboard entry APIs ([#6940](https://github.com/juspay/hyperswitch/pull/6940)) ([`227c274`](https://github.com/juspay/hyperswitch/commit/227c274ece0e593df1fa9555da29e37c689c9452)) + +### Refactors + +- **dynamic_routing:** + - Add non_deterministic value in SuccessBasedRoutingConclusiveState type ([#6878](https://github.com/juspay/hyperswitch/pull/6878)) ([`849fbbf`](https://github.com/juspay/hyperswitch/commit/849fbbf71b6332d30c3a955d2cc33b16dcd3e727)) + - Remove `tenant-id` prefixed in `id` field of dynamic routing grpc requests ([#6949](https://github.com/juspay/hyperswitch/pull/6949)) ([`8092c1f`](https://github.com/juspay/hyperswitch/commit/8092c1fd8fbe115ce7b3f4329758800e506bccae)) + +**Full Changelog:** [`2024.12.30.0...2024.12.31.0`](https://github.com/juspay/hyperswitch/compare/2024.12.30.0...2024.12.31.0) + +- - - + ## 2024.12.30.0 ### Features From fce5ffa4e06bc6b8e413b13ec550613617e05568 Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Fri, 3 Jan 2025 13:20:45 +0530 Subject: [PATCH 10/30] fix(cache): address in-memory cache invalidation using global tenant as `key_prefix` (#6976) --- crates/redis_interface/src/commands.rs | 15 ++-- crates/router/src/core/admin.rs | 2 +- crates/router/src/core/cache.rs | 4 +- crates/router/src/core/routing.rs | 2 +- crates/router/src/core/routing/helpers.rs | 6 +- crates/router/src/db/configs.rs | 33 ++++----- crates/router/src/db/merchant_account.rs | 4 +- crates/storage_impl/src/redis/cache.rs | 85 +++++++++++++---------- crates/storage_impl/src/redis/pub_sub.rs | 5 -- 9 files changed, 73 insertions(+), 83 deletions(-) diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index 19497d6fbb8..3c7ffa16ada 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -211,18 +211,13 @@ impl super::RedisConnectionPool { #[instrument(level = "DEBUG", skip(self))] pub async fn delete_multiple_keys( &self, - keys: Vec, + keys: &[String], ) -> CustomResult, errors::RedisError> { - let mut del_result = Vec::with_capacity(keys.len()); + let futures = keys.iter().map(|key| self.pool.del(self.add_prefix(key))); - for key in keys { - del_result.push( - self.pool - .del(self.add_prefix(&key)) - .await - .change_context(errors::RedisError::DeleteFailed)?, - ); - } + let del_result = futures::future::try_join_all(futures) + .await + .change_context(errors::RedisError::DeleteFailed)?; Ok(del_result) } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 24a2b25d6ce..19c3e6c1e22 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -4288,7 +4288,7 @@ impl ProfileWrapper { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in business profile")?; - storage_impl::redis::cache::publish_into_redact_channel( + storage_impl::redis::cache::redact_from_redis_and_publish( db.get_cache_store().as_ref(), [routing_cache_key], ) diff --git a/crates/router/src/core/cache.rs b/crates/router/src/core/cache.rs index 8cda60cf700..fbe75f3c9c0 100644 --- a/crates/router/src/core/cache.rs +++ b/crates/router/src/core/cache.rs @@ -1,6 +1,6 @@ use common_utils::errors::CustomResult; use error_stack::{report, ResultExt}; -use storage_impl::redis::cache::{publish_into_redact_channel, CacheKind}; +use storage_impl::redis::cache::{redact_from_redis_and_publish, CacheKind}; use super::errors; use crate::{routes::SessionState, services}; @@ -10,7 +10,7 @@ pub async fn invalidate( key: &str, ) -> CustomResult, errors::ApiErrorResponse> { let store = state.store.as_ref(); - let result = publish_into_redact_channel( + let result = redact_from_redis_and_publish( store.get_cache_store().as_ref(), [CacheKind::All(key.into())], ) diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index 717dfd0c6eb..99bd2b00209 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -1383,7 +1383,7 @@ pub async fn success_based_routing_update_configs( let cache_entries_to_redact = vec![cache::CacheKind::SuccessBasedDynamicRoutingCache( cache_key.into(), )]; - let _ = cache::publish_into_redact_channel( + let _ = cache::redact_from_redis_and_publish( state.store.get_cache_store().as_ref(), cache_entries_to_redact, ) diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 0d66c3b6f17..159def38621 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -189,7 +189,7 @@ pub async fn update_merchant_active_algorithm_ref( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in merchant account")?; - cache::publish_into_redact_channel(db.get_cache_store().as_ref(), [config_key]) + cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [config_key]) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to invalidate the config cache")?; @@ -256,7 +256,7 @@ pub async fn update_profile_active_algorithm_ref( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to update routing algorithm ref in business profile")?; - cache::publish_into_redact_channel(db.get_cache_store().as_ref(), [routing_cache_key]) + cache::redact_from_redis_and_publish(db.get_cache_store().as_ref(), [routing_cache_key]) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to invalidate routing cache")?; @@ -1031,7 +1031,7 @@ pub async fn disable_dynamic_routing_algorithm( }; // redact cache for dynamic routing config - let _ = cache::publish_into_redact_channel( + let _ = cache::redact_from_redis_and_publish( state.store.get_cache_store().as_ref(), cache_entries_to_redact, ) diff --git a/crates/router/src/db/configs.rs b/crates/router/src/db/configs.rs index 575481793ca..9b8ab5231b6 100644 --- a/crates/router/src/db/configs.rs +++ b/crates/router/src/db/configs.rs @@ -1,16 +1,13 @@ use diesel_models::configs::ConfigUpdateInternal; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; -use storage_impl::redis::{ - cache::{self, CacheKind, CONFIG_CACHE}, - kv_store::RedisConnInterface, - pub_sub::PubSubInterface, -}; +use storage_impl::redis::cache::{self, CacheKind, CONFIG_CACHE}; use super::{MockDb, Store}; use crate::{ connection, core::errors::{self, CustomResult}, + db::StorageInterface, types::storage, }; @@ -69,14 +66,11 @@ impl ConfigInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error)))?; - self.get_redis_conn() - .map_err(Into::::into)? - .publish( - cache::IMC_INVALIDATION_CHANNEL, - CacheKind::Config((&inserted.key).into()), - ) - .await - .map_err(Into::::into)?; + cache::redact_from_redis_and_publish( + self.get_cache_store().as_ref(), + [CacheKind::Config((&inserted.key).into())], + ) + .await?; Ok(inserted) } @@ -177,14 +171,11 @@ impl ConfigInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error)))?; - self.get_redis_conn() - .map_err(Into::::into)? - .publish( - cache::IMC_INVALIDATION_CHANNEL, - CacheKind::Config(key.into()), - ) - .await - .map_err(Into::::into)?; + cache::redact_from_redis_and_publish( + self.get_cache_store().as_ref(), + [CacheKind::Config((&deleted.key).into())], + ) + .await?; Ok(deleted) } diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 4f4a3f1cf00..1c104b22489 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -801,7 +801,7 @@ async fn publish_and_redact_merchant_account_cache( cache_keys.extend(publishable_key.into_iter()); cache_keys.extend(cgraph_key.into_iter()); - cache::publish_into_redact_channel(store.get_cache_store().as_ref(), cache_keys).await?; + cache::redact_from_redis_and_publish(store.get_cache_store().as_ref(), cache_keys).await?; Ok(()) } @@ -822,6 +822,6 @@ async fn publish_and_redact_all_merchant_account_cache( .map(|s| CacheKind::Accounts(s.into())) .collect(); - cache::publish_into_redact_channel(store.get_cache_store().as_ref(), cache_keys).await?; + cache::redact_from_redis_and_publish(store.get_cache_store().as_ref(), cache_keys).await?; Ok(()) } diff --git a/crates/storage_impl/src/redis/cache.rs b/crates/storage_impl/src/redis/cache.rs index 93255fac914..323d3d6df25 100644 --- a/crates/storage_impl/src/redis/cache.rs +++ b/crates/storage_impl/src/redis/cache.rs @@ -2,14 +2,17 @@ use std::{any::Any, borrow::Cow, fmt::Debug, sync::Arc}; use common_utils::{ errors::{self, CustomResult}, - ext_traits::{AsyncExt, ByteSliceExt}, + ext_traits::ByteSliceExt, }; use dyn_clone::DynClone; use error_stack::{Report, ResultExt}; use moka::future::Cache as MokaCache; use once_cell::sync::Lazy; use redis_interface::{errors::RedisError, RedisConnectionPool, RedisValue}; -use router_env::tracing::{self, instrument}; +use router_env::{ + logger, + tracing::{self, instrument}, +}; use crate::{ errors::StorageError, @@ -100,7 +103,7 @@ pub struct CacheRedact<'a> { pub kind: CacheKind<'a>, } -#[derive(serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum CacheKind<'a> { Config(Cow<'a, str>), Accounts(Cow<'a, str>), @@ -114,6 +117,23 @@ pub enum CacheKind<'a> { All(Cow<'a, str>), } +impl CacheKind<'_> { + pub(crate) fn get_key_without_prefix(&self) -> &str { + match self { + CacheKind::Config(key) + | CacheKind::Accounts(key) + | CacheKind::Routing(key) + | CacheKind::DecisionManager(key) + | CacheKind::Surcharge(key) + | CacheKind::CGraph(key) + | CacheKind::SuccessBasedDynamicRoutingCache(key) + | CacheKind::EliminationBasedDynamicRoutingCache(key) + | CacheKind::PmFiltersCGraph(key) + | CacheKind::All(key) => key, + } + } +} + impl<'a> TryFrom> for RedisValue { type Error = Report; fn try_from(v: CacheRedact<'a>) -> Result { @@ -343,48 +363,37 @@ where } #[instrument(skip_all)] -pub async fn redact_cache( +pub async fn redact_from_redis_and_publish< + 'a, + K: IntoIterator> + Send + Clone, +>( store: &(dyn RedisConnInterface + Send + Sync), - key: &'static str, - fun: F, - in_memory: Option<&Cache>, -) -> CustomResult -where - F: FnOnce() -> Fut + Send, - Fut: futures::Future> + Send, -{ - let data = fun().await?; - + keys: K, +) -> CustomResult { let redis_conn = store .get_redis_conn() .change_context(StorageError::RedisError( RedisError::RedisConnectionError.into(), )) .attach_printable("Failed to get redis connection")?; - let tenant_key = CacheKey { - key: key.to_string(), - prefix: redis_conn.key_prefix.clone(), - }; - in_memory.async_map(|cache| cache.remove(tenant_key)).await; - redis_conn - .delete_key(key) + let redis_keys_to_be_deleted = keys + .clone() + .into_iter() + .map(|val| val.get_key_without_prefix().to_owned()) + .collect::>(); + + let del_replies = redis_conn + .delete_multiple_keys(&redis_keys_to_be_deleted) .await - .change_context(StorageError::KVError)?; - Ok(data) -} + .map_err(StorageError::RedisError)?; -#[instrument(skip_all)] -pub async fn publish_into_redact_channel<'a, K: IntoIterator> + Send>( - store: &(dyn RedisConnInterface + Send + Sync), - keys: K, -) -> CustomResult { - let redis_conn = store - .get_redis_conn() - .change_context(StorageError::RedisError( - RedisError::RedisConnectionError.into(), - )) - .attach_printable("Failed to get redis connection")?; + let deletion_result = redis_keys_to_be_deleted + .into_iter() + .zip(del_replies) + .collect::>(); + + logger::debug!(redis_deletion_result=?deletion_result); let futures = keys.into_iter().map(|key| async { redis_conn @@ -411,7 +420,7 @@ where Fut: futures::Future> + Send, { let data = fun().await?; - publish_into_redact_channel(store, [key]).await?; + redact_from_redis_and_publish(store, [key]).await?; Ok(data) } @@ -424,10 +433,10 @@ pub async fn publish_and_redact_multiple<'a, T, F, Fut, K>( where F: FnOnce() -> Fut + Send, Fut: futures::Future> + Send, - K: IntoIterator> + Send, + K: IntoIterator> + Send + Clone, { let data = fun().await?; - publish_into_redact_channel(store, keys).await?; + redact_from_redis_and_publish(store, keys).await?; Ok(data) } diff --git a/crates/storage_impl/src/redis/pub_sub.rs b/crates/storage_impl/src/redis/pub_sub.rs index 42ad2ae0795..373ac370e2f 100644 --- a/crates/storage_impl/src/redis/pub_sub.rs +++ b/crates/storage_impl/src/redis/pub_sub.rs @@ -243,11 +243,6 @@ impl PubSubInterface for std::sync::Arc { } }; - self.delete_key(key.as_ref()) - .await - .map_err(|err| logger::error!("Error while deleting redis key: {err:?}")) - .ok(); - logger::debug!( key_prefix=?message.tenant.clone(), channel_name=?channel_name, From 2aa14e7fec19b31d84745b524cbf835ff16b8ce8 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 3 Jan 2025 08:26:26 +0000 Subject: [PATCH 11/30] chore(version): 2025.01.03.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3de8bcfeb93..74578ee9b40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.03.0 + +### Bug Fixes + +- **cache:** Address in-memory cache invalidation using global tenant as `key_prefix` ([#6976](https://github.com/juspay/hyperswitch/pull/6976)) ([`fce5ffa`](https://github.com/juspay/hyperswitch/commit/fce5ffa4e06bc6b8e413b13ec550613617e05568)) + +**Full Changelog:** [`2024.12.31.0...2025.01.03.0`](https://github.com/juspay/hyperswitch/compare/2024.12.31.0...2025.01.03.0) + +- - - + ## 2024.12.31.0 ### Features From 60ed69c1cff706aaba248e1aba0219f70bb679bd Mon Sep 17 00:00:00 2001 From: Kashif Date: Fri, 3 Jan 2025 16:23:23 +0530 Subject: [PATCH 12/30] chore: add migrations for Currency type in DB (#6980) --- .../2025-01-03-084904_add_currencies/down.sql | 1 + .../2025-01-03-084904_add_currencies/up.sql | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 migrations/2025-01-03-084904_add_currencies/down.sql create mode 100644 migrations/2025-01-03-084904_add_currencies/up.sql diff --git a/migrations/2025-01-03-084904_add_currencies/down.sql b/migrations/2025-01-03-084904_add_currencies/down.sql new file mode 100644 index 00000000000..e0ac49d1ecf --- /dev/null +++ b/migrations/2025-01-03-084904_add_currencies/down.sql @@ -0,0 +1 @@ +SELECT 1; diff --git a/migrations/2025-01-03-084904_add_currencies/up.sql b/migrations/2025-01-03-084904_add_currencies/up.sql new file mode 100644 index 00000000000..14167a32cfc --- /dev/null +++ b/migrations/2025-01-03-084904_add_currencies/up.sql @@ -0,0 +1,18 @@ +DO $$ + DECLARE currency TEXT; + BEGIN + FOR currency IN + SELECT + unnest( + ARRAY ['AFN', 'BTN', 'CDF', 'ERN', 'IRR', 'ISK', 'KPW', 'SDG', 'SYP', 'TJS', 'TMT', 'ZWL'] + ) AS currency + LOOP + IF NOT EXISTS ( + SELECT 1 + FROM pg_enum + WHERE enumlabel = currency + AND enumtypid = (SELECT oid FROM pg_type WHERE typname = 'Currency') + ) THEN EXECUTE format('ALTER TYPE "Currency" ADD VALUE %L', currency); + END IF; + END LOOP; +END $$; \ No newline at end of file From 8c4cd07ea69395bffdb8c8ddf1313dc87a5dc740 Mon Sep 17 00:00:00 2001 From: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> Date: Sat, 4 Jan 2025 01:00:45 +0530 Subject: [PATCH 13/30] ci(cypress): fix adyen sofort in cypress (#6984) --- .../cypress/e2e/PaymentUtils/Adyen.js | 4 +++- .../cypress/e2e/PaymentUtils/Iatapay.js | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js b/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js index 56efbf1f6f2..95fa82f9675 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js @@ -909,7 +909,9 @@ export const connectorDetails = { Response: { status: 200, body: { - status: "requires_customer_action", + status: "failed", + error_code: "14_006", + error_message: "Required object 'paymentMethod' is not provided.", }, }, }, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js b/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js index faa810e7bad..47772896955 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js @@ -88,6 +88,27 @@ export const connectorDetails = { }, }, }, + No3DSFailPayment: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + customer_acceptance: null, + setup_future_usage: "on_session", + }, + Response: { + status: 400, + body: { + error: { + type: "invalid_request", + message: + "Selected payment method through iatapay is not implemented", + code: "IR_00", + }, + }, + }, + }, }, upi_pm: { PaymentIntent: { From de2f209ae687d79fb7bf2575dcaa612718d1d1aa Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 00:32:33 +0000 Subject: [PATCH 14/30] chore(version): 2025.01.06.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74578ee9b40..ee0640a5641 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.06.0 + +### Miscellaneous Tasks + +- Add migrations for Currency type in DB ([#6980](https://github.com/juspay/hyperswitch/pull/6980)) ([`60ed69c`](https://github.com/juspay/hyperswitch/commit/60ed69c1cff706aaba248e1aba0219f70bb679bd)) + +**Full Changelog:** [`2025.01.03.0...2025.01.06.0`](https://github.com/juspay/hyperswitch/compare/2025.01.03.0...2025.01.06.0) + +- - - + ## 2025.01.03.0 ### Bug Fixes From 638e1f230a543a1ff2b7712d04b937a9a9db1969 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Mon, 6 Jan 2025 12:29:53 +0530 Subject: [PATCH 15/30] Ci(Cypress): Add PML test and Dynamic Fields Test for Novalnet (#6544) --- .../00000-PaymentMethodListTests.cy.js | 211 ++++++++++++++++-- .../e2e/PaymentMethodListUtils/Commons.js | 20 ++ .../{Stripe.js => Connector.js} | 0 .../e2e/PaymentMethodListUtils/Utils.js | 4 +- .../cypress/e2e/PaymentUtils/Novalnet.js | 163 ++++++++++++++ 5 files changed, 382 insertions(+), 16 deletions(-) rename cypress-tests/cypress/e2e/PaymentMethodListUtils/{Stripe.js => Connector.js} (100%) diff --git a/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js b/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js index 89b27a6b271..0f754831d46 100644 --- a/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js +++ b/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js @@ -6,6 +6,7 @@ import { cardCreditEnabled, cardCreditEnabledInUs, cardCreditEnabledInUsd, + cardCreditEnabledInEur, createPaymentBodyWithCurrency, createPaymentBodyWithCurrencyCountry, } from "../PaymentMethodListUtils/Commons"; @@ -68,7 +69,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as EUR and no billing address it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -88,7 +90,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have ideal with stripe it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithStripeForIdeal" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -151,7 +153,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as INR and no billing address it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -171,7 +174,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have ideal with stripe it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListNull" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -234,7 +237,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as USD and billing address as US it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -254,7 +258,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have credit with Stripe and Cybersource it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithCreditTwoConnector" ]; cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit( @@ -317,7 +321,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as EUR and billing address as US it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -337,7 +342,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which shouldn't have anything it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListNull" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -402,7 +407,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as USD and billing address as IN it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -422,7 +428,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should have credit with stripe and cybersource and no ideal it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithCreditTwoConnector" ]; cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit( @@ -486,7 +492,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as USD and billing address as IN it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -506,7 +513,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should have credit with stripe and cybersource and no ideal it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithCreditTwoConnector" ]; cy.paymentMethodListTestTwoConnectorsForOnePaymentMethodCredit( @@ -569,7 +576,8 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // creating payment with currency as EUR and no billing address it("create-payment-call-test", () => { - const data = getConnectorDetails("stripe")["pm_list"]["PaymentIntent"]; + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; const newData = { ...data, @@ -589,7 +597,7 @@ describe("Payment Method list using Constraint Graph flow tests", () => { // payment method list which should only have ideal with stripe it("payment-method-list-call-test", () => { const data = - getConnectorDetails("stripe")["pm_list"]["PmListResponse"][ + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ "PmListWithStripeForIdeal" ]; cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( @@ -599,4 +607,179 @@ describe("Payment Method list using Constraint Graph flow tests", () => { }); } ); + + context( + ` + MCA1 -> Stripe configured with credit = { currency = "USD" }\n + MCA2 -> Novalnet configured with credit = { currency = "EUR" }\n + Payment is done with currency as as USD and no billing address\n + The resultant Payment Method list should only have credit with stripe\n + `, + () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + it("merchant-create-call-test", () => { + cy.merchantCreateCallTest(fixtures.merchantCreateBody, globalState); + }); + + it("api-key-create-call-test", () => { + cy.apiKeyCreateTest(fixtures.apiKeyCreateBody, globalState); + }); + + it("customer-create-call-test", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + // stripe connector create with card credit enabled in USD + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInUsd, + globalState, + "stripe", + "stripe_US_default" + ); + }); + + // novalnet connector create with card credit enabled in EUR + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInEur, + globalState, + "novalnet", + "novalnet_DE_default" + ); + }); + + // creating payment with currency as USD and no billing email + // billing.email is mandatory for novalnet + it("create-payment-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; + const newData = { + ...data, + Request: data.RequestCurrencyUSD, + RequestCurrencyUSD: undefined, // we do not need this anymore + }; + + cy.createPaymentIntentTest( + createPaymentBodyWithCurrency("USD"), + newData, + "no_three_ds", + "automatic", + globalState + ); + }); + + // payment method list should only have credit with stripe + it("payment-method-list-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ + "PmListWithCreditOneConnector" + ]; + cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( + data, + globalState + ); + }); + } + ); + context( + ` + MCA1 -> Stripe configured with credit = { currency = "USD" }\n + MCA2 -> Novalnet configured with credit = { currency = "EUR" }\n + Payment is done with currency as as EUR and billing address for 3ds credit card\n + The resultant Payment Method list should only have credit with novalnet\n + `, + () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + it("merchant-create-call-test", () => { + cy.merchantCreateCallTest(fixtures.merchantCreateBody, globalState); + }); + + it("api-key-create-call-test", () => { + cy.apiKeyCreateTest(fixtures.apiKeyCreateBody, globalState); + }); + + it("customer-create-call-test", () => { + cy.createCustomerCallTest(fixtures.customerCreateBody, globalState); + }); + + // stripe connector create with card credit enabled in USD + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInUsd, + globalState, + "stripe", + "stripe_US_default" + ); + }); + + // novalnet connector create with card credit enabled in EUR + it("connector-create-call-test", () => { + cy.createNamedConnectorCallTest( + "payment_processor", + fixtures.createConnectorBody, + cardCreditEnabledInEur, + globalState, + "novalnet", + "novalnet_DE_default" + ); + }); + + // creating payment with currency as EUR and billing email + // billing.email is mandatory for novalnet + it("create-payment-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PaymentIntent"]; + const newData = { + ...data, + Request: data.RequestCurrencyEUR, + RequestCurrencyEUR: undefined, // we do not need this anymore + }; + + cy.createPaymentIntentTest( + createPaymentBodyWithCurrencyCountry("EUR", "IN", "IN"), + newData, + "three_ds", + "automatic", + globalState + ); + }); + + // payment method list should only have credit with novalnet + it("payment-method-list-call-test", () => { + const data = + getConnectorDetails("connector")["pm_list"]["PmListResponse"][ + "PmListWithCreditOneConnector" + ]; + cy.paymentMethodListTestLessThanEqualToOnePaymentMethod( + data, + globalState + ); + }); + } + ); }); diff --git a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js index ae72465b606..e782b32368b 100644 --- a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Commons.js @@ -58,6 +58,26 @@ export const cardCreditEnabledInUs = [ }, ]; +export const cardCreditEnabledInEur = [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: ["Visa"], + minimum_amount: 0, + accepted_currencies: { + type: "enable_only", + list: ["EUR"], + }, + maximum_amount: 68607706, + recurring_enabled: false, + installment_payment_enabled: true, + }, + ], + }, +]; + export const bankRedirectIdealEnabled = [ { payment_method: "bank_redirect", diff --git a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Connector.js similarity index 100% rename from cypress-tests/cypress/e2e/PaymentMethodListUtils/Stripe.js rename to cypress-tests/cypress/e2e/PaymentMethodListUtils/Connector.js diff --git a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js index f7d199164fd..64e127608a4 100644 --- a/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js +++ b/cypress-tests/cypress/e2e/PaymentMethodListUtils/Utils.js @@ -1,9 +1,9 @@ import { connectorDetails as CommonConnectorDetails } from "./Commons.js"; -import { connectorDetails as stripeConnectorDetails } from "./Stripe.js"; +import { connectorDetails as ConnectorDetails } from "./Connector.js"; const connectorDetails = { commons: CommonConnectorDetails, - stripe: stripeConnectorDetails, + connector: ConnectorDetails, }; export default function getConnectorDetails(connectorId) { diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js index 0a8235141dc..52e6acd6481 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js @@ -207,4 +207,167 @@ export const connectorDetails = { }, }, }, + pm_list: { + PmListResponse: { + PmListNull: { + payment_methods: [], + }, + pmListDynamicFieldWithoutBilling: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: null, + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: null, + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch_sdk_demo_id@gmail.com", + }, + }, + }, + ], + }, + ], + }, + pmListDynamicFieldWithBilling: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: "joseph", + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: "Doe", + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch.example@gmail.com", + }, + }, + }, + ], + }, + ], + }, + pmListDynamicFieldWithNames: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: "joseph", + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: "Doe", + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch.example@gmail.com", + }, + }, + }, + ], + }, + ], + }, + pmListDynamicFieldWithEmail: { + payment_methods: [ + { + payment_method: "card", + payment_method_types: [ + { + payment_method_type: "credit", + card_networks: [ + { + eligible_connectors: ["novalnet"], + }, + ], + required_fields: { + "billing.address.first_name": { + required_field: + "payment_method_data.billing.address.first_name", + display_name: "first_name", + field_type: "user_full_name", + value: "joseph", + }, + "billing.address.last_name": { + required_field: + "payment_method_data.billing.address.last_name", + display_name: "last_name", + field_type: "user_full_name", + value: "Doe", + }, + "billing.email": { + required_field: "payment_method_data.billing.email", + display_name: "email_address", + field_type: "user_email_address", + value: "hyperswitch.example@gmail.com", + }, + }, + }, + ], + }, + ], + }, + }, + }, }; From 84a4fb13d5d41ff5c089b6bfd68950290224f644 Mon Sep 17 00:00:00 2001 From: Gnanasundari24 <118818938+Gnanasundari24@users.noreply.github.com> Date: Mon, 6 Jan 2025 15:20:56 +0530 Subject: [PATCH 16/30] ci(cypress): Add Session Token Testcases (#6683) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Arindam Sahoo --- .github/workflows/cypress-tests-runner.yml | 5 ++ .../e2e/PaymentTest/00024-SessionCall.cy.js | 50 ++++++++++++++ .../cypress/e2e/PaymentUtils/Commons.js | 29 +++++++++ .../cypress/e2e/PaymentUtils/Cybersource.js | 17 +++++ .../cypress/e2e/PaymentUtils/Stripe.js | 17 +++++ cypress-tests/cypress/support/commands.js | 65 ++++++++++++++----- 6 files changed, 168 insertions(+), 15 deletions(-) create mode 100644 cypress-tests/cypress/e2e/PaymentTest/00024-SessionCall.cy.js diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index bc83c2388f7..070682b9c3a 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -222,6 +222,11 @@ jobs: - name: Setup Local Server if: ${{ env.RUN_TESTS == 'true' }} + env: + ROUTER__APPLEPAY_DECRYPT_KEYS__APPLE_PAY_PPC: ${{ secrets.APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE }} + ROUTER__APPLEPAY_DECRYPT_KEYS__APPLE_PAY_PPC_KEY: ${{ secrets.APPLE_PAY_PAYMENT_PROCESSING_CERTIFICATE_KEY }} + ROUTER__APPLEPAY_DECRYPT_KEYS__APPLE_PAY_MERCHANT_CERT: ${{ secrets.APPLE_PAY_MERCHANT_CERTIFICATE }} + ROUTER__APPLEPAY_DECRYPT_KEYS__APPLE_PAY_MERCHANT_CERT_KEY: ${{ secrets.APPLE_PAY_MERCHANT_CERTIFICATE_KEY }} run: | # Start the server in the background target/debug/router & diff --git a/cypress-tests/cypress/e2e/PaymentTest/00024-SessionCall.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00024-SessionCall.cy.js new file mode 100644 index 00000000000..8d729a2f77f --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentTest/00024-SessionCall.cy.js @@ -0,0 +1,50 @@ +import * as fixtures from "../../fixtures/imports"; +import State from "../../utils/State"; +import getConnectorDetails, * as utils from "../PaymentUtils/Utils"; + +let globalState; + +describe("Customer Create flow test", () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + const shouldContinue = true; // variable that will be used to skip tests if a previous test fails + + beforeEach(function () { + if (!shouldContinue) { + this.skip(); + } + }); + it("create-payment-call-test", () => { + let shouldContinue = true; // variable that will be used to skip tests if a previous test fails + + const data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "PaymentIntent" + ]; + + cy.createPaymentIntentTest( + fixtures.createPaymentBody, + data, + "no_three_ds", + "automatic", + globalState + ); + + if (shouldContinue) shouldContinue = utils.should_continue_further(data); + }); + + it("session-call-test", () => { + const data = getConnectorDetails(globalState.get("connectorId"))["card_pm"][ + "SessionToken" + ]; + + cy.sessionTokenCall(fixtures.sessionTokenBody, data, globalState); + }); +}); diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js index 85718d8f9ba..998c6b91f7b 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js @@ -398,6 +398,27 @@ export const payment_methods_enabled = [ }, ], }, + { + payment_method: "wallet", + payment_method_types: [ + { + payment_method_type: "apple_pay", + minimum_amount: 1, + maximum_amount: 68607706, + recurring_enabled: true, + installment_payment_enabled: true, + payment_experience: "invoke_sdk_client", + }, + { + payment_method_type: "google_pay", + minimum_amount: 1, + maximum_amount: 68607706, + recurring_enabled: true, + installment_payment_enabled: true, + payment_experience: "invoke_sdk_client", + }, + ], + }, ]; export const connectorDetails = { @@ -684,6 +705,14 @@ export const connectorDetails = { setup_future_usage: "on_session", }, }), + SessionToken: { + Response: { + status: 200, + body: { + session_token: [], + }, + }, + }, No3DSManualCapture: getCustomExchange({ Request: { payment_method: "card", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js b/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js index b98973250e9..6877d609ac1 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Cybersource.js @@ -151,6 +151,23 @@ export const connectorDetails = { }, }, }, + SessionToken: { + Response: { + status: 200, + body: { + session_token: [ + { + wallet_name: "apple_pay", + connector: "cybersource", + }, + { + wallet_name: "google_pay", + connector: "cybersource", + }, + ], + }, + }, + }, PaymentIntentWithShippingCost: { Request: { currency: "USD", diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js b/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js index b8dd275afae..8d4d94fa563 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js @@ -163,6 +163,23 @@ export const connectorDetails = { }, }, }, + SessionToken: { + Response: { + status: 200, + body: { + session_token: [ + { + wallet_name: "apple_pay", + connector: "stripe", + }, + { + wallet_name: "google_pay", + connector: "stripe", + }, + ], + }, + }, + }, PaymentIntentWithShippingCost: { Request: { currency: "USD", diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index 6135f26934e..bcbe64512fa 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -1016,21 +1016,56 @@ Cypress.Commands.add( } ); -Cypress.Commands.add("sessionTokenCall", (globalState, sessionTokenBody) => { - cy.request({ - method: "POST", - url: `${globalState.get("baseUrl")}/payments/session_tokens`, - headers: { - Accept: "application/json", - "Content-Type": "application/json", - "api-key": globalState.get("publishableKey"), - }, - body: sessionTokenBody, - failOnStatusCode: false, - }).then((response) => { - logRequestId(response.headers["x-request-id"]); - }); -}); +Cypress.Commands.add( + "sessionTokenCall", + (sessionTokenBody, data, globalState) => { + const { Response: resData } = data || {}; + + sessionTokenBody.payment_id = globalState.get("paymentID"); + sessionTokenBody.client_secret = globalState.get("clientSecret"); + + cy.request({ + method: "POST", + url: `${globalState.get("baseUrl")}/payments/session_tokens`, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "api-key": globalState.get("publishableKey"), + "x-merchant-domain": "hyperswitch - demo - store.netlify.app", + "x-client-platform": "web", + }, + body: sessionTokenBody, + failOnStatusCode: false, + }).then((response) => { + logRequestId(response.headers["x-request-id"]); + + if (response.status === 200) { + const expectedTokens = resData.body.session_token; + const actualTokens = response.body.session_token; + + // Verifying length of array + expect(actualTokens.length, "arrayLength").to.equal( + expectedTokens.length + ); + + // Verify specific fields in each session_token object + expectedTokens.forEach((expectedToken, index) => { + const actualToken = actualTokens[index]; + + // Check specific fields only + expect(actualToken.wallet_name, "wallet_name").to.equal( + expectedToken.wallet_name + ); + expect(actualToken.connector, "connector").to.equal( + expectedToken.connector + ); + }); + } else { + defaultErrorHandler(response, resData); + } + }); + } +); Cypress.Commands.add( "createPaymentIntentTest", From 79013024ff371efc6062310564b8b56e9bb22701 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Mon, 6 Jan 2025 20:37:20 +0530 Subject: [PATCH 17/30] chore(keymanager): add tenant-id to keymanager requests (#6968) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 2 +- config/deployments/env_specific.toml | 2 +- config/development.toml | 2 +- config/docker_compose.toml | 2 +- crates/common_utils/src/consts.rs | 3 +++ crates/common_utils/src/id_type/tenant.rs | 12 ++++++++- crates/common_utils/src/keymanager.rs | 15 ++++++++--- crates/common_utils/src/types/keymanager.rs | 30 +++++++++++++++++++++ crates/router/src/configs/defaults.rs | 12 +++++++++ crates/router/src/configs/settings.rs | 6 +++-- crates/router/src/types/domain/types.rs | 2 ++ loadtest/config/development.toml | 2 +- 12 files changed, 79 insertions(+), 11 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 99926632114..eb30435d5f8 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -762,7 +762,7 @@ sdk_eligible_payment_methods = "card" [multitenancy] enabled = false -global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"} +global_tenant = { tenant_id = "global", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} [multitenancy.tenants.public] base_url = "http://localhost:8080" # URL of the tenant diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 967b847dae5..809edf1bac6 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -303,7 +303,7 @@ region = "kms_region" # The AWS region used by the KMS SDK for decrypting data. [multitenancy] enabled = false -global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"} +global_tenant = { tenant_id = "global", schema = "public", redis_key_prefix = "", clickhouse_database = "default"} [multitenancy.tenants.public] base_url = "http://localhost:8080" diff --git a/config/development.toml b/config/development.toml index 4c9b8516b5a..e3631b96b17 100644 --- a/config/development.toml +++ b/config/development.toml @@ -794,7 +794,7 @@ sdk_eligible_payment_methods = "card" [multitenancy] enabled = false -global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default"} +global_tenant = { tenant_id = "global" ,schema = "public", redis_key_prefix = "global", clickhouse_database = "default"} [multitenancy.tenants.public] base_url = "http://localhost:8080" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 75699d0a967..6f95380d2db 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -635,7 +635,7 @@ sdk_eligible_payment_methods = "card" [multitenancy] enabled = false -global_tenant = { schema = "public", redis_key_prefix = "", clickhouse_database = "default" } +global_tenant = { tenant_id = "global", schema = "public", redis_key_prefix = "", clickhouse_database = "default" } [multitenancy.tenants.public] base_url = "http://localhost:8080" diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 3b437b703be..fdda26cc77c 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -149,3 +149,6 @@ pub const APPLEPAY_VALIDATION_URL: &str = /// Request ID pub const X_REQUEST_ID: &str = "x-request-id"; + +/// Default Tenant ID for the `Global` tenant +pub const DEFAULT_GLOBAL_TENANT_ID: &str = "global"; diff --git a/crates/common_utils/src/id_type/tenant.rs b/crates/common_utils/src/id_type/tenant.rs index 953bf82287a..0584496332a 100644 --- a/crates/common_utils/src/id_type/tenant.rs +++ b/crates/common_utils/src/id_type/tenant.rs @@ -1,4 +1,7 @@ -use crate::errors::{CustomResult, ValidationError}; +use crate::{ + consts::DEFAULT_GLOBAL_TENANT_ID, + errors::{CustomResult, ValidationError}, +}; crate::id_type!( TenantId, @@ -15,6 +18,13 @@ crate::impl_queryable_id_type!(TenantId); crate::impl_to_sql_from_sql_id_type!(TenantId); impl TenantId { + /// Get the default global tenant ID + pub fn get_default_global_tenant_id() -> Self { + Self(super::LengthId::new_unchecked( + super::AlphaNumericId::new_unchecked(DEFAULT_GLOBAL_TENANT_ID.to_string()), + )) + } + /// Get tenant id from String pub fn try_from_string(tenant_id: String) -> CustomResult { Self::try_from(std::borrow::Cow::from(tenant_id)) diff --git a/crates/common_utils/src/keymanager.rs b/crates/common_utils/src/keymanager.rs index 53761951791..0dc05b22fd7 100644 --- a/crates/common_utils/src/keymanager.rs +++ b/crates/common_utils/src/keymanager.rs @@ -11,11 +11,11 @@ use once_cell::sync::OnceCell; use router_env::{instrument, logger, tracing}; use crate::{ - consts::BASE64_ENGINE, + consts::{BASE64_ENGINE, TENANT_HEADER}, errors, types::keymanager::{ BatchDecryptDataRequest, DataKeyCreateResponse, DecryptDataRequest, - EncryptionCreateRequest, EncryptionTransferRequest, KeyManagerState, + EncryptionCreateRequest, EncryptionTransferRequest, GetKeymanagerTenant, KeyManagerState, TransientBatchDecryptDataRequest, TransientDecryptDataRequest, }, }; @@ -100,7 +100,7 @@ pub async fn call_encryption_service( request_body: T, ) -> errors::CustomResult where - T: ConvertRaw + Send + Sync + 'static + Debug, + T: GetKeymanagerTenant + ConvertRaw + Send + Sync + 'static + Debug, R: serde::de::DeserializeOwned, { let url = format!("{}/{endpoint}", &state.url); @@ -122,6 +122,15 @@ where .change_context(errors::KeyManagerClientError::FailedtoConstructHeader)?, )) } + + //Add Tenant ID + header.push(( + HeaderName::from_str(TENANT_HEADER) + .change_context(errors::KeyManagerClientError::FailedtoConstructHeader)?, + HeaderValue::from_str(request_body.get_tenant_id(state).get_string_repr()) + .change_context(errors::KeyManagerClientError::FailedtoConstructHeader)?, + )); + let response = send_encryption_request( state, HeaderMap::from_iter(header.into_iter()), diff --git a/crates/common_utils/src/types/keymanager.rs b/crates/common_utils/src/types/keymanager.rs index 09d26bd91ef..f18c6656207 100644 --- a/crates/common_utils/src/types/keymanager.rs +++ b/crates/common_utils/src/types/keymanager.rs @@ -23,8 +23,23 @@ use crate::{ transformers::{ForeignFrom, ForeignTryFrom}, }; +macro_rules! impl_get_tenant_for_request { + ($ty:ident) => { + impl GetKeymanagerTenant for $ty { + fn get_tenant_id(&self, state: &KeyManagerState) -> id_type::TenantId { + match self.identifier { + Identifier::User(_) | Identifier::UserAuth(_) => state.global_tenant_id.clone(), + Identifier::Merchant(_) => state.tenant_id.clone(), + } + } + } + }; +} + #[derive(Debug, Clone)] pub struct KeyManagerState { + pub tenant_id: id_type::TenantId, + pub global_tenant_id: id_type::TenantId, pub enabled: bool, pub url: String, pub client_idle_timeout: Option, @@ -35,6 +50,11 @@ pub struct KeyManagerState { #[cfg(feature = "keymanager_mtls")] pub cert: Secret, } + +pub trait GetKeymanagerTenant { + fn get_tenant_id(&self, state: &KeyManagerState) -> id_type::TenantId; +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] #[serde(tag = "data_identifier", content = "key_identifier")] pub enum Identifier { @@ -70,6 +90,10 @@ pub struct BatchEncryptDataRequest { pub data: DecryptedDataGroup, } +impl_get_tenant_for_request!(EncryptionCreateRequest); +impl_get_tenant_for_request!(EncryptionTransferRequest); +impl_get_tenant_for_request!(BatchEncryptDataRequest); + impl From<(Secret, S>, Identifier)> for EncryptDataRequest where S: Strategy>, @@ -219,6 +243,12 @@ pub struct DecryptDataRequest { pub data: StrongSecret, } +impl_get_tenant_for_request!(EncryptDataRequest); +impl_get_tenant_for_request!(TransientBatchDecryptDataRequest); +impl_get_tenant_for_request!(TransientDecryptDataRequest); +impl_get_tenant_for_request!(BatchDecryptDataRequest); +impl_get_tenant_for_request!(DecryptDataRequest); + impl ForeignFrom<(FxHashMap>, BatchEncryptDataResponse)> for FxHashMap>> where diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 74f6502159d..3c38fa2a775 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -1,6 +1,7 @@ use std::collections::{HashMap, HashSet}; use api_models::{enums, payment_methods::RequiredFieldInfo}; +use common_utils::id_type; #[cfg(feature = "payouts")] pub mod payout_required_fields; @@ -138,6 +139,17 @@ impl Default for super::settings::KvConfig { } } +impl Default for super::settings::GlobalTenant { + fn default() -> Self { + Self { + tenant_id: id_type::TenantId::get_default_global_tenant_id(), + schema: String::from("global"), + redis_key_prefix: String::from("global"), + clickhouse_database: String::from("global"), + } + } +} + #[allow(clippy::derivable_impls)] impl Default for super::settings::ApiKeys { fn default() -> Self { diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index ad5d9e89aaa..737f8dfb980 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -137,7 +137,7 @@ pub struct Platform { pub enabled: bool, } -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Clone, Default, Deserialize)] pub struct Multitenancy { pub tenants: TenantConfig, pub enabled: bool, @@ -195,8 +195,10 @@ impl storage_impl::config::TenantConfig for Tenant { } } -#[derive(Debug, Deserialize, Clone, Default)] +#[derive(Debug, Deserialize, Clone)] pub struct GlobalTenant { + #[serde(default = "id_type::TenantId::get_default_global_tenant_id")] + pub tenant_id: id_type::TenantId, pub schema: String, pub redis_key_prefix: String, pub clickhouse_database: String, diff --git a/crates/router/src/types/domain/types.rs b/crates/router/src/types/domain/types.rs index d4cd9ef62d7..bb123659060 100644 --- a/crates/router/src/types/domain/types.rs +++ b/crates/router/src/types/domain/types.rs @@ -7,6 +7,8 @@ impl From<&crate::SessionState> for KeyManagerState { fn from(state: &crate::SessionState) -> Self { let conf = state.conf.key_manager.get_inner(); Self { + global_tenant_id: state.conf.multitenancy.global_tenant.tenant_id.clone(), + tenant_id: state.tenant.tenant_id.clone(), enabled: conf.enabled, url: conf.url.clone(), client_idle_timeout: state.conf.proxy.idle_pool_connection_timeout, diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index ec58ab08b87..b26b5b2e438 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -400,7 +400,7 @@ keys = "accept-language,user-agent,x-profile-id" [multitenancy] enabled = false -global_tenant = { schema = "public", redis_key_prefix = "" } +global_tenant = { tenant_id = "global", schema = "public", redis_key_prefix = "" } [multitenancy.tenants.public] base_url = "http://localhost:8080" From d2da27e51990a33b2ce5ceeccf8d155b4db84d99 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 00:31:21 +0000 Subject: [PATCH 18/30] chore(version): 2025.01.07.0 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0640a5641..573f3de89be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.07.0 + +### Miscellaneous Tasks + +- **keymanager:** Add tenant-id to keymanager requests ([#6968](https://github.com/juspay/hyperswitch/pull/6968)) ([`7901302`](https://github.com/juspay/hyperswitch/commit/79013024ff371efc6062310564b8b56e9bb22701)) + +**Full Changelog:** [`2025.01.06.0...2025.01.07.0`](https://github.com/juspay/hyperswitch/compare/2025.01.06.0...2025.01.07.0) + +- - - + ## 2025.01.06.0 ### Miscellaneous Tasks From 099bd995851a3aa9688f5e160a744c6924f8ec7a Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 7 Jan 2025 12:26:49 +0530 Subject: [PATCH 19/30] docs(cypress): update cypress documentation (#6956) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- cypress-tests/README.md | 498 +++++++++++++++++++++++++++------------- 1 file changed, 337 insertions(+), 161 deletions(-) diff --git a/cypress-tests/README.md b/cypress-tests/README.md index 0a071aa34a0..87681bb6ea0 100644 --- a/cypress-tests/README.md +++ b/cypress-tests/README.md @@ -1,31 +1,82 @@ -# Cypress Tests +# Hyperswitch Cypress Testing Framework ## Overview -This Tool is a solution designed to automate testing for the [Hyperswitch](https://github.com/juspay/hyperswitch/) using Cypress, an open-source tool capable of conducting API call tests and UI tests. This README provides guidance on installing Cypress and its dependencies. - -## Installation - -### Prerequisites - -Before installing Cypress, ensure that `Node` and `npm` is installed on your machine. To check if it is installed, run the following command: +This is a comprehensive testing framework built with [Cypress](https://cypress.io) to automate testing for [Hyperswitch](https://github.com/juspay/hyperswitch/). The framework supports API testing with features like multiple credential management, configuration management, global state handling, and extensive utility functions. The framework provides extensive support for API testing with advanced features including: + +- [Multiple credential management](#multiple-credential-support) +- [Dynamic configuration management](#dynamic-configuration-management) +- Global state handling +- Extensive utility functions +- Parallel test execution +- Connector-specific implementations + +## Table of Contents + +- [Overview](#overview) +- [Table of Contents](#table-of-contents) +- [Quick Start](#quick-start) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) + - [Running Tests](#running-tests) + - [Development Mode (Interactive)](#development-mode-interactive) + - [CI Mode (Headless)](#ci-mode-headless) + - [Execute tests against multiple connectors or in parallel](#execute-tests-against-multiple-connectors-or-in-parallel) +- [Test reports](#test-reports) +- [Folder structure](#folder-structure) +- [Adding tests](#adding-tests) + - [Addition of test for a new connector](#addition-of-test-for-a-new-connector) + - [Developing Core Features or adding new tests](#developing-core-features-or-adding-new-tests) + - [1. Create or update test file](#1-create-or-update-test-file) + - [2. Add New Commands](#2-add-new-commands) + - [Managing global state](#managing-global-state) +- [Debugging](#debugging) + - [1. Interactive Mode](#1-interactive-mode) + - [2. Logging](#2-logging) + - [3. Screenshots](#3-screenshots) + - [4. State Debugging](#4-state-debugging) + - [5. Hooks](#5-hooks) + - [6. Tasks](#6-tasks) +- [Linting](#linting) +- [Best Practices](#best-practices) +- [Additional Resources](#additional-resources) +- [Contributing](#contributing) +- [Appendix](#appendix) + - [Example creds.json](#example-credsjson) + - [Multiple credential support](#multiple-credential-support) + - [Dynamic configuration management](#dynamic-configuration-management) + +## Quick Start + +For experienced users who want to get started quickly: ```shell -node -v -npm -v +git clone https://github.com/juspay/hyperswitch.git +cd hyperswitch/cypress-tests +npm ci +# connector_id must be replaced with the connector name that is being tested (e.g. stripe, paypal, etc.) +CYPRESS_CONNECTOR="connector_id" npm run cypress:ci ``` -If not, download and install `Node` from the official [Node.js website](https://nodejs.org/en/download/package-manager/current). This will also install `npm`. +## Getting Started -### Run Test Cases on your local +## Prerequisites -To run test cases, follow these steps: +- Node.js (18.x or above) +- npm or yarn +- [Hyperswitch development environment](https://github.com/juspay/hyperswitch/blob/main/docs/try_local_system.md) + +> [!NOTE] +> To learn about the hardware requirements and software dependencies for running Cypress, refer to the [official documentation](https://docs.cypress.io/app/get-started/install-cypress). + +## Installation 1. Clone the repository and switch to the project directory: ```shell - git clone https://github.com/juspay/hyperswitch - cd cypress-tests + git clone https://github.com/juspay/hyperswitch.git + cd hyperswitch/cypress-tests ``` 2. Install Cypress and its dependencies to `cypress-tests` directory by running the following command: @@ -34,65 +85,70 @@ To run test cases, follow these steps: npm ci ``` -3. Insert data to `cards_info` table in `hyperswitch_db` + Once installed, verify the installation by running: ```shell - psql --host=localhost --port=5432 --username=db_user --dbname=hyperswitch_db --command "\copy cards_info FROM '.github/data/cards_info.csv' DELIMITER ',' CSV HEADER;" + npx cypress --version ``` -4. Set environment variables for cypress + To learn about the supported commands, execute: ```shell - export CYPRESS_CONNECTOR="connector_id" - export CYPRESS_BASEURL="base_url" - export DEBUG=cypress:cli - export CYPRESS_ADMINAPIKEY="admin_api_key" - export CYPRESS_CONNECTOR_AUTH_FILE_PATH="path/to/creds.json" + npm run ``` -5. Run Cypress test cases - - To run the tests in interactive mode run the following command +3. Set up the cards database: ```shell - npm run cypress + psql --host=localhost --port=5432 --username=db_user --dbname=hyperswitch_db --command "\copy cards_info FROM '.github/data/cards_info.csv' DELIMITER ',' CSV HEADER;" ``` - To run all the tests in headless mode run the following command +4. Set environment variables for cypress ```shell - npm run cypress:ci + export CYPRESS_CONNECTOR="connector_id" + export CYPRESS_BASEURL="base_url" + export DEBUG=cypress:cli + export CYPRESS_ADMINAPIKEY="admin_api_key" + export CYPRESS_CONNECTOR_AUTH_FILE_PATH="path/to/creds.json" ``` - To run payment tests in headless mode run the following command +> [!TIP] +> It is recommended to install [direnv](https://github.com/direnv/direnv) and use a `.envrc` file to store these environment variables with `cypress-tests` directory. This will make it easier to manage environment variables while working with Cypress tests. - ```shell - npm run cypress:payments - ``` +> [!NOTE] +> To learn about how `creds` file should be structured, refer to the [example.creds.json](#example-credsjson) section below. - To run payout tests in headless mode run the following command +## Running Tests - ```shell - npm run cypress:payouts - ``` +Execution of Cypress tests can be done in two modes: Development mode (Interactive) and CI mode (Headless). The tests can be executed against a single connector or multiple connectors in parallel. Time taken to execute the tests will vary based on the number of connectors and the number of tests. For a single connector, the tests will take approximately 07-12 minutes to execute (this also depends on the hardware configurations). - To run routing tests in headless mode run the following command +For Development mode, the tests will run in the Cypress UI where execution of tests can be seen in real-time and provides a larger area for debugging based on the need. In CI mode (Headless), tests run in the terminal without UI interaction and generate reports automatically. - ```shell - npm run cypress:routing - ``` +### Development Mode (Interactive) -In order to run cypress tests against multiple connectors at a time or in parallel: +```shell +npm run cypress +``` -1. Set up `.env` file that exports necessary info: +### CI Mode (Headless) - ```env - export DEBUG=cypress:cli +```shell +# All tests +npm run cypress:ci + +# Specific test suites +npm run cypress:payments # Payment tests +npm run cypress:payment-method-list # Payment method list tests +npm run cypress:payouts # Payout tests +npm run cypress:routing # Routing tests +``` - export CYPRESS_ADMINAPIKEY='admin_api_key' - export CYPRESS_BASEURL='base_url' - export CYPRESS_CONNECTOR_AUTH_FILE_PATH="path/to/creds.json" +### Execute tests against multiple connectors or in parallel +1. Set additional environment variables: + + ```shell export PAYMENTS_CONNECTORS="payment_connector_1 payment_connector_2 payment_connector_3 payment_connector_4" export PAYOUTS_CONNECTORS="payout_connector_1 payout_connector_2 payout_connector_3" export PAYMENT_METHOD_LIST="" @@ -103,72 +159,64 @@ In order to run cypress tests against multiple connectors at a time or in parall ```shell source .env - scripts/execute_cypress.sh + ../scripts/execute_cypress.sh ``` Optionally, `--parallel ` can be passed to run cypress tests in parallel. By default, when `parallel` command is passed, it will be run in batches of `5`. -> [!NOTE] -> To learn about how creds file should be structured, refer to the [example.creds.json](#example-credsjson) section below. - -## Folder Structure +## Test reports -The folder structure of this directory is as follows: +The test reports are generated in the `cypress/reports` directory. The reports are generated in the `mochawesome` format and can be viewed in the browser. +These reports does include: -```text -. # The root directory for the Cypress tests. -├── .gitignore -├── cypress # Contains Cypress-related files and folders. -│ ├── e2e # End-to-end test directory. -│ │ ├── ConnectorTest # Directory for test scenarios related to connectors. -│ │ │ ├── your_testcase1_files_here.cy.js -│ │ │ ├── your_testcase2_files_here.cy.js -│ │ │ └── ... -│ │ └── ConnectorUtils # Directory for utility functions related to connectors. -│ │ ├── connector_detail_files_here.js -│ │ └── utils.js -│ ├── fixtures # Directory for storing test data API request. -│ │ └── your_fixture_files_here.json -│ ├── support # Directory for Cypress support files. -│ │ ├── commands.js # File containing custom Cypress commands and utilities. -│ │ └── e2e.js -│ └── utils -│ └── utility_files_go_here.js -├── cypress.config.js # Cypress configuration file. -├── cypress.env.json # File is used to store environment-specific configuration values,such as base URLs, which can be accessed within your Cypress tests. -├── package.json # Node.js package file. -├── readme.md # This file -└── yarn.lock -``` +- screenshots of the failed tests +- HTML and JSON reports -## Writing Tests +## Folder structure -### Adding Connectors +The folder structure of this directory is as follows: -To add a new connector for testing with Hyperswitch, follow these steps: +```txt +. +├── .prettierrc # prettier configs +├── README.md # this file +├── cypress +│   ├── e2e +│   │   ├── Test # Directory for test scenarios related to connectors. +│   │   │   ├── 00000-test_<0>.cy.js +│   │   │   ├── ... +│   │   │   └── 0000n-test_.cy.js +│   │   └── Utils # Directory for utility functions related to connectors. +│   │   ├── connector_<1>.js +│   │   ├── ... +│   │   └── connector_.js +│   ├── fixtures # Directory for storing test data API request. +│   │   ├── fixture_<1>.json +│   │   ├── ... +│   │   └── fixture_.json +│   ├── support # Directory for Cypress support files. +│   │   ├── commands.js # File containing custom Cypress commands and utilities. +│   │   ├── e2e.js +│   │   └── redirectionHandler.js +│   └── utils +│   ├── RequestBodyUtils.js +│   ├── State.js +│   └── featureFlags.js +├── cypress.config.js # Cypress configuration file. +├── eslint.config.js # linter configuration file. +└── package.json # Node.js package file. +``` -1. Include the connector details in the `creds.json` file: +## Adding tests - example: +### Addition of test for a new connector - ```json - { - "stripe": { - "connector_account_details": { - "auth_type": "HeaderKey", - "api_key": "SK_134" - } - } - } - ``` +1. Include the connector details in the `creds.json` file 2. Add the new connector details to the ConnectorUtils folder (including CardNo and connector-specific information). - Refer to Stripe.js file for guidance: - - ```javascript - /cypress-tests/cypress/e2e/ConnectorUtils/Stripe.js - ``` + To add a new Payment connector, refer to [`Stripe.js`](cypress/e2e/PaymentUtils/Stripe.js) file for reference. + To add a new Payout connector, refer to [`Adyen.js`](cypress-tests/cypress/e2e/PayoutUtils/Adyen.js) file for reference. **File Naming:** Create a new file named .js for your specific connector. @@ -176,107 +224,173 @@ To add a new connector for testing with Hyperswitch, follow these steps: **Handling Unsupported Features:** - - If a connector does not support a specific payment method or feature: - - You can omit the relevant configurations in the .js file. - - The handling of unsupported features will be managed by the commons.js file, which will throw an unsupported or not implemented error as appropriate. + - If a connector does not support a specific payment method or a feature: + - The relevant configurations in the `.js` file can be omitted + - The handling of unsupported or unimplemented features will be managed by the [`Commons.js`](cypress/e2e/PaymentUtils/Commons.js) file, which will throw the appropriate `unsupported` or `not implemented` error + +3. In `Utils.js`, import the new connector details + +4. If the connector has a specific redirection requirement, add relevant redirection logic in `support/redirectionHandler.js` -3. In `Utils.js`, import the new connector details. +### Developing Core Features or adding new tests + +#### 1. Create or update test file + +To add a new test, create a new test file in the `e2e` directory under respective `service`. The test file should follow the naming convention `000-Test.cy.js` and should contain the test cases related to the service. + +```javascript +// cypress/e2e/Test/NewFeature.cy.js +import * as fixtures from "../../fixtures/imports"; +import State from "../../utils/State"; -### Adding Functions +describe("New Feature", () => { + let globalState; -Similarly, add any helper functions or utilities in the `commands.js` in support folder and import them into your tests as needed. + before(() => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + it("tests new functionality", () => { + // Test implementation + }); +}); +``` -Example: Adding List Mandate function to support `ListMandate` scenario +#### 2. Add New Commands ```javascript -Cypress.Commands.add("listMandateCallTest", (globalState) => { - // declare all the variables and constants - const customerId = globalState.get("customerId"); - // construct the URL for the API call - const url: `${globalState.get("baseUrl")}/customers/${customerId}/mandates` - const api_key = globalState.get("apiKey"); +// cypress/support/commands.js +Cypress.Commands.add("newCommand", (params, globalState) => { + const baseUrl = globalState.get("baseUrl"); + const apiKey = globalState.get("apiKey"); + const url = `${baseUrl}/endpoint`; cy.request({ - method: "GET", + method: "POST", url: url, headers: { - "Content-Type": "application/json", - "api-key": api_key, + "api-key": apiKey, }, - // set failOnStatusCode to false to prevent Cypress from failing the test - failOnStatusCode: false, + body: params, }).then((response) => { - // mandatorliy log the `x-request-id` to the console - logRequestId(response.headers["x-request-id"]); - - expect(response.headers["content-type"]).to.include("application/json"); - - if (response.status === 200) { - // do the necessary validations like below - for (const key in response.body) { - expect(response.body[key]).to.have.property("mandate_id"); - expect(response.body[key]).to.have.property("status"); - } - } else { - // handle the error response - expect(response.status).to.equal(400); - } + // Assertions }); }); ``` -### Adding Scenarios +### Managing global state -To add new test scenarios: +The global state is used to share data between tests. The global state is stored in the `State` class and is accessible across all tests. Can only be accessed in the `before` and `after` hooks. -1. Navigate to the ConnectorTest directory. -2. Create a new test file or modify existing ones to add your scenarios. -3. Write your test scenarios using Cypress commands. +## Debugging -For example, to add a scenario for listing mandates in the `Mandateflows`: +### 1. Interactive Mode + +- Use `npm run cypress` for real-time test execution +- View request/response details in Cypress UI +- Use DevTools for deeper debugging + +### 2. Logging ```javascript -// cypress/ConnectorTest/CreateSingleuseMandate.js -describe("Payment Scenarios", () => { - it("should complete a successful payment", () => { - // Your test logic here - }); -}); +cy.task("cli_log", "Debug message"); +cy.log("Test state:", globalState.data); ``` -In this scenario, you can call functions defined in `command.js`. For instance, to test the `listMandateCallTest` function: +### 3. Screenshots + +- Automatically captured on test failure +- Custom screenshot capture: ```javascript -describe("Payment Scenarios", () => { - it("list-mandate-call-test", () => { - cy.listMandateCallTest(globalState); - }); +cy.screenshot("debug-state"); +``` + +### 4. State Debugging + +- Add state logging in hooks: + +```javascript +beforeEach(() => { + cy.log("Current state:", JSON.stringify(globalState.data)); }); ``` -You can create similar scenarios by calling other functions defined in `commands.js`. These functions interact with utility files like `.js` and include necessary assertions to support various connector scenarios. +### 5. Hooks -### Debugging +- If the `globalState` object does not contain latest data, it must be due to the hooks not being executed in the correct order +- Add `cy.log(globalState)` to the test case to verify the data in the `globalState` object -It is recommended to run `npm run cypress` while developing new test cases to debug and verify as it opens the Cypress UI allowing the developer to run individual tests. This also opens up the possibility to to view the test execution in real-time and debug any issues that may arise by viewing the request and response payloads directly. +> [!NOTE] +> Refer to the Cypress's official documentation for more information on hooks and their execution order [here](https://docs.cypress.io/app/core-concepts/writing-and-organizing-tests#Hooks). + +### 6. Tasks + +- Use `cy.task` to interact with the Node.js environment +- Task can only be used in `support` files and `spec` files. Using them in files outside these directories will result in unexpected behavior or errors like abrupt termination of the test suite + +## Linting -If, for any reason, the `globalState` object does not contain latest data, it must be due to the hooks not being executed in the correct order. In such cases, it is recommended to add `cy.log(globalState)` to the test case to verify the data in the `globalState` object. -Please refer to the Cypress's official documentation for more information on hooks and their execution order [here](https://docs.cypress.io/app/core-concepts/writing-and-organizing-tests#Hooks). +To run the formatting and lint checks, execute the following command: + +```shell +# Format the code +npm run format + +# Check the formatting +npm run format:check + +# Lint the code. This wont fix the logic issues, unused imports or variables +npm run lint -- --fix +``` + +## Best Practices + +1. Use the global state for sharing data between tests +2. Implement proper error handling +3. Use appropriate wait strategies +4. Maintain test independence +5. Follow the existing folder structure +6. Document connector-specific behaviors +7. Use descriptive test and variable names +8. Use custom commands for repetitive tasks +9. Use `cy.log` for debugging and do not use `console.log` ## Additional Resources -For more information on using Cypress and writing effective tests, refer to the official Cypress documentation: [Cypress Documentation](https://docs.cypress.io/) +- [Cypress Documentation](https://docs.cypress.io/) +- [API Testing Best Practices](https://docs.cypress.io/guides/end-to-end-testing/api-testing) +- [Hyperswitch API Documentation](https://hyperswitch.io/docs) + +## Contributing -## Example creds.json +1. Fork the repository +2. Create a feature branch +3. Add tests following the guidelines +4. Submit a pull request + +## Appendix + +### Example creds.json ```json { + // Connector with single credential support and metadata support "adyen": { "connector_account_details": { "auth_type": "SignatureKey", "api_key": "api_key", "key1": "key1", "api_secret": "api_secret" + }, + "metadata": { + "key": "value" } }, "bankofamerica": { @@ -294,12 +408,23 @@ For more information on using Cypress and writing effective tests, refer to the "key1": "key1" } }, + // Connector with multiple credential support "cybersource": { - "connector_account_details": { - "auth_type": "SignatureKey", - "api_key": "api_key", - "key1": "key1", - "api_secret": "api_secret" + "connector_1": { + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "api_key", + "key1": "key1", + "api_secret": "api_secret" + } + }, + "connector_2": { + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "api_key", + "key1": "key1", + "api_secret": "api_secret" + } } }, "nmi": { @@ -332,3 +457,54 @@ For more information on using Cypress and writing effective tests, refer to the } } ``` + +### Multiple credential support + +- There are some use cases where a connector supports a feature that requires a different set of API keys (example: Network transaction ID for Stripe expects a different API Key to be passed). This forces the need for having multiple credentials that serves different use cases +- This basically means that a connector can have multiple credentials +- At present the maximum number of credentials that can be supported is `2` +- The `creds.json` file should be structured to support multiple credentials for such connectors. The `creds.json` file should be structured as follows: + +```json +{ + "connector_name": { + "connector_1": { + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "api_key", + "key1": "key1", + "api_secret": "api_secret" + } + }, + "connector_2": { + "connector_account_details": { + "auth_type": "SignatureKey", + "api_key": "api_key", + "key1": "key1", + "api_secret": "api_secret" + } + } + } +} +``` + +### Dynamic configuration management + +- `Configs` is the new `object` that is introduced to manage the dynamic configurations that are required for the tests +- This is supposed to be passed in an exchange (configuration for a specific can be passed to a test based on the need and this will impact everywhere in the test execution for that connector) +- At present, only 3 configs are supported: + - `DELAY`: This is used to introduce a delay in the test execution. This is useful when a connector requires a delay in order to perform a specific operation + - `CONNECTOR_CREDENTIAL`: This is used to control the connector credentials that are used in the test execution. This is useful only when a connector supports multiple credentials and the test needs to be executed with a specific credential + - `TRIGGER_SKIP`: This is used to skip a test execution (preferably redirection flows). This is useful when a test is does not support a specific redirection flow and needs to be skipped +- Example: In order to refund a payment in Trustpay, a `DELAY` of at least `5` seconds is required. By passing `DELAY` to the `Configs` object for Trustpay, the delay will be applied to all the tests that are executed for Trustpay + +```json +{ + "Configs": { + "DELAY": { + "STATUS": true, + "TIMEOUT": 15000 + } + } +} +``` From 6b1e5b0aec190b9563df83703efee9cbeaee59fd Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:00:26 +0530 Subject: [PATCH 20/30] feat(connector): [Fiuu] Consume transaction id for error cases for Fiuu (#6998) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connectors/fiuu/transformers.rs | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 14ce13f971b..e914454e557 100644 --- a/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs @@ -838,11 +838,11 @@ impl reason: non_threeds_data.error_desc.clone(), status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(data.txn_id), }) } else { Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(data.txn_id), + resource_id: ResponseId::ConnectorTransactionId(data.txn_id.clone()), redirection_data: Box::new(None), mandate_reference: Box::new(mandate_reference), connector_metadata: None, @@ -1041,14 +1041,14 @@ impl TryFrom> reason: refund_data.reason.clone(), status_code: item.http_code, attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(refund_data.refund_id.to_string()), }), ..item.data }) } else { Ok(Self { response: Ok(RefundsResponseData { - connector_refund_id: refund_data.refund_id.to_string(), + connector_refund_id: refund_data.refund_id.clone().to_string(), refund_status, }), ..item.data @@ -1161,6 +1161,7 @@ impl TryFrom> for PaymentsSy FiuuPaymentResponse::FiuuPaymentSyncResponse(response) => { let stat_name = response.stat_name; let stat_code = response.stat_code.clone(); + let txn_id = response.tran_id; let status = enums::AttemptStatus::try_from(FiuuSyncStatus { stat_name, stat_code, @@ -1172,13 +1173,13 @@ impl TryFrom> for PaymentsSy message: response.error_desc.clone(), reason: Some(response.error_desc), attempt_status: Some(enums::AttemptStatus::Failure), - connector_transaction_id: None, + connector_transaction_id: Some(txn_id.clone()), }) } else { None }; let payments_response_data = PaymentsResponseData::TransactionResponse { - resource_id: item.data.request.connector_transaction_id.clone(), + resource_id: ResponseId::ConnectorTransactionId(txn_id.clone().to_string()), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, @@ -1198,6 +1199,7 @@ impl TryFrom> for PaymentsSy capture_method: item.data.request.capture_method, status: response.status, })?; + let txn_id = response.tran_id; let mandate_reference = response.extra_parameters.as_ref().and_then(|extra_p| { let mandate_token: Result = serde_json::from_str(&extra_p.clone().expose()); match mandate_token { @@ -1233,13 +1235,13 @@ impl TryFrom> for PaymentsSy .unwrap_or(consts::NO_ERROR_MESSAGE.to_owned()), reason: response.error_desc.clone(), attempt_status: Some(enums::AttemptStatus::Failure), - connector_transaction_id: None, + connector_transaction_id: Some(txn_id.clone()), }) } else { None }; let payments_response_data = PaymentsResponseData::TransactionResponse { - resource_id: item.data.request.connector_transaction_id.clone(), + resource_id: ResponseId::ConnectorTransactionId(txn_id.clone().to_string()), redirection_data: Box::new(None), mandate_reference: Box::new(mandate_reference), connector_metadata: None, @@ -1402,13 +1404,15 @@ impl TryFrom> .to_string(), ), attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(item.response.tran_id.clone()), }) } else { None }; let payments_response_data = PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.tran_id.to_string()), + resource_id: ResponseId::ConnectorTransactionId( + item.response.tran_id.clone().to_string(), + ), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, @@ -1513,13 +1517,15 @@ impl TryFrom> .to_string(), ), attempt_status: None, - connector_transaction_id: None, + connector_transaction_id: Some(item.response.tran_id.clone()), }) } else { None }; let payments_response_data = PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.tran_id.to_string()), + resource_id: ResponseId::ConnectorTransactionId( + item.response.tran_id.clone().to_string(), + ), redirection_data: Box::new(None), mandate_reference: Box::new(None), connector_metadata: None, From c4d36b506e159f39acff17e13f72b5c53edec184 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:01:17 +0530 Subject: [PATCH 21/30] feat(core): add columns unified error code and error message in refund table (#6933) --- api-reference/openapi_spec.json | 10 ++ crates/api_models/src/refunds.rs | 4 + crates/diesel_models/src/refund.rs | 26 +++++ crates/diesel_models/src/schema.rs | 4 + crates/diesel_models/src/schema_v2.rs | 4 + .../src/connectors/airwallex/transformers.rs | 6 -- crates/router/src/bin/scheduler.rs | 4 +- crates/router/src/consts.rs | 3 + crates/router/src/core/payment_link.rs | 67 ++++-------- crates/router/src/core/payout_link.rs | 11 +- crates/router/src/core/payouts.rs | 22 ++-- crates/router/src/core/refunds.rs | 100 ++++++++++++++++-- crates/router/src/db/events.rs | 1 + .../src/db/merchant_connector_account.rs | 2 + crates/router/src/db/merchant_key_store.rs | 1 + crates/router/src/db/refund.rs | 4 + crates/router/src/routes/app.rs | 3 + crates/router/src/routes/payment_link.rs | 4 - crates/router/src/routes/payout_link.rs | 18 +--- crates/router/src/routes/payouts.rs | 59 ++--------- crates/router/src/services/api.rs | 15 +-- crates/router/src/utils.rs | 11 +- crates/router/tests/cache.rs | 1 + crates/router/tests/connectors/aci.rs | 4 + crates/router/tests/connectors/utils.rs | 6 ++ crates/router/tests/payments.rs | 2 + crates/router/tests/payments2.rs | 2 + crates/router/tests/services.rs | 2 + .../down.sql | 3 + .../up.sql | 4 + 30 files changed, 237 insertions(+), 166 deletions(-) create mode 100644 migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/down.sql create mode 100644 migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/up.sql diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index df7e96431db..5e0e07bb6b4 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -23241,6 +23241,16 @@ "description": "The code for the error", "nullable": true }, + "unified_code": { + "type": "string", + "description": "Error code unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, + "unified_message": { + "type": "string", + "description": "Error message unified across the connectors is received here if there was an error while calling connector", + "nullable": true + }, "created_at": { "type": "string", "format": "date-time", diff --git a/crates/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 36a92e3ab03..ad09c333ea7 100644 --- a/crates/api_models/src/refunds.rs +++ b/crates/api_models/src/refunds.rs @@ -188,6 +188,10 @@ pub struct RefundResponse { pub error_message: Option, /// The code for the error pub error_code: Option, + /// Error code unified across the connectors is received here if there was an error while calling connector + pub unified_code: Option, + /// Error message unified across the connectors is received here if there was an error while calling connector + pub unified_message: Option, /// The timestamp at which refund is created #[serde(with = "common_utils::custom_serde::iso8601::option")] pub created_at: Option, diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index 758eba7255a..b929481d64d 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -54,6 +54,8 @@ pub struct Refund { pub connector_refund_data: Option, pub connector_transaction_data: Option, pub split_refunds: Option, + pub unified_code: Option, + pub unified_message: Option, } #[derive( @@ -132,6 +134,8 @@ pub enum RefundUpdate { updated_by: String, connector_refund_id: Option, connector_refund_data: Option, + unified_code: Option, + unified_message: Option, }, ManualUpdate { refund_status: Option, @@ -155,6 +159,8 @@ pub struct RefundUpdateInternal { updated_by: String, modified_at: PrimitiveDateTime, connector_refund_data: Option, + unified_code: Option, + unified_message: Option, } impl RefundUpdateInternal { @@ -171,6 +177,8 @@ impl RefundUpdateInternal { updated_by: self.updated_by, modified_at: self.modified_at, connector_refund_data: self.connector_refund_data, + unified_code: self.unified_code, + unified_message: self.unified_message, ..source } } @@ -199,6 +207,8 @@ impl From for RefundUpdateInternal { refund_reason: None, refund_error_code: None, modified_at: common_utils::date_time::now(), + unified_code: None, + unified_message: None, }, RefundUpdate::MetadataAndReasonUpdate { metadata, @@ -216,6 +226,8 @@ impl From for RefundUpdateInternal { refund_error_code: None, modified_at: common_utils::date_time::now(), connector_refund_data: None, + unified_code: None, + unified_message: None, }, RefundUpdate::StatusUpdate { connector_refund_id, @@ -235,11 +247,15 @@ impl From for RefundUpdateInternal { refund_reason: None, refund_error_code: None, modified_at: common_utils::date_time::now(), + unified_code: None, + unified_message: None, }, RefundUpdate::ErrorUpdate { refund_status, refund_error_message, refund_error_code, + unified_code, + unified_message, updated_by, connector_refund_id, connector_refund_data, @@ -255,6 +271,8 @@ impl From for RefundUpdateInternal { metadata: None, refund_reason: None, modified_at: common_utils::date_time::now(), + unified_code, + unified_message, }, RefundUpdate::ManualUpdate { refund_status, @@ -273,6 +291,8 @@ impl From for RefundUpdateInternal { refund_reason: None, modified_at: common_utils::date_time::now(), connector_refund_data: None, + unified_code: None, + unified_message: None, }, } } @@ -292,6 +312,8 @@ impl RefundUpdate { updated_by, modified_at: _, connector_refund_data, + unified_code, + unified_message, } = self.into(); Refund { connector_refund_id: connector_refund_id.or(source.connector_refund_id), @@ -305,6 +327,8 @@ impl RefundUpdate { updated_by, modified_at: common_utils::date_time::now(), connector_refund_data: connector_refund_data.or(source.connector_refund_data), + unified_code: unified_code.or(source.unified_code), + unified_message: unified_message.or(source.unified_message), ..source } } @@ -392,6 +416,8 @@ mod tests { "merchant_connector_id": null, "charges": null, "connector_transaction_data": null + "unified_code": null, + "unified_message": null, }"#; let deserialized = serde_json::from_str::(serialized_refund); diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 37a39cb731a..41df00da809 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1235,6 +1235,10 @@ diesel::table! { #[max_length = 512] connector_transaction_data -> Nullable, split_refunds -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 3e27f29427b..d3e6db6ba63 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1181,6 +1181,10 @@ diesel::table! { #[max_length = 512] connector_transaction_data -> Nullable, split_refunds -> Nullable, + #[max_length = 255] + unified_code -> Nullable, + #[max_length = 1024] + unified_message -> Nullable, } } diff --git a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs index 075a7b070d6..45a7b577a86 100644 --- a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs @@ -147,12 +147,6 @@ pub struct Browser { user_agent: String, } -#[derive(Debug, Serialize)] -pub struct Location { - lat: String, - lon: String, -} - #[derive(Debug, Serialize)] pub struct Mobile { device_model: Option, diff --git a/crates/router/src/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 62c8c03e1a9..e18ed318be1 100644 --- a/crates/router/src/bin/scheduler.rs +++ b/crates/router/src/bin/scheduler.rs @@ -158,7 +158,7 @@ pub async fn deep_health_check( let app_state = Arc::clone(&state.into_inner()); let service_name = service.into_inner(); for (tenant, _) in stores { - let session_state_res = app_state.clone().get_session_state(&tenant, || { + let session_state_res = app_state.clone().get_session_state(&tenant, None, || { errors::ApiErrorResponse::MissingRequiredField { field_name: "tenant_id", } @@ -397,7 +397,7 @@ async fn start_scheduler( WorkflowRunner {}, |state, tenant| { Arc::new(state.clone()) - .get_session_state(tenant, || ProcessTrackerError::TenantNotFound.into()) + .get_session_state(tenant, None, || ProcessTrackerError::TenantNotFound.into()) }, ) .await diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index e2784b968ad..bcc9a591863 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -210,3 +210,6 @@ pub const DYNAMIC_ROUTING_MAX_VOLUME: u8 = 100; /// Click To Pay pub const CLICK_TO_PAY: &str = "click_to_pay"; + +/// Refund flow identifier used for performing GSM operations +pub const REFUND_FLOW_STR: &str = "refund_flow"; diff --git a/crates/router/src/core/payment_link.rs b/crates/router/src/core/payment_link.rs index 498a00c240d..477803c92a2 100644 --- a/crates/router/src/core/payment_link.rs +++ b/crates/router/src/core/payment_link.rs @@ -6,7 +6,7 @@ use api_models::{ }; use common_utils::{ consts::{DEFAULT_LOCALE, DEFAULT_SESSION_EXPIRY}, - ext_traits::{AsyncExt, OptionExt, ValueExt}, + ext_traits::{OptionExt, ValueExt}, types::{AmountConvertor, StringMajorUnitForCore}, }; use error_stack::{report, ResultExt}; @@ -28,9 +28,8 @@ use crate::{ }, errors::RouterResponse, get_payment_link_config_value, get_payment_link_config_value_based_on_priority, - headers::ACCEPT_LANGUAGE, routes::SessionState, - services::{self, authentication::get_header_value_by_key}, + services, types::{ api::payment_link::PaymentLinkResponseExt, domain, @@ -70,7 +69,6 @@ pub async fn form_payment_link_data( key_store: domain::MerchantKeyStore, merchant_id: common_utils::id_type::MerchantId, payment_id: common_utils::id_type::PaymentId, - locale: Option, ) -> RouterResult<(PaymentLink, PaymentLinkData, PaymentLinkConfig)> { todo!() } @@ -82,7 +80,6 @@ pub async fn form_payment_link_data( key_store: domain::MerchantKeyStore, merchant_id: common_utils::id_type::MerchantId, payment_id: common_utils::id_type::PaymentId, - locale: Option, ) -> RouterResult<(PaymentLink, PaymentLinkData, PaymentLinkConfig)> { let db = &*state.store; let key_manager_state = &state.into(); @@ -242,7 +239,7 @@ pub async fn form_payment_link_data( redirect: false, theme: payment_link_config.theme.clone(), return_url: return_url.clone(), - locale: locale.clone(), + locale: Some(state.clone().locale), transaction_details: payment_link_config.transaction_details.clone(), unified_code: payment_attempt.unified_code, unified_message: payment_attempt.unified_message, @@ -273,7 +270,7 @@ pub async fn form_payment_link_data( display_sdk_only: payment_link_config.display_sdk_only, hide_card_nickname_field: payment_link_config.hide_card_nickname_field, show_card_form_by_default: payment_link_config.show_card_form_by_default, - locale, + locale: Some(state.clone().locale), transaction_details: payment_link_config.transaction_details.clone(), background_image: payment_link_config.background_image.clone(), details_layout: payment_link_config.details_layout, @@ -296,17 +293,9 @@ pub async fn initiate_secure_payment_link_flow( payment_id: common_utils::id_type::PaymentId, request_headers: &header::HeaderMap, ) -> RouterResponse { - let locale = get_header_value_by_key(ACCEPT_LANGUAGE.into(), request_headers)? - .map(|val| val.to_string()); - let (payment_link, payment_link_details, payment_link_config) = form_payment_link_data( - &state, - merchant_account, - key_store, - merchant_id, - payment_id, - locale, - ) - .await?; + let (payment_link, payment_link_details, payment_link_config) = + form_payment_link_data(&state, merchant_account, key_store, merchant_id, payment_id) + .await?; validator::validate_secure_payment_link_render_request( request_headers, @@ -396,19 +385,10 @@ pub async fn initiate_payment_link_flow( key_store: domain::MerchantKeyStore, merchant_id: common_utils::id_type::MerchantId, payment_id: common_utils::id_type::PaymentId, - request_headers: &header::HeaderMap, ) -> RouterResponse { - let locale = get_header_value_by_key(ACCEPT_LANGUAGE.into(), request_headers)? - .map(|val| val.to_string()); - let (_, payment_details, payment_link_config) = form_payment_link_data( - &state, - merchant_account, - key_store, - merchant_id, - payment_id, - locale, - ) - .await?; + let (_, payment_details, payment_link_config) = + form_payment_link_data(&state, merchant_account, key_store, merchant_id, payment_id) + .await?; let css_script = get_color_scheme_css(&payment_link_config); let js_script = get_js_script(&payment_details)?; @@ -727,7 +707,6 @@ pub async fn get_payment_link_status( _key_store: domain::MerchantKeyStore, _merchant_id: common_utils::id_type::MerchantId, _payment_id: common_utils::id_type::PaymentId, - _request_headers: &header::HeaderMap, ) -> RouterResponse { todo!() } @@ -739,10 +718,7 @@ pub async fn get_payment_link_status( key_store: domain::MerchantKeyStore, merchant_id: common_utils::id_type::MerchantId, payment_id: common_utils::id_type::PaymentId, - request_headers: &header::HeaderMap, ) -> RouterResponse { - let locale = get_header_value_by_key(ACCEPT_LANGUAGE.into(), request_headers)? - .map(|val| val.to_string()); let db = &*state.store; let key_manager_state = &(&state).into(); @@ -858,19 +834,14 @@ pub async fn get_payment_link_status( consts::DEFAULT_UNIFIED_ERROR_MESSAGE.to_owned(), ) }; - let unified_translated_message = locale - .as_ref() - .async_and_then(|locale_str| async { - helpers::get_unified_translation( - &state, - unified_code.to_owned(), - unified_message.to_owned(), - locale_str.to_owned(), - ) - .await - }) - .await - .or(Some(unified_message)); + let unified_translated_message = helpers::get_unified_translation( + &state, + unified_code.to_owned(), + unified_message.to_owned(), + state.locale.clone(), + ) + .await + .or(Some(unified_message)); let payment_details = api_models::payments::PaymentLinkStatusDetails { amount, @@ -885,7 +856,7 @@ pub async fn get_payment_link_status( redirect: true, theme: payment_link_config.theme.clone(), return_url, - locale, + locale: Some(state.locale.clone()), transaction_details: payment_link_config.transaction_details, unified_code: Some(unified_code), unified_message: unified_translated_message, diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index 22354b4c00f..e45b4fcead1 100644 --- a/crates/router/src/core/payout_link.rs +++ b/crates/router/src/core/payout_link.rs @@ -46,7 +46,6 @@ pub async fn initiate_payout_link( key_store: domain::MerchantKeyStore, req: payouts::PayoutLinkInitiateRequest, request_headers: &header::HeaderMap, - locale: String, ) -> RouterResponse { let db: &dyn StorageInterface = &*state.store; let merchant_id = merchant_account.get_id(); @@ -128,7 +127,7 @@ pub async fn initiate_payout_link( GenericLinks { allowed_domains, data: GenericLinksData::ExpiredLink(expired_link_data), - locale, + locale: state.locale, }, ))) } @@ -245,7 +244,7 @@ pub async fn initiate_payout_link( enabled_payment_methods_with_required_fields, amount, currency: payout.destination_currency, - locale: locale.clone(), + locale: state.locale.clone(), form_layout: link_data.form_layout, test_mode: link_data.test_mode.unwrap_or(false), }; @@ -270,7 +269,7 @@ pub async fn initiate_payout_link( GenericLinks { allowed_domains, data: GenericLinksData::PayoutLink(generic_form_data), - locale, + locale: state.locale.clone(), }, ))) } @@ -282,7 +281,7 @@ pub async fn initiate_payout_link( &state, payout_attempt.unified_code.as_ref(), payout_attempt.unified_message.as_ref(), - &locale, + &state.locale.clone(), ) .await?; let js_data = payouts::PayoutLinkStatusDetails { @@ -322,7 +321,7 @@ pub async fn initiate_payout_link( GenericLinks { allowed_domains, data: GenericLinksData::PayoutLinkStatus(generic_status_data), - locale, + locale: state.locale.clone(), }, ))) } diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index b2d6b2ace45..3c8432795a3 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -317,7 +317,6 @@ pub async fn payouts_create_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, - locale: &str, ) -> RouterResponse { // Validate create request let (payout_id, payout_method_data, profile_id, customer) = @@ -332,7 +331,7 @@ pub async fn payouts_create_core( &payout_id, &profile_id, payout_method_data.as_ref(), - locale, + &state.locale, customer.as_ref(), ) .await?; @@ -382,7 +381,6 @@ pub async fn payouts_confirm_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, - locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -390,7 +388,7 @@ pub async fn payouts_confirm_core( None, &key_store, &payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())), - locale, + &state.locale, ) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); @@ -454,7 +452,6 @@ pub async fn payouts_update_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, - locale: &str, ) -> RouterResponse { let payout_id = req.payout_id.clone().get_required_value("payout_id")?; let mut payout_data = make_payout_data( @@ -463,7 +460,7 @@ pub async fn payouts_update_core( None, &key_store, &payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())), - locale, + &state.locale, ) .await?; @@ -539,7 +536,6 @@ pub async fn payouts_retrieve_core( profile_id: Option, key_store: domain::MerchantKeyStore, req: payouts::PayoutRetrieveRequest, - locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -547,7 +543,7 @@ pub async fn payouts_retrieve_core( profile_id, &key_store, &payouts::PayoutRequest::PayoutRetrieveRequest(req.to_owned()), - locale, + &state.locale, ) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); @@ -584,7 +580,6 @@ pub async fn payouts_cancel_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutActionRequest, - locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -592,7 +587,7 @@ pub async fn payouts_cancel_core( None, &key_store, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), - locale, + &state.locale, ) .await?; @@ -678,7 +673,6 @@ pub async fn payouts_fulfill_core( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: payouts::PayoutActionRequest, - locale: &str, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, @@ -686,7 +680,7 @@ pub async fn payouts_fulfill_core( None, &key_store, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), - locale, + &state.locale, ) .await?; @@ -773,7 +767,6 @@ pub async fn payouts_list_core( _profile_id_list: Option>, _key_store: domain::MerchantKeyStore, _constraints: payouts::PayoutListConstraints, - _locale: &str, ) -> RouterResponse { todo!() } @@ -789,7 +782,6 @@ pub async fn payouts_list_core( profile_id_list: Option>, key_store: domain::MerchantKeyStore, constraints: payouts::PayoutListConstraints, - _locale: &str, ) -> RouterResponse { validator::validate_payout_list_request(&constraints)?; let merchant_id = merchant_account.get_id(); @@ -910,7 +902,6 @@ pub async fn payouts_filtered_list_core( profile_id_list: Option>, key_store: domain::MerchantKeyStore, filters: payouts::PayoutListFilterConstraints, - _locale: &str, ) -> RouterResponse { let limit = &filters.limit; validator::validate_payout_list_request_for_joins(*limit)?; @@ -1014,7 +1005,6 @@ pub async fn payouts_list_available_filters_core( merchant_account: domain::MerchantAccount, profile_id_list: Option>, time_range: common_utils::types::TimeRange, - _locale: &str, ) -> RouterResponse { let db = state.store.as_ref(); let payouts = db diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 373a268d691..e78c9471e61 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -26,7 +26,7 @@ use crate::{ consts, core::{ errors::{self, ConnectorErrorExt, RouterResponse, RouterResult, StorageErrorExt}, - payments::{self, access_token}, + payments::{self, access_token, helpers}, refunds::transformers::SplitRefundInput, utils as core_utils, }, @@ -116,7 +116,7 @@ pub async fn refund_create_core( req.merchant_connector_details .to_owned() .async_map(|mcd| async { - payments::helpers::insert_merchant_connector_creds_to_config(db, merchant_id, mcd).await + helpers::insert_merchant_connector_creds_to_config(db, merchant_id, mcd).await }) .await .transpose()?; @@ -237,6 +237,8 @@ pub async fn trigger_refund_to_gateway( updated_by: storage_scheme.to_string(), connector_refund_id: None, connector_refund_data: None, + unified_code: None, + unified_message: None, }) } errors::ConnectorError::NotSupported { message, connector } => { @@ -249,6 +251,8 @@ pub async fn trigger_refund_to_gateway( updated_by: storage_scheme.to_string(), connector_refund_id: None, connector_refund_data: None, + unified_code: None, + unified_message: None, }) } _ => None, @@ -284,14 +288,41 @@ pub async fn trigger_refund_to_gateway( }; let refund_update = match router_data_res.response { - Err(err) => storage::RefundUpdate::ErrorUpdate { - refund_status: Some(enums::RefundStatus::Failure), - refund_error_message: err.reason.or(Some(err.message)), - refund_error_code: Some(err.code), - updated_by: storage_scheme.to_string(), - connector_refund_id: None, - connector_refund_data: None, - }, + Err(err) => { + let option_gsm = helpers::get_gsm_record( + state, + Some(err.code.clone()), + Some(err.message.clone()), + connector.connector_name.to_string(), + consts::REFUND_FLOW_STR.to_string(), + ) + .await; + + let gsm_unified_code = option_gsm.as_ref().and_then(|gsm| gsm.unified_code.clone()); + let gsm_unified_message = option_gsm.and_then(|gsm| gsm.unified_message); + + let (unified_code, unified_message) = if let Some((code, message)) = + gsm_unified_code.as_ref().zip(gsm_unified_message.as_ref()) + { + (code.to_owned(), message.to_owned()) + } else { + ( + consts::DEFAULT_UNIFIED_ERROR_CODE.to_owned(), + consts::DEFAULT_UNIFIED_ERROR_MESSAGE.to_owned(), + ) + }; + + storage::RefundUpdate::ErrorUpdate { + refund_status: Some(enums::RefundStatus::Failure), + refund_error_message: err.reason.or(Some(err.message)), + refund_error_code: Some(err.code), + updated_by: storage_scheme.to_string(), + connector_refund_id: None, + connector_refund_data: None, + unified_code: Some(unified_code), + unified_message: Some(unified_message), + } + } Ok(response) => { // match on connector integrity checks match router_data_res.integrity_check.clone() { @@ -319,6 +350,8 @@ pub async fn trigger_refund_to_gateway( updated_by: storage_scheme.to_string(), connector_refund_id: refund_connector_transaction_id, connector_refund_data, + unified_code: None, + unified_message: None, } } Ok(()) => { @@ -461,7 +494,7 @@ pub async fn refund_retrieve_core( .merchant_connector_details .to_owned() .async_map(|mcd| async { - payments::helpers::insert_merchant_connector_creds_to_config(db, merchant_id, mcd).await + helpers::insert_merchant_connector_creds_to_config(db, merchant_id, mcd).await }) .await .transpose()?; @@ -479,6 +512,26 @@ pub async fn refund_retrieve_core( }) .transpose()?; + let unified_translated_message = if let (Some(unified_code), Some(unified_message)) = + (refund.unified_code.clone(), refund.unified_message.clone()) + { + helpers::get_unified_translation( + &state, + unified_code, + unified_message.clone(), + state.locale.to_string(), + ) + .await + .or(Some(unified_message)) + } else { + refund.unified_message + }; + + let refund = storage::Refund { + unified_message: unified_translated_message, + ..refund + }; + let response = if should_call_refund(&refund, request.force_sync.unwrap_or(false)) { Box::pin(sync_refund_with_gateway( &state, @@ -617,6 +670,8 @@ pub async fn sync_refund_with_gateway( updated_by: storage_scheme.to_string(), connector_refund_id: None, connector_refund_data: None, + unified_code: None, + unified_message: None, } } Ok(response) => match router_data_res.integrity_check.clone() { @@ -645,6 +700,8 @@ pub async fn sync_refund_with_gateway( updated_by: storage_scheme.to_string(), connector_refund_id: refund_connector_transaction_id, connector_refund_data, + unified_code: None, + unified_message: None, } } Ok(()) => { @@ -899,6 +956,25 @@ pub async fn validate_and_create_refund( } } }; + let unified_translated_message = if let (Some(unified_code), Some(unified_message)) = + (refund.unified_code.clone(), refund.unified_message.clone()) + { + helpers::get_unified_translation( + state, + unified_code, + unified_message.clone(), + state.locale.to_string(), + ) + .await + .or(Some(unified_message)) + } else { + refund.unified_message + }; + + let refund = storage::Refund { + unified_message: unified_translated_message, + ..refund + }; Ok(refund.foreign_into()) } @@ -1199,6 +1275,8 @@ impl ForeignFrom for api::RefundResponse { connector: refund.connector, merchant_connector_id: refund.merchant_connector_id, split_refunds: refund.split_refunds, + unified_code: refund.unified_code, + unified_message: refund.unified_message, } } } diff --git a/crates/router/src/db/events.rs b/crates/router/src/db/events.rs index 6bb7de1b7d9..33fcece85a0 100644 --- a/crates/router/src/db/events.rs +++ b/crates/router/src/db/events.rs @@ -734,6 +734,7 @@ mod tests { let state = &Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index 0abbccd2cb3..fc0fa5aca75 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -1561,6 +1561,7 @@ mod merchant_connector_account_cache_tests { let state = &Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -1746,6 +1747,7 @@ mod merchant_connector_account_cache_tests { let state = &Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/src/db/merchant_key_store.rs b/crates/router/src/db/merchant_key_store.rs index 9f12ec8e8fd..aaeba6085a0 100644 --- a/crates/router/src/db/merchant_key_store.rs +++ b/crates/router/src/db/merchant_key_store.rs @@ -350,6 +350,7 @@ mod tests { let state = &Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index 3a5c8b7da1e..88962f73233 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -437,6 +437,8 @@ mod storage { organization_id: new.organization_id.clone(), connector_refund_data: new.connector_refund_data.clone(), connector_transaction_data: new.connector_transaction_data.clone(), + unified_code: None, + unified_message: None, }; let field = format!( @@ -932,6 +934,8 @@ impl RefundInterface for MockDb { organization_id: new.organization_id, connector_refund_data: new.connector_refund_data, connector_transaction_data: new.connector_transaction_data, + unified_code: None, + unified_message: None, }; refunds.push(refund.clone()); Ok(refund) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f9dbec77452..f8b5ef44567 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -112,6 +112,7 @@ pub struct SessionState { pub opensearch_client: Arc, pub grpc_client: Arc, pub theme_storage_client: Arc, + pub locale: String, } impl scheduler::SchedulerSessionState for SessionState { fn get_db(&self) -> Box { @@ -458,6 +459,7 @@ impl AppState { pub fn get_session_state( self: Arc, tenant: &id_type::TenantId, + locale: Option, err: F, ) -> Result where @@ -484,6 +486,7 @@ impl AppState { opensearch_client: Arc::clone(&self.opensearch_client), grpc_client: Arc::clone(&self.grpc_client), theme_storage_client: self.theme_storage_client.clone(), + locale: locale.unwrap_or(common_utils::consts::DEFAULT_LOCALE.to_string()), }) } } diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index 71f10fe73e9..361367c7d27 100644 --- a/crates/router/src/routes/payment_link.rs +++ b/crates/router/src/routes/payment_link.rs @@ -65,7 +65,6 @@ pub async fn initiate_payment_link( payment_id, merchant_id: merchant_id.clone(), }; - let headers = req.headers(); Box::pin(api::server_wrap( flow, state, @@ -78,7 +77,6 @@ pub async fn initiate_payment_link( auth.key_store, payload.merchant_id.clone(), payload.payment_id.clone(), - headers, ) }, &crate::services::authentication::MerchantIdAuth(merchant_id), @@ -183,7 +181,6 @@ pub async fn payment_link_status( payment_id, merchant_id: merchant_id.clone(), }; - let headers = req.headers(); Box::pin(api::server_wrap( flow, state, @@ -196,7 +193,6 @@ pub async fn payment_link_status( auth.key_store, payload.merchant_id.clone(), payload.payment_id.clone(), - headers, ) }, &crate::services::authentication::MerchantIdAuth(merchant_id), diff --git a/crates/router/src/routes/payout_link.rs b/crates/router/src/routes/payout_link.rs index 25528b21ed8..0234b4fca82 100644 --- a/crates/router/src/routes/payout_link.rs +++ b/crates/router/src/routes/payout_link.rs @@ -1,14 +1,12 @@ use actix_web::{web, Responder}; use api_models::payouts::PayoutLinkInitiateRequest; -use common_utils::consts::DEFAULT_LOCALE; use router_env::Flow; use crate::{ core::{api_locking, payout_link::*}, - headers::ACCEPT_LANGUAGE, services::{ api, - authentication::{self as auth, get_header_value_by_key}, + authentication::{self as auth}, }, AppState, }; @@ -25,25 +23,13 @@ pub async fn render_payout_link( payout_id, }; let headers = req.headers(); - let locale = get_header_value_by_key(ACCEPT_LANGUAGE.into(), headers) - .ok() - .flatten() - .map(|val| val.to_string()) - .unwrap_or(DEFAULT_LOCALE.to_string()); Box::pin(api::server_wrap( flow, state, &req, payload.clone(), |state, auth, req, _| { - initiate_payout_link( - state, - auth.merchant_account, - auth.key_store, - req, - headers, - locale.clone(), - ) + initiate_payout_link(state, auth.merchant_account, auth.key_store, req, headers) }, &auth::MerchantIdAuth(merchant_id), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/routes/payouts.rs b/crates/router/src/routes/payouts.rs index 2329a48ef2b..4044630b4da 100644 --- a/crates/router/src/routes/payouts.rs +++ b/crates/router/src/routes/payouts.rs @@ -1,31 +1,20 @@ use actix_web::{ body::{BoxBody, MessageBody}, - http::header::HeaderMap, web, HttpRequest, HttpResponse, Responder, }; -use common_utils::consts; use router_env::{instrument, tracing, Flow}; use super::app::AppState; use crate::{ core::{api_locking, payouts::*}, - headers::ACCEPT_LANGUAGE, services::{ api, - authentication::{self as auth, get_header_value_by_key}, + authentication::{self as auth}, authorization::permissions::Permission, }, types::api::payouts as payout_types, }; -fn get_locale_from_header(headers: &HeaderMap) -> String { - get_header_value_by_key(ACCEPT_LANGUAGE.into(), headers) - .ok() - .flatten() - .map(|val| val.to_string()) - .unwrap_or(consts::DEFAULT_LOCALE.to_string()) -} - /// Payouts - Create #[instrument(skip_all, fields(flow = ?Flow::PayoutsCreate))] pub async fn payouts_create( @@ -34,7 +23,6 @@ pub async fn payouts_create( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsCreate; - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -42,7 +30,7 @@ pub async fn payouts_create( &req, json_payload.into_inner(), |state, auth: auth::AuthenticationData, req, _| { - payouts_create_core(state, auth.merchant_account, auth.key_store, req, &locale) + payouts_create_core(state, auth.merchant_account, auth.key_store, req) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -65,7 +53,6 @@ pub async fn payouts_retrieve( merchant_id: query_params.merchant_id.to_owned(), }; let flow = Flow::PayoutsRetrieve; - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -79,7 +66,6 @@ pub async fn payouts_retrieve( auth.profile_id, auth.key_store, req, - &locale, ) }, auth::auth_type( @@ -102,7 +88,6 @@ pub async fn payouts_update( json_payload: web::Json, ) -> HttpResponse { let flow = Flow::PayoutsUpdate; - let locale = get_locale_from_header(req.headers()); let payout_id = path.into_inner(); let mut payout_update_payload = json_payload.into_inner(); payout_update_payload.payout_id = Some(payout_id); @@ -112,7 +97,7 @@ pub async fn payouts_update( &req, payout_update_payload, |state, auth: auth::AuthenticationData, req, _| { - payouts_update_core(state, auth.merchant_account, auth.key_store, req, &locale) + payouts_update_core(state, auth.merchant_account, auth.key_store, req) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -138,7 +123,6 @@ pub async fn payouts_confirm( Ok(auth) => auth, Err(e) => return api::log_and_return_error_response(e), }; - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -146,7 +130,7 @@ pub async fn payouts_confirm( &req, payload, |state, auth, req, _| { - payouts_confirm_core(state, auth.merchant_account, auth.key_store, req, &locale) + payouts_confirm_core(state, auth.merchant_account, auth.key_store, req) }, &*auth_type, api_locking::LockAction::NotApplicable, @@ -165,7 +149,6 @@ pub async fn payouts_cancel( let flow = Flow::PayoutsCancel; let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -173,7 +156,7 @@ pub async fn payouts_cancel( &req, payload, |state, auth: auth::AuthenticationData, req, _| { - payouts_cancel_core(state, auth.merchant_account, auth.key_store, req, &locale) + payouts_cancel_core(state, auth.merchant_account, auth.key_store, req) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -191,7 +174,6 @@ pub async fn payouts_fulfill( let flow = Flow::PayoutsFulfill; let mut payload = json_payload.into_inner(); payload.payout_id = path.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -199,7 +181,7 @@ pub async fn payouts_fulfill( &req, payload, |state, auth: auth::AuthenticationData, req, _| { - payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req, &locale) + payouts_fulfill_core(state, auth.merchant_account, auth.key_store, req) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -217,7 +199,6 @@ pub async fn payouts_list( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -225,14 +206,7 @@ pub async fn payouts_list( &req, payload, |state, auth: auth::AuthenticationData, req, _| { - payouts_list_core( - state, - auth.merchant_account, - None, - auth.key_store, - req, - &locale, - ) + payouts_list_core(state, auth.merchant_account, None, auth.key_store, req) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -256,7 +230,6 @@ pub async fn payouts_list_profile( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -270,7 +243,6 @@ pub async fn payouts_list_profile( auth.profile_id.map(|profile_id| vec![profile_id]), auth.key_store, req, - &locale, ) }, auth::auth_type( @@ -295,7 +267,6 @@ pub async fn payouts_list_by_filter( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -303,14 +274,7 @@ pub async fn payouts_list_by_filter( &req, payload, |state, auth: auth::AuthenticationData, req, _| { - payouts_filtered_list_core( - state, - auth.merchant_account, - None, - auth.key_store, - req, - &locale, - ) + payouts_filtered_list_core(state, auth.merchant_account, None, auth.key_store, req) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -334,7 +298,6 @@ pub async fn payouts_list_by_filter_profile( ) -> HttpResponse { let flow = Flow::PayoutsList; let payload = json_payload.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -348,7 +311,6 @@ pub async fn payouts_list_by_filter_profile( auth.profile_id.map(|profile_id| vec![profile_id]), auth.key_store, req, - &locale, ) }, auth::auth_type( @@ -373,7 +335,6 @@ pub async fn payouts_list_available_filters_for_merchant( ) -> HttpResponse { let flow = Flow::PayoutsFilter; let payload = json_payload.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -381,7 +342,7 @@ pub async fn payouts_list_available_filters_for_merchant( &req, payload, |state, auth: auth::AuthenticationData, req, _| { - payouts_list_available_filters_core(state, auth.merchant_account, None, req, &locale) + payouts_list_available_filters_core(state, auth.merchant_account, None, req) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -405,7 +366,6 @@ pub async fn payouts_list_available_filters_for_profile( ) -> HttpResponse { let flow = Flow::PayoutsFilter; let payload = json_payload.into_inner(); - let locale = get_locale_from_header(req.headers()); Box::pin(api::server_wrap( flow, @@ -418,7 +378,6 @@ pub async fn payouts_list_available_filters_for_profile( auth.merchant_account, auth.profile_id.map(|profile_id| vec![profile_id]), req, - &locale, ) }, auth::auth_type( diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index cac856b2c48..f90528353cf 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -79,6 +79,7 @@ use crate::{ generic_link_response::build_generic_link_html, }, types::{self, api, ErrorResponse}, + utils, }; pub type BoxedPaymentConnectorIntegrationInterface = @@ -759,12 +760,14 @@ where )? }; - let mut session_state = Arc::new(app_state.clone()).get_session_state(&tenant_id, || { - errors::ApiErrorResponse::InvalidTenant { - tenant_id: tenant_id.get_string_repr().to_string(), - } - .switch() - })?; + let locale = utils::get_locale_from_header(&incoming_request_header.clone()); + let mut session_state = + Arc::new(app_state.clone()).get_session_state(&tenant_id, Some(locale), || { + errors::ApiErrorResponse::InvalidTenant { + tenant_id: tenant_id.get_string_repr().to_string(), + } + .switch() + })?; session_state.add_request_id(request_id); let mut request_state = session_state.get_req_state(); diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index e4046f95a15..f9ef4880924 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -55,9 +55,10 @@ use crate::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payments as payments_core, }, + headers::ACCEPT_LANGUAGE, logger, routes::{metrics, SessionState}, - services, + services::{self, authentication::get_header_value_by_key}, types::{ self, domain, transformers::{ForeignFrom, ForeignInto}, @@ -1324,3 +1325,11 @@ pub async fn trigger_refund_outgoing_webhook( ) -> RouterResult<()> { todo!() } + +pub fn get_locale_from_header(headers: &actix_web::http::header::HeaderMap) -> String { + get_header_value_by_key(ACCEPT_LANGUAGE.into(), headers) + .ok() + .flatten() + .map(|val| val.to_string()) + .unwrap_or(common_utils::consts::DEFAULT_LOCALE.to_string()) +} diff --git a/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index 55b92b4aace..a1f85534b6b 100644 --- a/crates/router/tests/cache.rs +++ b/crates/router/tests/cache.rs @@ -20,6 +20,7 @@ async fn invalidate_existing_cache_success() { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 18e9c43ed6a..645b81e7069 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -220,6 +220,7 @@ async fn payments_create_success() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -270,6 +271,7 @@ async fn payments_create_failure() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -336,6 +338,7 @@ async fn refund_for_successful_payments() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -409,6 +412,7 @@ async fn refunds_create_failure() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 305b11fe5b3..03ea1f42e83 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -602,6 +602,7 @@ pub trait ConnectorActions: Connector { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -645,6 +646,7 @@ pub trait ConnectorActions: Connector { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -689,6 +691,7 @@ pub trait ConnectorActions: Connector { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -732,6 +735,7 @@ pub trait ConnectorActions: Connector { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -826,6 +830,7 @@ pub trait ConnectorActions: Connector { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -866,6 +871,7 @@ async fn call_connector< let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index beaacb79fc0..e1fe40b4206 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -297,6 +297,7 @@ async fn payments_create_core() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -558,6 +559,7 @@ async fn payments_create_core_adyen_no_redirect() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 1d573d007ba..49d2e12b819 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -58,6 +58,7 @@ async fn payments_create_core() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -327,6 +328,7 @@ async fn payments_create_core_adyen_no_redirect() { let state = Arc::new(app_state) .get_session_state( &id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/crates/router/tests/services.rs b/crates/router/tests/services.rs index c014370b24f..36f969dac1c 100644 --- a/crates/router/tests/services.rs +++ b/crates/router/tests/services.rs @@ -20,6 +20,7 @@ async fn get_redis_conn_failure() { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); @@ -51,6 +52,7 @@ async fn get_redis_conn_success() { let state = Arc::new(app_state) .get_session_state( &common_utils::id_type::TenantId::try_from_string("public".to_string()).unwrap(), + None, || {}, ) .unwrap(); diff --git a/migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/down.sql b/migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/down.sql new file mode 100644 index 00000000000..74679837d38 --- /dev/null +++ b/migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE refund DROP COLUMN IF EXISTS unified_code; +ALTER TABLE refund DROP COLUMN IF EXISTS unified_message; \ No newline at end of file diff --git a/migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/up.sql b/migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/up.sql new file mode 100644 index 00000000000..3d350c790f6 --- /dev/null +++ b/migrations/2024-12-24-115958_add-unified-code-and-message-in-refunds/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE refund +ADD COLUMN IF NOT EXISTS unified_code VARCHAR(255) DEFAULT NULL, +ADD COLUMN IF NOT EXISTS unified_message VARCHAR(1024) DEFAULT NULL; \ No newline at end of file From d2212cb7eafa37c00ce3a8897a6ae4f1266f01cf Mon Sep 17 00:00:00 2001 From: Chethan Rao <70657455+Chethan-rao@users.noreply.github.com> Date: Tue, 7 Jan 2025 22:37:46 +0530 Subject: [PATCH 22/30] fix: consider status of payment method before filtering wallets in list pm (#7004) --- crates/router/src/core/payment_methods/cards.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 164c0e9557a..deae78b5540 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -3470,12 +3470,14 @@ pub async fn list_payment_methods( .any(|mca| mca.payment_method == enums::PaymentMethod::Wallet); if wallet_pm_exists { match db - .find_payment_method_by_customer_id_merchant_id_list( + .find_payment_method_by_customer_id_merchant_id_status( &((&state).into()), &key_store, - &customer.customer_id, - merchant_account.get_id(), + &customer.customer_id, + merchant_account.get_id(), + common_enums::PaymentMethodStatus::Active, None, + merchant_account.storage_scheme, ) .await { From d594173de2aefaedce0c808f5d4903d0ad7eb447 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Jan 2025 00:31:12 +0000 Subject: [PATCH 23/30] chore(version): 2025.01.08.0 --- CHANGELOG.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 573f3de89be..bff01f9396a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.08.0 + +### Features + +- **connector:** [Fiuu] Consume transaction id for error cases for Fiuu ([#6998](https://github.com/juspay/hyperswitch/pull/6998)) ([`6b1e5b0`](https://github.com/juspay/hyperswitch/commit/6b1e5b0aec190b9563df83703efee9cbeaee59fd)) +- **core:** Add columns unified error code and error message in refund table ([#6933](https://github.com/juspay/hyperswitch/pull/6933)) ([`c4d36b5`](https://github.com/juspay/hyperswitch/commit/c4d36b506e159f39acff17e13f72b5c53edec184)) + +### Bug Fixes + +- Consider status of payment method before filtering wallets in list pm ([#7004](https://github.com/juspay/hyperswitch/pull/7004)) ([`d2212cb`](https://github.com/juspay/hyperswitch/commit/d2212cb7eafa37c00ce3a8897a6ae4f1266f01cf)) + +### Documentation + +- **cypress:** Update cypress documentation ([#6956](https://github.com/juspay/hyperswitch/pull/6956)) ([`099bd99`](https://github.com/juspay/hyperswitch/commit/099bd995851a3aa9688f5e160a744c6924f8ec7a)) + +**Full Changelog:** [`2025.01.07.0...2025.01.08.0`](https://github.com/juspay/hyperswitch/compare/2025.01.07.0...2025.01.08.0) + +- - - + ## 2025.01.07.0 ### Miscellaneous Tasks From b46a921ccb05dc194253659c12991d9df7abe71e Mon Sep 17 00:00:00 2001 From: Kashif Date: Wed, 8 Jan 2025 13:19:14 +0530 Subject: [PATCH 24/30] chore(dynamic-fields): [Worldpay] update dynamic fields for payments (#7002) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../payment_connector_required_fields.rs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/router/src/configs/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 9e42aec4a51..4f6327d74b7 100644 --- a/crates/router/src/configs/defaults/payment_connector_required_fields.rs +++ b/crates/router/src/configs/defaults/payment_connector_required_fields.rs @@ -3224,7 +3224,8 @@ impl Default for settings::RequiredFields { enums::Connector::Worldpay, RequiredFieldFinal { mandate: HashMap::new(), - non_mandate: { + non_mandate: HashMap::new(), + common: { let mut pmd_fields = HashMap::from([ ( "payment_method_data.card.card_number".to_string(), @@ -3257,7 +3258,6 @@ impl Default for settings::RequiredFields { pmd_fields.extend(get_worldpay_billing_required_fields()); pmd_fields }, - common: HashMap::new(), } ), ( @@ -6420,7 +6420,8 @@ impl Default for settings::RequiredFields { enums::Connector::Worldpay, RequiredFieldFinal { mandate: HashMap::new(), - non_mandate: { + non_mandate: HashMap::new(), + common: { let mut pmd_fields = HashMap::from([ ( "payment_method_data.card.card_number".to_string(), @@ -6453,7 +6454,6 @@ impl Default for settings::RequiredFields { pmd_fields.extend(get_worldpay_billing_required_fields()); pmd_fields }, - common: HashMap::new(), } ), ( @@ -12840,18 +12840,18 @@ impl Default for settings::RequiredFields { pub fn get_worldpay_billing_required_fields() -> HashMap { HashMap::from([ ( - "billing.address.zip".to_string(), + "billing.address.line1".to_string(), RequiredFieldInfo { - required_field: "billing.address.zip".to_string(), - display_name: "zip".to_string(), - field_type: enums::FieldType::UserAddressPincode, + required_field: "payment_method_data.billing.address.line1".to_string(), + display_name: "line1".to_string(), + field_type: enums::FieldType::UserAddressLine1, value: None, }, ), ( "billing.address.country".to_string(), RequiredFieldInfo { - required_field: "billing.address.country".to_string(), + required_field: "payment_method_data.billing.address.country".to_string(), display_name: "country".to_string(), field_type: enums::FieldType::UserAddressCountry { options: vec![ @@ -12994,5 +12994,14 @@ pub fn get_worldpay_billing_required_fields() -> HashMap Date: Wed, 8 Jan 2025 14:37:02 +0530 Subject: [PATCH 25/30] feat(users): handle edge features for users in tenancy (#6990) --- crates/diesel_models/src/query/role.rs | 37 ++-- crates/diesel_models/src/query/user_role.rs | 30 ++++ crates/diesel_models/src/role.rs | 2 + crates/diesel_models/src/schema.rs | 2 + crates/diesel_models/src/schema_v2.rs | 2 + crates/router/src/analytics.rs | 164 ++++++++++++------ crates/router/src/core/user.rs | 99 +++++++++-- crates/router/src/core/user_role.rs | 98 ++++++++++- crates/router/src/core/user_role/role.rs | 66 +++++-- crates/router/src/db/kafka_store.rs | 29 +++- crates/router/src/db/role.rs | 59 +++++-- crates/router/src/db/user_role.rs | 41 +++++ crates/router/src/services/authorization.rs | 14 +- .../src/services/authorization/roles.rs | 9 +- crates/router/src/utils/user.rs | 11 +- crates/router/src/utils/user_role.rs | 29 ++-- .../down.sql | 2 + .../up.sql | 2 + 18 files changed, 556 insertions(+), 140 deletions(-) create mode 100644 migrations/2024-12-28-121104_add_column_tenant_id_to_roles/down.sql create mode 100644 migrations/2024-12-28-121104_add_column_tenant_id_to_roles/up.sql diff --git a/crates/diesel_models/src/query/role.rs b/crates/diesel_models/src/query/role.rs index 6f6a1404ee2..2ab58ec2382 100644 --- a/crates/diesel_models/src/query/role.rs +++ b/crates/diesel_models/src/query/role.rs @@ -32,14 +32,18 @@ impl Role { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, - dsl::role_id.eq(role_id.to_owned()).and( - dsl::merchant_id.eq(merchant_id.to_owned()).or(dsl::org_id - .eq(org_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Organization))), - ), + dsl::role_id + .eq(role_id.to_owned()) + .and(dsl::tenant_id.eq(tenant_id.to_owned())) + .and( + dsl::merchant_id.eq(merchant_id.to_owned()).or(dsl::org_id + .eq(org_id.to_owned()) + .and(dsl::scope.eq(RoleScope::Organization))), + ), ) .await } @@ -49,11 +53,13 @@ impl Role { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, dsl::role_id .eq(role_id.to_owned()) + .and(dsl::tenant_id.eq(tenant_id.to_owned())) .and(dsl::org_id.eq(org_id.to_owned())) .and( dsl::scope.eq(RoleScope::Organization).or(dsl::merchant_id @@ -64,15 +70,17 @@ impl Role { .await } - pub async fn find_by_role_id_and_org_id( + pub async fn find_by_role_id_org_id_tenant_id( conn: &PgPooledConn, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> StorageResult { generics::generic_find_one::<::Table, _, _>( conn, dsl::role_id .eq(role_id.to_owned()) + .and(dsl::tenant_id.eq(tenant_id.to_owned())) .and(dsl::org_id.eq(org_id.to_owned())), ) .await @@ -108,12 +116,16 @@ impl Role { conn: &PgPooledConn, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> StorageResult> { - let predicate = dsl::org_id.eq(org_id.to_owned()).and( - dsl::scope.eq(RoleScope::Organization).or(dsl::merchant_id - .eq(merchant_id.to_owned()) - .and(dsl::scope.eq(RoleScope::Merchant))), - ); + let predicate = dsl::tenant_id + .eq(tenant_id.to_owned()) + .and(dsl::org_id.eq(org_id.to_owned())) + .and( + dsl::scope.eq(RoleScope::Organization).or(dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::scope.eq(RoleScope::Merchant))), + ); generics::generic_filter::<::Table, _, _, _>( conn, @@ -127,13 +139,14 @@ impl Role { pub async fn generic_roles_list_for_org( conn: &PgPooledConn, + tenant_id: id_type::TenantId, org_id: id_type::OrganizationId, merchant_id: Option, entity_type: Option, limit: Option, ) -> StorageResult> { let mut query = ::table() - .filter(dsl::org_id.eq(org_id)) + .filter(dsl::tenant_id.eq(tenant_id).and(dsl::org_id.eq(org_id))) .into_boxed(); if let Some(merchant_id) = merchant_id { diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index bb07f671824..01fa05e925a 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -294,4 +294,34 @@ impl UserRole { }, } } + + pub async fn list_user_roles_by_user_id_across_tenants( + conn: &PgPooledConn, + user_id: String, + limit: Option, + ) -> StorageResult> { + let mut query = ::table() + .filter(dsl::user_id.eq(user_id)) + .into_boxed(); + if let Some(limit) = limit { + query = query.limit(limit.into()); + } + + router_env::logger::debug!(query = %debug_query::(&query).to_string()); + + match generics::db_metrics::track_database_call::( + query.get_results_async(conn), + generics::db_metrics::DatabaseOperation::Filter, + ) + .await + { + Ok(value) => Ok(value), + Err(err) => match err { + DieselError::NotFound => { + Err(report!(err)).change_context(errors::DatabaseError::NotFound) + } + _ => Err(report!(err)).change_context(errors::DatabaseError::Others), + }, + } + } } diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs index 8199bd3979c..16728801933 100644 --- a/crates/diesel_models/src/role.rs +++ b/crates/diesel_models/src/role.rs @@ -19,6 +19,7 @@ pub struct Role { pub last_modified_at: PrimitiveDateTime, pub last_modified_by: String, pub entity_type: enums::EntityType, + pub tenant_id: id_type::TenantId, } #[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -36,6 +37,7 @@ pub struct RoleNew { pub last_modified_at: PrimitiveDateTime, pub last_modified_by: String, pub entity_type: enums::EntityType, + pub tenant_id: id_type::TenantId, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 41df00da809..6e8bdb5ec26 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1312,6 +1312,8 @@ diesel::table! { last_modified_by -> Varchar, #[max_length = 64] entity_type -> Varchar, + #[max_length = 64] + tenant_id -> Varchar, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index d3e6db6ba63..d38f684a44d 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1259,6 +1259,8 @@ diesel::table! { last_modified_by -> Varchar, #[max_length = 64] entity_type -> Varchar, + #[max_length = 64] + tenant_id -> Varchar, } } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 8fe30aa59d4..8fb4f2ac2c1 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1925,32 +1925,57 @@ pub mod routes { json_payload.into_inner(), |state, auth: UserFromToken, req, _| async move { let role_id = auth.role_id; - let role_info = RoleInfo::from_role_id_and_org_id(&state, &role_id, &auth.org_id) - .await - .change_context(UserErrors::InternalServerError) - .change_context(OpenSearchError::UnknownError)?; + let role_info = RoleInfo::from_role_id_org_id_tenant_id( + &state, + &role_id, + &auth.org_id, + auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + ) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)?; let permission_groups = role_info.get_permission_groups(); if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) { return Err(OpenSearchError::AccessForbiddenError)?; } - let user_roles: HashSet = state - .global_store - .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { - user_id: &auth.user_id, - tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), - org_id: Some(&auth.org_id), - merchant_id: None, - profile_id: None, - entity_id: None, - version: None, - status: None, - limit: None, - }) - .await - .change_context(UserErrors::InternalServerError) - .change_context(OpenSearchError::UnknownError)? - .into_iter() - .collect(); + let user_roles: HashSet = match role_info.get_entity_type() { + EntityType::Tenant => state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &auth.user_id, + tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + org_id: None, + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: None, + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)? + .into_iter() + .collect(), + EntityType::Organization | EntityType::Merchant | EntityType::Profile => state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &auth.user_id, + tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + org_id: Some(&auth.org_id), + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: None, + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)? + .into_iter() + .collect(), + }; let state = Arc::new(state); let role_info_map: HashMap = user_roles @@ -1959,12 +1984,15 @@ pub mod routes { let state = Arc::clone(&state); let role_id = user_role.role_id.clone(); let org_id = user_role.org_id.clone().unwrap_or_default(); + let tenant_id = &user_role.tenant_id; async move { - RoleInfo::from_role_id_and_org_id(&state, &role_id, &org_id) - .await - .change_context(UserErrors::InternalServerError) - .change_context(OpenSearchError::UnknownError) - .map(|role_info| (role_id, role_info)) + RoleInfo::from_role_id_org_id_tenant_id( + &state, &role_id, &org_id, tenant_id, + ) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError) + .map(|role_info| (role_id, role_info)) } }) .collect::>() @@ -2047,32 +2075,57 @@ pub mod routes { indexed_req, |state, auth: UserFromToken, req, _| async move { let role_id = auth.role_id; - let role_info = RoleInfo::from_role_id_and_org_id(&state, &role_id, &auth.org_id) - .await - .change_context(UserErrors::InternalServerError) - .change_context(OpenSearchError::UnknownError)?; + let role_info = RoleInfo::from_role_id_org_id_tenant_id( + &state, + &role_id, + &auth.org_id, + auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + ) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)?; let permission_groups = role_info.get_permission_groups(); if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) { return Err(OpenSearchError::AccessForbiddenError)?; } - let user_roles: HashSet = state - .global_store - .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { - user_id: &auth.user_id, - tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), - org_id: Some(&auth.org_id), - merchant_id: None, - profile_id: None, - entity_id: None, - version: None, - status: None, - limit: None, - }) - .await - .change_context(UserErrors::InternalServerError) - .change_context(OpenSearchError::UnknownError)? - .into_iter() - .collect(); + let user_roles: HashSet = match role_info.get_entity_type() { + EntityType::Tenant => state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &auth.user_id, + tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + org_id: None, + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: None, + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)? + .into_iter() + .collect(), + EntityType::Organization | EntityType::Merchant | EntityType::Profile => state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &auth.user_id, + tenant_id: auth.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + org_id: Some(&auth.org_id), + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: None, + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)? + .into_iter() + .collect(), + }; let state = Arc::new(state); let role_info_map: HashMap = user_roles .iter() @@ -2080,12 +2133,15 @@ pub mod routes { let state = Arc::clone(&state); let role_id = user_role.role_id.clone(); let org_id = user_role.org_id.clone().unwrap_or_default(); + let tenant_id = &user_role.tenant_id; async move { - RoleInfo::from_role_id_and_org_id(&state, &role_id, &org_id) - .await - .change_context(UserErrors::InternalServerError) - .change_context(OpenSearchError::UnknownError) - .map(|role_info| (role_id, role_info)) + RoleInfo::from_role_id_org_id_tenant_id( + &state, &role_id, &org_id, tenant_id, + ) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError) + .map(|role_info| (role_id, role_info)) } }) .collect::>() diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index a7c60f33ff7..52d7d6a252e 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -116,10 +116,14 @@ pub async fn get_user_details( ) -> UserResponse { let user = user_from_token.get_user_from_db(&state).await?; let verification_days_left = utils::user::get_verification_days_left(&state, &user)?; - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -633,6 +637,10 @@ async fn handle_invitation( &request.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .to_not_found_response(UserErrors::InvalidRoleId)?; @@ -1155,10 +1163,14 @@ pub async fn resend_invite( .get_entity_id_and_type() .ok_or(UserErrors::InternalServerError)?; - let invitee_role_info = roles::RoleInfo::from_role_id_and_org_id( + let invitee_role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_role.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -1401,7 +1413,7 @@ pub async fn create_tenant_user( .change_context(UserErrors::InternalServerError) .attach_printable("Failed to get merchants list for org")? .pop() - .ok_or(UserErrors::InternalServerError) + .ok_or(UserErrors::InvalidRoleOperation) .attach_printable("No merchants found in the tenancy")?; let new_user = domain::NewUser::try_from(( @@ -1490,10 +1502,14 @@ pub async fn list_user_roles_details( .await .to_not_found_response(UserErrors::InvalidRoleOperation)?; - let requestor_role_info = roles::RoleInfo::from_role_id_and_org_id( + let requestor_role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .to_not_found_response(UserErrors::InternalServerError) @@ -1644,10 +1660,14 @@ pub async fn list_user_roles_details( .collect::>() .into_iter() .map(|role_id| async { - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -2757,10 +2777,14 @@ pub async fn list_orgs_for_user( state: SessionState, user_from_token: auth::UserFromToken, ) -> UserResponse> { - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -2834,10 +2858,14 @@ pub async fn list_merchants_for_user_in_org( state: SessionState, user_from_token: auth::UserFromToken, ) -> UserResponse> { - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -2909,10 +2937,14 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( state: SessionState, user_from_token: auth::UserFromToken, ) -> UserResponse> { - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -3001,10 +3033,14 @@ pub async fn switch_org_for_user( .into()); } - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError) @@ -3092,12 +3128,20 @@ pub async fn switch_org_for_user( request.org_id.clone(), role_id.clone(), profile_id.clone(), - user_from_token.tenant_id, + user_from_token.tenant_id.clone(), ) .await?; - utils::user_role::set_role_info_in_cache_by_role_id_org_id(&state, &role_id, &request.org_id) - .await; + utils::user_role::set_role_info_in_cache_by_role_id_org_id( + &state, + &role_id, + &request.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + ) + .await; let response = user_api::TokenResponse { token: token.clone(), @@ -3120,10 +3164,14 @@ pub async fn switch_merchant_for_user_in_org( } let key_manager_state = &(&state).into(); - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError) @@ -3275,11 +3323,20 @@ pub async fn switch_merchant_for_user_in_org( org_id.clone(), role_id.clone(), profile_id, - user_from_token.tenant_id, + user_from_token.tenant_id.clone(), ) .await?; - utils::user_role::set_role_info_in_cache_by_role_id_org_id(&state, &role_id, &org_id).await; + utils::user_role::set_role_info_in_cache_by_role_id_org_id( + &state, + &role_id, + &org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + ) + .await; let response = user_api::TokenResponse { token: token.clone(), @@ -3302,10 +3359,14 @@ pub async fn switch_profile_for_user_in_org_and_merchant( } let key_manager_state = &(&state).into(); - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError) @@ -3378,7 +3439,7 @@ pub async fn switch_profile_for_user_in_org_and_merchant( user_from_token.org_id.clone(), role_id.clone(), profile_id, - user_from_token.tenant_id, + user_from_token.tenant_id.clone(), ) .await?; @@ -3386,6 +3447,10 @@ pub async fn switch_profile_for_user_in_org_and_merchant( &state, &role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await; diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index d8fdff0e623..19d91b14f01 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -83,10 +83,14 @@ pub async fn get_parent_group_info( state: SessionState, user_from_token: auth::UserFromToken, ) -> UserResponse> { - let role_info = roles::RoleInfo::from_role_id_and_org_id( + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .to_not_found_response(UserErrors::InvalidRoleId)?; @@ -123,6 +127,10 @@ pub async fn update_user_role( &req.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .to_not_found_response(UserErrors::InvalidRoleId)?; @@ -143,10 +151,14 @@ pub async fn update_user_role( .attach_printable("User Changing their own role"); } - let updator_role = roles::RoleInfo::from_role_id_and_org_id( + let updator_role = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -179,10 +191,14 @@ pub async fn update_user_role( }; if let Some(user_role) = v2_user_role_to_be_updated { - let role_to_be_updated = roles::RoleInfo::from_role_id_and_org_id( + let role_to_be_updated = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_role.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -259,10 +275,14 @@ pub async fn update_user_role( }; if let Some(user_role) = v1_user_role_to_be_updated { - let role_to_be_updated = roles::RoleInfo::from_role_id_and_org_id( + let role_to_be_updated = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_role.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -485,10 +505,14 @@ pub async fn delete_user_role( .attach_printable("User deleting himself"); } - let deletion_requestor_role_info = roles::RoleInfo::from_role_id_and_org_id( + let deletion_requestor_role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -527,6 +551,10 @@ pub async fn delete_user_role( &role_to_be_deleted.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -597,6 +625,10 @@ pub async fn delete_user_role( &role_to_be_deleted.role_id, &user_from_token.merchant_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -680,10 +712,14 @@ pub async fn list_users_in_lineage( user_from_token: auth::UserFromToken, request: user_role_api::ListUsersInEntityRequest, ) -> UserResponse> { - let requestor_role_info = roles::RoleInfo::from_role_id_and_org_id( + let requestor_role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_from_token.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .change_context(UserErrors::InternalServerError)?; @@ -692,7 +728,49 @@ pub async fn list_users_in_lineage( requestor_role_info.get_entity_type(), request.entity_type, )? { - EntityType::Tenant | EntityType::Organization => { + EntityType::Tenant => { + let mut org_users = utils::user_role::fetch_user_roles_by_payload( + &state, + ListUserRolesByOrgIdPayload { + user_id: None, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + org_id: &user_from_token.org_id, + merchant_id: None, + profile_id: None, + version: None, + limit: None, + }, + request.entity_type, + ) + .await?; + + // Fetch tenant user + let tenant_user = state + .global_store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &user_from_token.user_id, + tenant_id: user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + org_id: None, + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: None, + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError)?; + + org_users.extend(tenant_user); + org_users + } + EntityType::Organization => { utils::user_role::fetch_user_roles_by_payload( &state, ListUserRolesByOrgIdPayload { @@ -777,10 +855,14 @@ pub async fn list_users_in_lineage( let role_info_map = futures::future::try_join_all(user_roles_set.iter().map(|user_role| async { - roles::RoleInfo::from_role_id_and_org_id( + roles::RoleInfo::from_role_id_org_id_tenant_id( &state, &user_role.role_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .map(|role_info| { diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index e897e1b336a..ca4c062244c 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -73,16 +73,21 @@ pub async fn create_role( &role_name, &user_from_token.merchant_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await?; let user_role_info = user_from_token.get_role_info_from_db(&state).await?; if matches!(req.role_scope, RoleScope::Organization) - && user_role_info.get_entity_type() != EntityType::Organization + && user_role_info.get_entity_type() < EntityType::Organization { - return Err(report!(UserErrors::InvalidRoleOperation)) - .attach_printable("Non org admin user creating org level role"); + return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable( + "User does not have sufficient privileges to perform organization-level role operation", + ); } let role = state @@ -99,6 +104,7 @@ pub async fn create_role( last_modified_by: user_from_token.user_id, created_at: now, last_modified_at: now, + tenant_id: user_from_token.tenant_id.unwrap_or(state.tenant.tenant_id), }) .await .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?; @@ -118,10 +124,17 @@ pub async fn get_role_with_groups( user_from_token: UserFromToken, role: role_api::GetRoleRequest, ) -> UserResponse { - let role_info = - roles::RoleInfo::from_role_id_and_org_id(&state, &role.role_id, &user_from_token.org_id) - .await - .to_not_found_response(UserErrors::InvalidRoleId)?; + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( + &state, + &role.role_id, + &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + ) + .await + .to_not_found_response(UserErrors::InvalidRoleId)?; if role_info.is_internal() { return Err(UserErrors::InvalidRoleId.into()); @@ -142,10 +155,17 @@ pub async fn get_parent_info_for_role( user_from_token: UserFromToken, role: role_api::GetRoleRequest, ) -> UserResponse { - let role_info = - roles::RoleInfo::from_role_id_and_org_id(&state, &role.role_id, &user_from_token.org_id) - .await - .to_not_found_response(UserErrors::InvalidRoleId)?; + let role_info = roles::RoleInfo::from_role_id_org_id_tenant_id( + &state, + &role.role_id, + &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), + ) + .await + .to_not_found_response(UserErrors::InvalidRoleId)?; if role_info.is_internal() { return Err(UserErrors::InvalidRoleId.into()); @@ -193,6 +213,10 @@ pub async fn update_role( role_name, &user_from_token.merchant_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await?; } @@ -206,6 +230,10 @@ pub async fn update_role( role_id, &user_from_token.merchant_id, &user_from_token.org_id, + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), ) .await .to_not_found_response(UserErrors::InvalidRoleOperation)?; @@ -273,6 +301,10 @@ pub async fn list_roles_with_info( EntityType::Tenant | EntityType::Organization => state .global_store .list_roles_for_org_by_parameters( + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, None, request.entity_type, @@ -284,6 +316,10 @@ pub async fn list_roles_with_info( EntityType::Merchant => state .global_store .list_roles_for_org_by_parameters( + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, Some(&user_from_token.merchant_id), request.entity_type, @@ -346,6 +382,10 @@ pub async fn list_roles_at_entity_level( EntityType::Tenant | EntityType::Organization => state .global_store .list_roles_for_org_by_parameters( + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, None, Some(req.entity_type), @@ -358,6 +398,10 @@ pub async fn list_roles_at_entity_level( EntityType::Merchant => state .global_store .list_roles_for_org_by_parameters( + user_from_token + .tenant_id + .as_ref() + .unwrap_or(&state.tenant.tenant_id), &user_from_token.org_id, Some(&user_from_token.merchant_id), Some(req.entity_type), diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 8eec7f04169..cbced16bdcd 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3209,6 +3209,16 @@ impl UserRoleInterface for KafkaStore { self.diesel_store.list_user_roles_by_user_id(payload).await } + async fn list_user_roles_by_user_id_across_tenants( + &self, + user_id: &str, + limit: Option, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_user_roles_by_user_id_across_tenants(user_id, limit) + .await + } + async fn list_user_roles_by_org_id<'a>( &self, payload: ListUserRolesByOrgIdPayload<'a>, @@ -3606,9 +3616,10 @@ impl RoleInterface for KafkaStore { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { self.diesel_store - .find_role_by_role_id_in_merchant_scope(role_id, merchant_id, org_id) + .find_role_by_role_id_in_merchant_scope(role_id, merchant_id, org_id, tenant_id) .await } @@ -3617,19 +3628,21 @@ impl RoleInterface for KafkaStore { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { self.diesel_store - .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id) + .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id, tenant_id) .await } - async fn find_by_role_id_and_org_id( + async fn find_by_role_id_org_id_tenant_id( &self, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { self.diesel_store - .find_by_role_id_and_org_id(role_id, org_id) + .find_by_role_id_org_id_tenant_id(role_id, org_id, tenant_id) .await } @@ -3654,19 +3667,23 @@ impl RoleInterface for KafkaStore { &self, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult, errors::StorageError> { - self.diesel_store.list_all_roles(merchant_id, org_id).await + self.diesel_store + .list_all_roles(merchant_id, org_id, tenant_id) + .await } async fn list_roles_for_org_by_parameters( &self, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, entity_type: Option, limit: Option, ) -> CustomResult, errors::StorageError> { self.diesel_store - .list_roles_for_org_by_parameters(org_id, merchant_id, entity_type, limit) + .list_roles_for_org_by_parameters(tenant_id, org_id, merchant_id, entity_type, limit) .await } } diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs index 877a4c54077..1006c33aaa0 100644 --- a/crates/router/src/db/role.rs +++ b/crates/router/src/db/role.rs @@ -29,6 +29,7 @@ pub trait RoleInterface { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult; async fn find_role_by_role_id_in_lineage( @@ -36,12 +37,14 @@ pub trait RoleInterface { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult; - async fn find_by_role_id_and_org_id( + async fn find_by_role_id_org_id_tenant_id( &self, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult; async fn update_role_by_role_id( @@ -59,10 +62,12 @@ pub trait RoleInterface { &self, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult, errors::StorageError>; async fn list_roles_for_org_by_parameters( &self, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, entity_type: Option, @@ -101,11 +106,18 @@ impl RoleInterface for Store { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; - storage::Role::find_by_role_id_in_merchant_scope(&conn, role_id, merchant_id, org_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) + storage::Role::find_by_role_id_in_merchant_scope( + &conn, + role_id, + merchant_id, + org_id, + tenant_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) } #[instrument(skip_all)] @@ -114,21 +126,23 @@ impl RoleInterface for Store { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; - storage::Role::find_by_role_id_in_lineage(&conn, role_id, merchant_id, org_id) + storage::Role::find_by_role_id_in_lineage(&conn, role_id, merchant_id, org_id, tenant_id) .await .map_err(|error| report!(errors::StorageError::from(error))) } #[instrument(skip_all)] - async fn find_by_role_id_and_org_id( + async fn find_by_role_id_org_id_tenant_id( &self, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { let conn = connection::pg_connection_read(self).await?; - storage::Role::find_by_role_id_and_org_id(&conn, role_id, org_id) + storage::Role::find_by_role_id_org_id_tenant_id(&conn, role_id, org_id, tenant_id) .await .map_err(|error| report!(errors::StorageError::from(error))) } @@ -161,9 +175,10 @@ impl RoleInterface for Store { &self, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; - storage::Role::list_roles(&conn, merchant_id, org_id) + storage::Role::list_roles(&conn, merchant_id, org_id, tenant_id) .await .map_err(|error| report!(errors::StorageError::from(error))) } @@ -171,6 +186,7 @@ impl RoleInterface for Store { #[instrument(skip_all)] async fn list_roles_for_org_by_parameters( &self, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, entity_type: Option, @@ -179,6 +195,7 @@ impl RoleInterface for Store { let conn = connection::pg_connection_read(self).await?; storage::Role::generic_roles_list_for_org( &conn, + tenant_id.to_owned(), org_id.to_owned(), merchant_id.cloned(), entity_type, @@ -217,6 +234,7 @@ impl RoleInterface for MockDb { created_at: role.created_at, last_modified_at: role.last_modified_at, last_modified_by: role.last_modified_by, + tenant_id: role.tenant_id, }; roles.push(role.clone()); Ok(role) @@ -245,12 +263,14 @@ impl RoleInterface for MockDb { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { let roles = self.roles.lock().await; roles .iter() .find(|role| { role.role_id == role_id + && (role.tenant_id == *tenant_id) && (role.merchant_id == *merchant_id || (role.org_id == *org_id && role.scope == enums::RoleScope::Organization)) }) @@ -269,12 +289,14 @@ impl RoleInterface for MockDb { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { let roles = self.roles.lock().await; roles .iter() .find(|role| { role.role_id == role_id + && (role.tenant_id == *tenant_id) && role.org_id == *org_id && ((role.scope == enums::RoleScope::Organization) || (role.merchant_id == *merchant_id @@ -290,15 +312,18 @@ impl RoleInterface for MockDb { ) } - async fn find_by_role_id_and_org_id( + async fn find_by_role_id_org_id_tenant_id( &self, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { let roles = self.roles.lock().await; roles .iter() - .find(|role| role.role_id == role_id && role.org_id == *org_id) + .find(|role| { + role.role_id == role_id && role.org_id == *org_id && role.tenant_id == *tenant_id + }) .cloned() .ok_or( errors::StorageError::ValueNotFound(format!( @@ -361,15 +386,17 @@ impl RoleInterface for MockDb { &self, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult, errors::StorageError> { let roles = self.roles.lock().await; let roles_list: Vec<_> = roles .iter() .filter(|role| { - role.merchant_id == *merchant_id - || (role.org_id == *org_id - && role.scope == diesel_models::enums::RoleScope::Organization) + role.tenant_id == *tenant_id + && (role.merchant_id == *merchant_id + || (role.org_id == *org_id + && role.scope == diesel_models::enums::RoleScope::Organization)) }) .cloned() .collect(); @@ -388,6 +415,7 @@ impl RoleInterface for MockDb { #[instrument(skip_all)] async fn list_roles_for_org_by_parameters( &self, + tenant_id: &id_type::TenantId, org_id: &id_type::OrganizationId, merchant_id: Option<&id_type::MerchantId>, entity_type: Option, @@ -403,7 +431,10 @@ impl RoleInterface for MockDb { None => true, }; - matches_merchant && role.org_id == *org_id && Some(role.entity_type) == entity_type + matches_merchant + && role.org_id == *org_id + && role.tenant_id == *tenant_id + && Some(role.entity_type) == entity_type }) .take(limit_usize) .cloned() diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index 0da51898326..1df6160f81e 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -79,6 +79,12 @@ pub trait UserRoleInterface { payload: ListUserRolesByUserIdPayload<'a>, ) -> CustomResult, errors::StorageError>; + async fn list_user_roles_by_user_id_across_tenants( + &self, + user_id: &str, + limit: Option, + ) -> CustomResult, errors::StorageError>; + async fn list_user_roles_by_org_id<'a>( &self, payload: ListUserRolesByOrgIdPayload<'a>, @@ -195,6 +201,21 @@ impl UserRoleInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } + async fn list_user_roles_by_user_id_across_tenants( + &self, + user_id: &str, + limit: Option, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::UserRole::list_user_roles_by_user_id_across_tenants( + &conn, + user_id.to_owned(), + limit, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + async fn list_user_roles_by_org_id<'a>( &self, payload: ListUserRolesByOrgIdPayload<'a>, @@ -472,6 +493,26 @@ impl UserRoleInterface for MockDb { Ok(filtered_roles) } + async fn list_user_roles_by_user_id_across_tenants( + &self, + user_id: &str, + limit: Option, + ) -> CustomResult, errors::StorageError> { + let user_roles = self.user_roles.lock().await; + + let filtered_roles: Vec<_> = user_roles + .iter() + .filter(|role| role.user_id == user_id) + .cloned() + .collect(); + + if let Some(Ok(limit)) = limit.map(|val| val.try_into()) { + return Ok(filtered_roles.into_iter().take(limit).collect()); + } + + Ok(filtered_roles) + } + async fn list_user_roles_by_org_id<'a>( &self, payload: ListUserRolesByOrgIdPayload<'a>, diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs index da296373d80..db3483f8164 100644 --- a/crates/router/src/services/authorization.rs +++ b/crates/router/src/services/authorization.rs @@ -33,7 +33,16 @@ where return Ok(role_info.clone()); } - let role_info = get_role_info_from_db(state, &token.role_id, &token.org_id).await?; + let role_info = get_role_info_from_db( + state, + &token.role_id, + &token.org_id, + token + .tenant_id + .as_ref() + .unwrap_or(&state.session_state().tenant.tenant_id), + ) + .await?; let token_expiry = i64::try_from(token.exp).change_context(ApiErrorResponse::InternalServerError)?; @@ -68,6 +77,7 @@ async fn get_role_info_from_db( state: &A, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> RouterResult where A: SessionStateInfo + Sync, @@ -75,7 +85,7 @@ where state .session_state() .global_store - .find_by_role_id_and_org_id(role_id, org_id) + .find_by_role_id_org_id_tenant_id(role_id, org_id, tenant_id) .await .map(roles::RoleInfo::from) .to_not_found_response(ApiErrorResponse::InvalidJwtToken) diff --git a/crates/router/src/services/authorization/roles.rs b/crates/router/src/services/authorization/roles.rs index c9c64b76143..df2a14a1a1a 100644 --- a/crates/router/src/services/authorization/roles.rs +++ b/crates/router/src/services/authorization/roles.rs @@ -121,29 +121,32 @@ impl RoleInfo { role_id: &str, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { if let Some(role) = predefined_roles::PREDEFINED_ROLES.get(role_id) { Ok(role.clone()) } else { state .global_store - .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id) + .find_role_by_role_id_in_lineage(role_id, merchant_id, org_id, tenant_id) .await .map(Self::from) } } - pub async fn from_role_id_and_org_id( + // TODO: To evaluate whether we can omit org_id and tenant_id for this function + pub async fn from_role_id_org_id_tenant_id( state: &SessionState, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> CustomResult { if let Some(role) = predefined_roles::PREDEFINED_ROLES.get(role_id) { Ok(role.clone()) } else { state .global_store - .find_by_role_id_and_org_id(role_id, org_id) + .find_by_role_id_org_id_tenant_id(role_id, org_id, tenant_id) .await .map(Self::from) } diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index b8ffcf836d7..ef06531b421 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -77,9 +77,14 @@ impl UserFromToken { } pub async fn get_role_info_from_db(&self, state: &SessionState) -> UserResult { - RoleInfo::from_role_id_and_org_id(state, &self.role_id, &self.org_id) - .await - .change_context(UserErrors::InternalServerError) + RoleInfo::from_role_id_org_id_tenant_id( + state, + &self.role_id, + &self.org_id, + self.tenant_id.as_ref().unwrap_or(&state.tenant.tenant_id), + ) + .await + .change_context(UserErrors::InternalServerError) } } diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index ac8ee11fc6a..7413e66070f 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -48,6 +48,7 @@ pub async fn validate_role_name( role_name: &domain::RoleName, merchant_id: &id_type::MerchantId, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> UserResult<()> { let role_name_str = role_name.clone().get_role_name(); @@ -58,7 +59,7 @@ pub async fn validate_role_name( // TODO: Create and use find_by_role_name to make this efficient let is_present_in_custom_roles = state .global_store - .list_all_roles(merchant_id, org_id) + .list_all_roles(merchant_id, org_id, tenant_id) .await .change_context(UserErrors::InternalServerError)? .iter() @@ -78,18 +79,24 @@ pub async fn set_role_info_in_cache_by_user_role( let Some(ref org_id) = user_role.org_id else { return false; }; - set_role_info_in_cache_if_required(state, user_role.role_id.as_str(), org_id) - .await - .map_err(|e| logger::error!("Error setting permissions in cache {:?}", e)) - .is_ok() + set_role_info_in_cache_if_required( + state, + user_role.role_id.as_str(), + org_id, + &user_role.tenant_id, + ) + .await + .map_err(|e| logger::error!("Error setting permissions in cache {:?}", e)) + .is_ok() } pub async fn set_role_info_in_cache_by_role_id_org_id( state: &SessionState, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> bool { - set_role_info_in_cache_if_required(state, role_id, org_id) + set_role_info_in_cache_if_required(state, role_id, org_id, tenant_id) .await .map_err(|e| logger::error!("Error setting permissions in cache {:?}", e)) .is_ok() @@ -99,15 +106,17 @@ pub async fn set_role_info_in_cache_if_required( state: &SessionState, role_id: &str, org_id: &id_type::OrganizationId, + tenant_id: &id_type::TenantId, ) -> UserResult<()> { if roles::predefined_roles::PREDEFINED_ROLES.contains_key(role_id) { return Ok(()); } - let role_info = roles::RoleInfo::from_role_id_and_org_id(state, role_id, org_id) - .await - .change_context(UserErrors::InternalServerError) - .attach_printable("Error getting role_info from role_id")?; + let role_info = + roles::RoleInfo::from_role_id_org_id_tenant_id(state, role_id, org_id, tenant_id) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Error getting role_info from role_id")?; authz::set_role_info_in_cache( state, diff --git a/migrations/2024-12-28-121104_add_column_tenant_id_to_roles/down.sql b/migrations/2024-12-28-121104_add_column_tenant_id_to_roles/down.sql new file mode 100644 index 00000000000..58d6b900e2d --- /dev/null +++ b/migrations/2024-12-28-121104_add_column_tenant_id_to_roles/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE roles DROP COLUMN IF EXISTS tenant_id; \ No newline at end of file diff --git a/migrations/2024-12-28-121104_add_column_tenant_id_to_roles/up.sql b/migrations/2024-12-28-121104_add_column_tenant_id_to_roles/up.sql new file mode 100644 index 00000000000..13ec3cf48b7 --- /dev/null +++ b/migrations/2024-12-28-121104_add_column_tenant_id_to_roles/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE roles ADD COLUMN IF NOT EXISTS tenant_id VARCHAR(64) NOT NULL DEFAULT 'public'; \ No newline at end of file From 0b54b375ef42bc46830871db6d0f7b68e386c3f5 Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Wed, 8 Jan 2025 16:45:27 +0530 Subject: [PATCH 26/30] fix(cypress): backup and restore sessions when using user apis (#6978) --- .../RoutingTest/00000-PriorityRouting.cy.js | 19 +++-- .../00001-VolumeBasedRouting.cy.js | 17 +++-- .../RoutingTest/00002-RuleBasedRouting.cy.js | 17 +++-- .../e2e/RoutingTest/00003-Retries.cy.js | 17 +++-- cypress-tests/cypress/support/commands.js | 71 ++++++++++--------- 5 files changed, 79 insertions(+), 62 deletions(-) diff --git a/cypress-tests/cypress/e2e/RoutingTest/00000-PriorityRouting.cy.js b/cypress-tests/cypress/e2e/RoutingTest/00000-PriorityRouting.cy.js index 22e4b3783af..5699712ec2e 100644 --- a/cypress-tests/cypress/e2e/RoutingTest/00000-PriorityRouting.cy.js +++ b/cypress-tests/cypress/e2e/RoutingTest/00000-PriorityRouting.cy.js @@ -7,7 +7,16 @@ let globalState; describe("Priority Based Routing Test", () => { let shouldContinue = true; - context("Login", () => { + beforeEach(() => { + // Restore the session if it exists + cy.session("login", () => { + cy.userLogin(globalState); + cy.terminate2Fa(globalState); + cy.userInfo(globalState); + }); + }); + + context("Get merchant info", () => { before("seed global state", () => { cy.task("getGlobalState").then((state) => { globalState = new State(state); @@ -18,12 +27,6 @@ describe("Priority Based Routing Test", () => { cy.task("setGlobalState", globalState.data); }); - it("User login", () => { - cy.userLogin(globalState); - cy.terminate2Fa(globalState); - cy.userInfo(globalState); - }); - it("merchant retrieve call", () => { cy.merchantRetrieveCall(globalState); }); @@ -39,6 +42,7 @@ describe("Priority Based Routing Test", () => { after("flush global state", () => { cy.task("setGlobalState", globalState.data); }); + it("list-mca-by-mid", () => { cy.ListMcaByMid(globalState); }); @@ -117,6 +121,7 @@ describe("Priority Based Routing Test", () => { after("flush global state", () => { cy.task("setGlobalState", globalState.data); }); + it("list-mca-by-mid", () => { cy.ListMcaByMid(globalState); }); diff --git a/cypress-tests/cypress/e2e/RoutingTest/00001-VolumeBasedRouting.cy.js b/cypress-tests/cypress/e2e/RoutingTest/00001-VolumeBasedRouting.cy.js index 7d7f75e3519..16b640bc03d 100644 --- a/cypress-tests/cypress/e2e/RoutingTest/00001-VolumeBasedRouting.cy.js +++ b/cypress-tests/cypress/e2e/RoutingTest/00001-VolumeBasedRouting.cy.js @@ -5,7 +5,16 @@ import * as utils from "../RoutingUtils/Utils"; let globalState; describe("Volume Based Routing Test", () => { - context("Login", () => { + beforeEach(() => { + // Restore the session if it exists + cy.session("login", () => { + cy.userLogin(globalState); + cy.terminate2Fa(globalState); + cy.userInfo(globalState); + }); + }); + + context("Get merchant info", () => { before("seed global state", () => { cy.task("getGlobalState").then((state) => { globalState = new State(state); @@ -16,12 +25,6 @@ describe("Volume Based Routing Test", () => { cy.task("setGlobalState", globalState.data); }); - it("User login", () => { - cy.userLogin(globalState); - cy.terminate2Fa(globalState); - cy.userInfo(globalState); - }); - it("merchant retrieve call", () => { cy.merchantRetrieveCall(globalState); }); diff --git a/cypress-tests/cypress/e2e/RoutingTest/00002-RuleBasedRouting.cy.js b/cypress-tests/cypress/e2e/RoutingTest/00002-RuleBasedRouting.cy.js index 304668752cd..a1621a530ae 100644 --- a/cypress-tests/cypress/e2e/RoutingTest/00002-RuleBasedRouting.cy.js +++ b/cypress-tests/cypress/e2e/RoutingTest/00002-RuleBasedRouting.cy.js @@ -5,7 +5,16 @@ import * as utils from "../RoutingUtils/Utils"; let globalState; describe("Rule Based Routing Test", () => { - context("Login", () => { + beforeEach(() => { + // Restore the session if it exists + cy.session("login", () => { + cy.userLogin(globalState); + cy.terminate2Fa(globalState); + cy.userInfo(globalState); + }); + }); + + context("Get merchant info", () => { before("seed global state", () => { cy.task("getGlobalState").then((state) => { globalState = new State(state); @@ -16,12 +25,6 @@ describe("Rule Based Routing Test", () => { cy.task("setGlobalState", globalState.data); }); - it("User login", () => { - cy.userLogin(globalState); - cy.terminate2Fa(globalState); - cy.userInfo(globalState); - }); - it("merchant retrieve call", () => { cy.merchantRetrieveCall(globalState); }); diff --git a/cypress-tests/cypress/e2e/RoutingTest/00003-Retries.cy.js b/cypress-tests/cypress/e2e/RoutingTest/00003-Retries.cy.js index 74c02d7ac9f..94fcaed104d 100644 --- a/cypress-tests/cypress/e2e/RoutingTest/00003-Retries.cy.js +++ b/cypress-tests/cypress/e2e/RoutingTest/00003-Retries.cy.js @@ -5,7 +5,16 @@ import * as utils from "../RoutingUtils/Utils"; let globalState; describe("Auto Retries & Step Up 3DS", () => { - context("Login", () => { + beforeEach(() => { + // Restore the session if it exists + cy.session("login", () => { + cy.userLogin(globalState); + cy.terminate2Fa(globalState); + cy.userInfo(globalState); + }); + }); + + context("Get merchant info", () => { before("seed global state", () => { cy.task("getGlobalState").then((state) => { globalState = new State(state); @@ -16,12 +25,6 @@ describe("Auto Retries & Step Up 3DS", () => { cy.task("setGlobalState", globalState.data); }); - it("User login", () => { - cy.userLogin(globalState); - cy.terminate2Fa(globalState); - cy.userInfo(globalState); - }); - it("List MCA", () => { cy.ListMcaByMid(globalState); }); diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index bcbe64512fa..e2e83edaf0c 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -3005,14 +3005,13 @@ Cypress.Commands.add("retrievePayoutCallTest", (globalState) => { // User API calls // Below 3 commands should be called in sequence to login a user Cypress.Commands.add("userLogin", (globalState) => { - // Define the necessary variables and constant - const base_url = globalState.get("baseUrl"); - const query_params = `token_only=true`; - const signin_body = { - email: `${globalState.get("email")}`, - password: `${globalState.get("password")}`, + const baseUrl = globalState.get("baseUrl"); + const queryParams = `token_only=true`; + const signinBody = { + email: globalState.get("email"), + password: globalState.get("password"), }; - const url = `${base_url}/user/v2/signin?${query_params}`; + const url = `${baseUrl}/user/v2/signin?${queryParams}`; cy.request({ method: "POST", @@ -3020,37 +3019,38 @@ Cypress.Commands.add("userLogin", (globalState) => { headers: { "Content-Type": "application/json", }, - body: signin_body, + body: signinBody, failOnStatusCode: false, }).then((response) => { logRequestId(response.headers["x-request-id"]); if (response.status === 200) { if (response.body.token_type === "totp") { - expect(response.body).to.have.property("token").and.to.not.be.empty; + expect(response.body, "totp_token").to.have.property("token").and.to.not + .be.empty; - globalState.set("totpToken", response.body.token); - cy.task("setGlobalState", globalState.data); + const totpToken = response.body.token; + globalState.set("totpToken", totpToken); } } else { throw new Error( - `User login call failed to get totp token with status ${response.status} and message ${response.body.message}` + `User login call failed to get totp token with status: "${response.status}" and message: "${response.body.error.message}"` ); } }); }); Cypress.Commands.add("terminate2Fa", (globalState) => { // Define the necessary variables and constant - const base_url = globalState.get("baseUrl"); - const query_params = `skip_two_factor_auth=true`; - const api_key = globalState.get("totpToken"); - const url = `${base_url}/user/2fa/terminate?${query_params}`; + const baseUrl = globalState.get("baseUrl"); + const queryParams = `skip_two_factor_auth=true`; + const apiKey = globalState.get("totpToken"); + const url = `${baseUrl}/user/2fa/terminate?${queryParams}`; cy.request({ method: "GET", url: url, headers: { - Authorization: `Bearer ${api_key}`, + Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, failOnStatusCode: false, @@ -3059,29 +3059,30 @@ Cypress.Commands.add("terminate2Fa", (globalState) => { if (response.status === 200) { if (response.body.token_type === "user_info") { - expect(response.body).to.have.property("token").and.to.not.be.empty; + expect(response.body, "user_info_token").to.have.property("token").and + .to.not.be.empty; - globalState.set("userInfoToken", response.body.token); - cy.task("setGlobalState", globalState.data); + const userInfoToken = response.body.token; + globalState.set("userInfoToken", userInfoToken); } } else { throw new Error( - `2FA terminate call failed with status ${response.status} and message ${response.body.message}` + `2FA terminate call failed with status: "${response.status}" and message: "${response.body.error.message}"` ); } }); }); Cypress.Commands.add("userInfo", (globalState) => { // Define the necessary variables and constant - const base_url = globalState.get("baseUrl"); - const api_key = globalState.get("userInfoToken"); - const url = `${base_url}/user`; + const baseUrl = globalState.get("baseUrl"); + const apiKey = globalState.get("userInfoToken"); + const url = `${baseUrl}/user`; cy.request({ method: "GET", url: url, headers: { - Authorization: `Bearer ${api_key}`, + Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, failOnStatusCode: false, @@ -3089,16 +3090,21 @@ Cypress.Commands.add("userInfo", (globalState) => { logRequestId(response.headers["x-request-id"]); if (response.status === 200) { - expect(response.body).to.have.property("merchant_id").and.to.not.be.empty; - expect(response.body).to.have.property("org_id").and.to.not.be.empty; - expect(response.body).to.have.property("profile_id").and.to.not.be.empty; + expect(response.body, "merchant_id").to.have.property("merchant_id").and + .to.not.be.empty; + expect(response.body, "organization_id").to.have.property("org_id").and.to + .not.be.empty; + expect(response.body, "profile_id").to.have.property("profile_id").and.to + .not.be.empty; globalState.set("merchantId", response.body.merchant_id); globalState.set("organizationId", response.body.org_id); globalState.set("profileId", response.body.profile_id); + + globalState.set("userInfoToken", apiKey); } else { throw new Error( - `User login call failed to fetch user info with status ${response.status} and message ${response.body.message}` + `User login call failed to fetch user info with status: "${response.status}" and message: "${response.body.error.message}"` ); } }); @@ -3146,7 +3152,6 @@ Cypress.Commands.add( headers: { Authorization: `Bearer ${globalState.get("userInfoToken")}`, "Content-Type": "application/json", - Cookie: `${globalState.get("cookie")}`, }, failOnStatusCode: false, body: routingBody, @@ -3169,15 +3174,14 @@ Cypress.Commands.add( Cypress.Commands.add("activateRoutingConfig", (data, globalState) => { const { Response: resData } = data || {}; - const routing_config_id = globalState.get("routingConfigId"); + cy.request({ method: "POST", url: `${globalState.get("baseUrl")}/routing/${routing_config_id}/activate`, headers: { Authorization: `Bearer ${globalState.get("userInfoToken")}`, "Content-Type": "application/json", - Cookie: `${globalState.get("cookie")}`, }, failOnStatusCode: false, }).then((response) => { @@ -3197,15 +3201,14 @@ Cypress.Commands.add("activateRoutingConfig", (data, globalState) => { Cypress.Commands.add("retrieveRoutingConfig", (data, globalState) => { const { Response: resData } = data || {}; - const routing_config_id = globalState.get("routingConfigId"); + cy.request({ method: "GET", url: `${globalState.get("baseUrl")}/routing/${routing_config_id}`, headers: { Authorization: `Bearer ${globalState.get("userInfoToken")}`, "Content-Type": "application/json", - Cookie: `${globalState.get("cookie")}`, }, failOnStatusCode: false, }).then((response) => { From d516fc72cbed8b061b8328f181611d28078f46c7 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 00:30:59 +0000 Subject: [PATCH 27/30] chore(version): 2025.01.09.0 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bff01f9396a..a8be15b40fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.09.0 + +### Features + +- **users:** Handle edge features for users in tenancy ([#6990](https://github.com/juspay/hyperswitch/pull/6990)) ([`d04e840`](https://github.com/juspay/hyperswitch/commit/d04e840c958595d86590149d92b03cbd61fd69ed)) + +### Bug Fixes + +- **cypress:** Backup and restore sessions when using user apis ([#6978](https://github.com/juspay/hyperswitch/pull/6978)) ([`0b54b37`](https://github.com/juspay/hyperswitch/commit/0b54b375ef42bc46830871db6d0f7b68e386c3f5)) + +### Miscellaneous Tasks + +- **dynamic-fields:** [Worldpay] update dynamic fields for payments ([#7002](https://github.com/juspay/hyperswitch/pull/7002)) ([`b46a921`](https://github.com/juspay/hyperswitch/commit/b46a921ccb05dc194253659c12991d9df7abe71e)) + +**Full Changelog:** [`2025.01.08.0...2025.01.09.0`](https://github.com/juspay/hyperswitch/compare/2025.01.08.0...2025.01.09.0) + +- - - + ## 2025.01.08.0 ### Features From 9c983b68bd834e33c5c57d1d050aa5d41cb10f56 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Thu, 9 Jan 2025 14:41:38 +0530 Subject: [PATCH 28/30] fix(dummyconnector): add tenant id in dummyconnector requests (#7008) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/router_data.rs | 1 + .../src/router_data_v2.rs | 2 + crates/router/src/connector/dummyconnector.rs | 20 +++-- crates/router/src/core/authentication.rs | 4 + .../src/core/authentication/transformers.rs | 10 +++ .../core/fraud_check/flows/checkout_flow.rs | 3 +- .../fraud_check/flows/fulfillment_flow.rs | 1 + .../core/fraud_check/flows/record_return.rs | 3 +- .../src/core/fraud_check/flows/sale_flow.rs | 3 +- .../fraud_check/flows/transaction_flow.rs | 3 +- crates/router/src/core/mandate.rs | 1 + crates/router/src/core/mandate/utils.rs | 3 + crates/router/src/core/payments/helpers.rs | 1 + .../router/src/core/payments/transformers.rs | 8 +- crates/router/src/core/payouts.rs | 70 +++++++++++----- crates/router/src/core/relay/utils.rs | 1 + .../core/unified_authentication_service.rs | 2 + .../unified_authentication_service/utils.rs | 3 + crates/router/src/core/utils.rs | 10 +++ crates/router/src/core/webhooks/incoming.rs | 1 + .../router/src/core/webhooks/incoming_v2.rs | 1 + crates/router/src/core/webhooks/utils.rs | 3 + .../router/src/services/conversion_impls.rs | 79 +++++++++++++++---- crates/router/src/types.rs | 2 + .../router/src/types/api/verify_connector.rs | 4 +- .../src/types/api/verify_connector/paypal.rs | 2 +- crates/router/tests/connectors/aci.rs | 2 + crates/router/tests/connectors/utils.rs | 2 + 28 files changed, 194 insertions(+), 51 deletions(-) diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 2970ac127ed..8e4c79a965e 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -22,6 +22,7 @@ pub struct RouterData { // Make this change after all the connector dependency has been removed from connectors pub payment_id: String, pub attempt_id: String, + pub tenant_id: id_type::TenantId, pub status: common_enums::enums::AttemptStatus, pub payment_method: common_enums::enums::PaymentMethod, pub connector_auth_type: ConnectorAuthType, diff --git a/crates/hyperswitch_domain_models/src/router_data_v2.rs b/crates/hyperswitch_domain_models/src/router_data_v2.rs index acc7343cb14..7ca106331e4 100644 --- a/crates/hyperswitch_domain_models/src/router_data_v2.rs +++ b/crates/hyperswitch_domain_models/src/router_data_v2.rs @@ -2,6 +2,7 @@ pub mod flow_common_types; use std::{marker::PhantomData, ops::Deref}; +use common_utils::id_type; #[cfg(feature = "frm")] pub use flow_common_types::FrmFlowData; #[cfg(feature = "payouts")] @@ -16,6 +17,7 @@ use crate::router_data::{ConnectorAuthType, ErrorResponse}; #[derive(Debug, Clone)] pub struct RouterDataV2 { pub flow: PhantomData, + pub tenant_id: id_type::TenantId, pub resource_common_data: ResourceCommonData, pub connector_auth_type: ConnectorAuthType, /// Contains flow-specific data required to construct a request and send it to the connector. diff --git a/crates/router/src/connector/dummyconnector.rs b/crates/router/src/connector/dummyconnector.rs index 15a2ff69036..226067cef96 100644 --- a/crates/router/src/connector/dummyconnector.rs +++ b/crates/router/src/connector/dummyconnector.rs @@ -2,7 +2,7 @@ pub mod transformers; use std::fmt::Debug; -use common_utils::request::RequestContent; +use common_utils::{consts as common_consts, request::RequestContent}; use diesel_models::enums; use error_stack::{report, ResultExt}; @@ -62,12 +62,18 @@ where req: &types::RouterData, _connectors: &settings::Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - types::PaymentsAuthorizeType::get_content_type(self) - .to_string() - .into(), - )]; + let mut header = vec![ + ( + headers::CONTENT_TYPE.to_string(), + types::PaymentsAuthorizeType::get_content_type(self) + .to_string() + .into(), + ), + ( + common_consts::TENANT_HEADER.to_string(), + req.tenant_id.get_string_repr().to_string().into(), + ), + ]; let mut api_key = self.get_auth_header(&req.connector_auth_type)?; header.append(&mut api_key); Ok(header) diff --git a/crates/router/src/core/authentication.rs b/crates/router/src/core/authentication.rs index 09cebda4908..bedd7787a73 100644 --- a/crates/router/src/core/authentication.rs +++ b/crates/router/src/core/authentication.rs @@ -42,6 +42,7 @@ pub async fn perform_authentication( psd2_sca_exemption_type: Option, ) -> CustomResult { let router_data = transformers::construct_authentication_router_data( + state, merchant_id, authentication_connector.clone(), payment_method_data, @@ -108,6 +109,7 @@ pub async fn perform_post_authentication( .attach_printable_lazy(|| format!("Error while fetching authentication record with authentication_id {authentication_id}"))?; if !authentication.authentication_status.is_terminal_status() && is_pull_mechanism_enabled { let router_data = transformers::construct_post_authentication_router_data( + state, authentication_connector.to_string(), business_profile, three_ds_connector_account, @@ -151,6 +153,7 @@ pub async fn perform_pre_authentication( let authentication = if authentication_connector.is_separate_version_call_required() { let router_data: core_types::authentication::PreAuthNVersionCallRouterData = transformers::construct_pre_authentication_router_data( + state, authentication_connector_name.clone(), card_number.clone(), &three_ds_connector_account, @@ -178,6 +181,7 @@ pub async fn perform_pre_authentication( let router_data: core_types::authentication::PreAuthNRouterData = transformers::construct_pre_authentication_router_data( + state, authentication_connector_name.clone(), card_number, &three_ds_connector_account, diff --git a/crates/router/src/core/authentication/transformers.rs b/crates/router/src/core/authentication/transformers.rs index 30373d1408f..4e7e005c946 100644 --- a/crates/router/src/core/authentication/transformers.rs +++ b/crates/router/src/core/authentication/transformers.rs @@ -15,6 +15,7 @@ use crate::{ transformers::{ForeignFrom, ForeignTryFrom}, }, utils::ext_traits::OptionExt, + SessionState, }; const IRRELEVANT_ATTEMPT_ID_IN_AUTHENTICATION_FLOW: &str = @@ -24,6 +25,7 @@ const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_AUTHENTICATION_FLOW: &str = #[allow(clippy::too_many_arguments)] pub fn construct_authentication_router_data( + state: &SessionState, merchant_id: common_utils::id_type::MerchantId, authentication_connector: String, payment_method_data: domain::PaymentMethodData, @@ -65,6 +67,7 @@ pub fn construct_authentication_router_data( webhook_url, }; construct_router_data( + state, authentication_connector, payment_method, merchant_id.clone(), @@ -76,6 +79,7 @@ pub fn construct_authentication_router_data( } pub fn construct_post_authentication_router_data( + state: &SessionState, authentication_connector: String, business_profile: domain::Profile, merchant_connector_account: payments_helpers::MerchantConnectorAccountType, @@ -90,6 +94,7 @@ pub fn construct_post_authentication_router_data( threeds_server_transaction_id, }; construct_router_data( + state, authentication_connector, PaymentMethod::default(), business_profile.merchant_id.clone(), @@ -101,6 +106,7 @@ pub fn construct_post_authentication_router_data( } pub fn construct_pre_authentication_router_data( + state: &SessionState, authentication_connector: String, card_holder_account_number: cards::CardNumber, merchant_connector_account: &payments_helpers::MerchantConnectorAccountType, @@ -116,6 +122,7 @@ pub fn construct_pre_authentication_router_data( card_holder_account_number, }; construct_router_data( + state, authentication_connector, PaymentMethod::default(), merchant_id, @@ -126,7 +133,9 @@ pub fn construct_pre_authentication_router_data( ) } +#[allow(clippy::too_many_arguments)] pub fn construct_router_data( + state: &SessionState, authentication_connector_name: String, payment_method: PaymentMethod, merchant_id: common_utils::id_type::MerchantId, @@ -144,6 +153,7 @@ pub fn construct_router_data( flow: PhantomData, merchant_id, customer_id: None, + tenant_id: state.tenant.tenant_id.clone(), connector_customer: None, connector: authentication_connector_name, payment_id: common_utils::id_type::PaymentId::get_irrelevant_id("authentication") diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index c413208b208..e6ff3fdbfca 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -47,7 +47,7 @@ impl ConstructFlowSpecificData( &self, - _state: &SessionState, + state: &SessionState, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -75,6 +75,7 @@ impl ConstructFlowSpecificData( let router_data = RouterData { flow: std::marker::PhantomData, merchant_id: merchant_account.get_id().clone(), + tenant_id: state.tenant.tenant_id.clone(), connector, payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), attempt_id: payment_attempt.attempt_id.clone(), diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index 5e354031185..21c5de5ed3b 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -45,7 +45,7 @@ impl ConstructFlowSpecificData( &self, - _state: &SessionState, + state: &SessionState, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -69,6 +69,7 @@ impl ConstructFlowSpecificData( &self, - _state: &SessionState, + state: &SessionState, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -69,6 +69,7 @@ impl ConstructFlowSpecificData( &self, - _state: &SessionState, + state: &SessionState, connector_id: &str, merchant_account: &domain::MerchantAccount, _key_store: &domain::MerchantKeyStore, @@ -77,6 +77,7 @@ impl let router_data = RouterData { flow: std::marker::PhantomData, merchant_id: merchant_account.get_id().clone(), + tenant_id: state.tenant.tenant_id.clone(), customer_id, connector: connector_id.to_string(), payment_id: self.payment_intent.payment_id.get_string_repr().to_owned(), diff --git a/crates/router/src/core/mandate.rs b/crates/router/src/core/mandate.rs index 15ceb9b1da2..414bb00e76e 100644 --- a/crates/router/src/core/mandate.rs +++ b/crates/router/src/core/mandate.rs @@ -110,6 +110,7 @@ pub async fn revoke_mandate( > = connector_data.connector.get_connector_integration(); let router_data = utils::construct_mandate_revoke_router_data( + &state, merchant_connector_account, &merchant_account, mandate.clone(), diff --git a/crates/router/src/core/mandate/utils.rs b/crates/router/src/core/mandate/utils.rs index 5418d7b7a70..95736604fc5 100644 --- a/crates/router/src/core/mandate/utils.rs +++ b/crates/router/src/core/mandate/utils.rs @@ -7,6 +7,7 @@ use error_stack::ResultExt; use crate::{ core::{errors, payments::helpers}, types::{self, domain, PaymentAddress}, + SessionState, }; const IRRELEVANT_ATTEMPT_ID_IN_MANDATE_REVOKE_FLOW: &str = @@ -16,6 +17,7 @@ const IRRELEVANT_CONNECTOR_REQUEST_REFERENCE_ID_IN_MANDATE_REVOKE_FLOW: &str = "irrelevant_connector_request_reference_id_in_mandate_revoke_flow"; pub async fn construct_mandate_revoke_router_data( + state: &SessionState, merchant_connector_account: helpers::MerchantConnectorAccountType, merchant_account: &domain::MerchantAccount, mandate: Mandate, @@ -28,6 +30,7 @@ pub async fn construct_mandate_revoke_router_data( flow: PhantomData, merchant_id: merchant_account.get_id().clone(), customer_id: Some(mandate.customer_id), + tenant_id: state.tenant.tenant_id.clone(), connector_customer: None, connector: mandate.connector, payment_id: mandate diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index b2c90e03b58..7f6e8c7fc86 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4035,6 +4035,7 @@ pub fn router_data_type_conversion( request, response, merchant_id: router_data.merchant_id, + tenant_id: router_data.tenant_id, address: router_data.address, amount_captured: router_data.amount_captured, minor_amount_captured: router_data.minor_amount_captured, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 79bcfb6a497..c6686912193 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -122,6 +122,7 @@ where .payment_id .get_string_repr() .to_owned(), + tenant_id: state.tenant.tenant_id.clone(), attempt_id: payment_data.payment_attempt.get_id().to_owned(), status: payment_data.payment_attempt.status, payment_method: diesel_models::enums::PaymentMethod::default(), @@ -301,6 +302,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( let router_data = types::RouterData { flow: PhantomData, merchant_id: merchant_account.get_id().clone(), + tenant_id: state.tenant.tenant_id.clone(), // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. customer_id, connector: connector_id.to_owned(), @@ -464,6 +466,7 @@ pub async fn construct_payment_router_data_for_capture<'a>( // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. customer_id, connector: connector_id.to_owned(), + tenant_id: state.tenant.tenant_id.clone(), // TODO: evaluate why we need payment id at the connector level. We already have connector reference id payment_id: payment_data .payment_attempt @@ -599,6 +602,7 @@ pub async fn construct_router_data_for_psync<'a>( merchant_id: merchant_account.get_id().clone(), // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. customer_id, + tenant_id: state.tenant.tenant_id.clone(), connector: connector_id.to_owned(), // TODO: evaluate why we need payment id at the connector level. We already have connector reference id payment_id: payment_intent.id.get_string_repr().to_owned(), @@ -662,7 +666,7 @@ pub async fn construct_router_data_for_psync<'a>( #[instrument(skip_all)] #[allow(clippy::too_many_arguments)] pub async fn construct_payment_router_data_for_sdk_session<'a>( - _state: &'a SessionState, + state: &'a SessionState, payment_data: hyperswitch_domain_models::payments::PaymentIntentData, connector_id: &str, merchant_account: &domain::MerchantAccount, @@ -756,6 +760,7 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>( // TODO: evaluate why we need customer id at the connector level. We already have connector customer id. customer_id, connector: connector_id.to_owned(), + tenant_id: state.tenant.tenant_id.clone(), // TODO: evaluate why we need payment id at the connector level. We already have connector reference id payment_id: payment_data.payment_intent.id.get_string_repr().to_owned(), // TODO: evaluate why we need attempt id at the connector level. We already have connector reference id @@ -944,6 +949,7 @@ where flow: PhantomData, merchant_id: merchant_account.get_id().clone(), customer_id, + tenant_id: state.tenant.tenant_id.clone(), connector: connector_id.to_owned(), payment_id: payment_data .payment_attempt diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 3c8432795a3..11be2ebfb5b 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -1216,9 +1216,13 @@ pub async fn create_recipient( ); if should_call_connector { // 1. Form router data - let router_data = - core_utils::construct_payout_router_data(connector_data, merchant_account, payout_data) - .await?; + let router_data = core_utils::construct_payout_router_data( + state, + connector_data, + merchant_account, + payout_data, + ) + .await?; // 2. Fetch connector integration details let connector_integration: services::BoxedPayoutConnectorIntegrationInterface< @@ -1396,9 +1400,13 @@ pub async fn check_payout_eligibility( payout_data: &mut PayoutData, ) -> RouterResult<()> { // 1. Form Router data - let router_data = - core_utils::construct_payout_router_data(connector_data, merchant_account, payout_data) - .await?; + let router_data = core_utils::construct_payout_router_data( + state, + connector_data, + merchant_account, + payout_data, + ) + .await?; // 2. Fetch connector integration details let connector_integration: services::BoxedPayoutConnectorIntegrationInterface< @@ -1594,9 +1602,13 @@ pub async fn create_payout( payout_data: &mut PayoutData, ) -> RouterResult<()> { // 1. Form Router data - let mut router_data = - core_utils::construct_payout_router_data(connector_data, merchant_account, payout_data) - .await?; + let mut router_data = core_utils::construct_payout_router_data( + state, + connector_data, + merchant_account, + payout_data, + ) + .await?; // 2. Get/Create access token access_token::create_access_token( @@ -1808,9 +1820,13 @@ pub async fn create_payout_retrieve( payout_data: &mut PayoutData, ) -> RouterResult<()> { // 1. Form Router data - let mut router_data = - core_utils::construct_payout_router_data(connector_data, merchant_account, payout_data) - .await?; + let mut router_data = core_utils::construct_payout_router_data( + state, + connector_data, + merchant_account, + payout_data, + ) + .await?; // 2. Get/Create access token access_token::create_access_token( @@ -1964,9 +1980,13 @@ pub async fn create_recipient_disburse_account( payout_data: &mut PayoutData, ) -> RouterResult<()> { // 1. Form Router data - let router_data = - core_utils::construct_payout_router_data(connector_data, merchant_account, payout_data) - .await?; + let router_data = core_utils::construct_payout_router_data( + state, + connector_data, + merchant_account, + payout_data, + ) + .await?; // 2. Fetch connector integration details let connector_integration: services::BoxedPayoutConnectorIntegrationInterface< @@ -2067,9 +2087,13 @@ pub async fn cancel_payout( payout_data: &mut PayoutData, ) -> RouterResult<()> { // 1. Form Router data - let router_data = - core_utils::construct_payout_router_data(connector_data, merchant_account, payout_data) - .await?; + let router_data = core_utils::construct_payout_router_data( + state, + connector_data, + merchant_account, + payout_data, + ) + .await?; // 2. Fetch connector integration details let connector_integration: services::BoxedPayoutConnectorIntegrationInterface< @@ -2191,9 +2215,13 @@ pub async fn fulfill_payout( payout_data: &mut PayoutData, ) -> RouterResult<()> { // 1. Form Router data - let mut router_data = - core_utils::construct_payout_router_data(connector_data, merchant_account, payout_data) - .await?; + let mut router_data = core_utils::construct_payout_router_data( + state, + connector_data, + merchant_account, + payout_data, + ) + .await?; // 2. Get/Create access token access_token::create_access_token( diff --git a/crates/router/src/core/relay/utils.rs b/crates/router/src/core/relay/utils.rs index 946f8df7d64..0829934fdc7 100644 --- a/crates/router/src/core/relay/utils.rs +++ b/crates/router/src/core/relay/utils.rs @@ -71,6 +71,7 @@ pub async fn construct_relay_refund_router_data<'a, F>( flow: std::marker::PhantomData, merchant_id: merchant_id.clone(), customer_id: None, + tenant_id: state.tenant.tenant_id.clone(), connector: connector_name.to_string(), payment_id: IRRELEVANT_PAYMENT_INTENT_ID.to_string(), attempt_id: IRRELEVANT_PAYMENT_ATTEMPT_ID.to_string(), diff --git a/crates/router/src/core/unified_authentication_service.rs b/crates/router/src/core/unified_authentication_service.rs index 5ed1b63d721..2afd8477f1f 100644 --- a/crates/router/src/core/unified_authentication_service.rs +++ b/crates/router/src/core/unified_authentication_service.rs @@ -43,6 +43,7 @@ impl UnifiedAuthenticationService for ClickToPay { let pre_auth_router_data: hyperswitch_domain_models::types::UasPreAuthenticationRouterData = utils::construct_uas_router_data( + state, connector_name.to_string(), payment_method, payment_data.payment_attempt.merchant_id.clone(), @@ -81,6 +82,7 @@ impl UnifiedAuthenticationService for ClickToPay { let post_authentication_data = UasPostAuthenticationRequestData {}; let post_auth_router_data: hyperswitch_domain_models::types::UasPostAuthenticationRouterData = utils::construct_uas_router_data( + state, connector_name.to_string(), payment_method, payment_data.payment_attempt.merchant_id.clone(), diff --git a/crates/router/src/core/unified_authentication_service/utils.rs b/crates/router/src/core/unified_authentication_service/utils.rs index fe4f864604e..dfa56628c87 100644 --- a/crates/router/src/core/unified_authentication_service/utils.rs +++ b/crates/router/src/core/unified_authentication_service/utils.rs @@ -102,7 +102,9 @@ where Ok(router_data) } +#[allow(clippy::too_many_arguments)] pub fn construct_uas_router_data( + state: &SessionState, authentication_connector_name: String, payment_method: PaymentMethod, merchant_id: common_utils::id_type::MerchantId, @@ -125,6 +127,7 @@ pub fn construct_uas_router_data( payment_id: common_utils::id_type::PaymentId::get_irrelevant_id("authentication") .get_string_repr() .to_owned(), + tenant_id: state.tenant.tenant_id.clone(), attempt_id: IRRELEVANT_ATTEMPT_ID_IN_AUTHENTICATION_FLOW.to_owned(), status: common_enums::AttemptStatus::default(), payment_method, diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index c441e32581e..fac89112df8 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -54,6 +54,7 @@ const IRRELEVANT_ATTEMPT_ID_IN_DISPUTE_FLOW: &str = "irrelevant_attempt_id_in_di #[cfg(all(feature = "payouts", feature = "v2", feature = "customer_v2"))] #[instrument(skip_all)] pub async fn construct_payout_router_data<'a, F>( + _state: &SessionState, _connector_data: &api::ConnectorData, _merchant_account: &domain::MerchantAccount, _payout_data: &mut PayoutData, @@ -68,6 +69,7 @@ pub async fn construct_payout_router_data<'a, F>( ))] #[instrument(skip_all)] pub async fn construct_payout_router_data<'a, F>( + state: &SessionState, connector_data: &api::ConnectorData, merchant_account: &domain::MerchantAccount, payout_data: &mut PayoutData, @@ -152,6 +154,7 @@ pub async fn construct_payout_router_data<'a, F>( flow: PhantomData, merchant_id: merchant_account.get_id().to_owned(), customer_id: customer_details.to_owned().map(|c| c.customer_id), + tenant_id: state.tenant.tenant_id.clone(), connector_customer: connector_customer_id, connector: connector_name.to_string(), payment_id: common_utils::id_type::PaymentId::get_irrelevant_id("payout") @@ -330,6 +333,7 @@ pub async fn construct_refund_router_data<'a, F>( flow: PhantomData, merchant_id: merchant_account.get_id().clone(), customer_id: payment_intent.customer_id.to_owned(), + tenant_id: state.tenant.tenant_id.clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), attempt_id: payment_attempt.attempt_id.clone(), @@ -652,6 +656,7 @@ pub async fn construct_accept_dispute_router_data<'a>( flow: PhantomData, merchant_id: merchant_account.get_id().clone(), connector: dispute.connector.to_string(), + tenant_id: state.tenant.tenant_id.clone(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, @@ -753,6 +758,7 @@ pub async fn construct_submit_evidence_router_data<'a>( merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), + tenant_id: state.tenant.tenant_id.clone(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, payment_method, @@ -851,6 +857,7 @@ pub async fn construct_upload_file_router_data<'a>( merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), + tenant_id: state.tenant.tenant_id.clone(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, payment_method, @@ -978,6 +985,7 @@ pub async fn construct_payments_dynamic_tax_calculation_router_data<'a, F: Clone connector: merchant_connector_account.connector_name.clone(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), attempt_id: payment_attempt.attempt_id.clone(), + tenant_id: state.tenant.tenant_id.clone(), status: payment_attempt.status, payment_method: diesel_models::enums::PaymentMethod::default(), connector_auth_type, @@ -1076,6 +1084,7 @@ pub async fn construct_defend_dispute_router_data<'a>( merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), payment_id: payment_attempt.payment_id.get_string_repr().to_owned(), + tenant_id: state.tenant.tenant_id.clone(), attempt_id: payment_attempt.attempt_id.clone(), status: payment_attempt.status, payment_method, @@ -1169,6 +1178,7 @@ pub async fn construct_retrieve_file_router_data<'a>( flow: PhantomData, merchant_id: merchant_account.get_id().clone(), connector: connector_id.to_string(), + tenant_id: state.tenant.tenant_id.clone(), customer_id: None, connector_customer: None, payment_id: common_utils::id_type::PaymentId::get_irrelevant_id("dispute") diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index ff9849958b5..474a0c628e2 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -1658,6 +1658,7 @@ async fn verify_webhook_source_verification_call( .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let router_data = construct_webhook_router_data( + state, connector_name, merchant_connector_account, merchant_account, diff --git a/crates/router/src/core/webhooks/incoming_v2.rs b/crates/router/src/core/webhooks/incoming_v2.rs index 569cd330a07..39b7091e73c 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -665,6 +665,7 @@ async fn verify_webhook_source_verification_call( .change_context(errors::ConnectorError::WebhookSourceVerificationFailed)?; let router_data = construct_webhook_router_data( + state, connector_name, merchant_connector_account, merchant_account, diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index b21ec0056c1..256fae78d4a 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -11,6 +11,7 @@ use crate::{ db::{get_and_deserialize_key, StorageInterface}, services::logger, types::{self, api, domain, PaymentAddress}, + SessionState, }; const IRRELEVANT_ATTEMPT_ID_IN_SOURCE_VERIFICATION_FLOW: &str = @@ -57,6 +58,7 @@ pub async fn is_webhook_event_disabled( } pub async fn construct_webhook_router_data<'a>( + state: &SessionState, connector_name: &str, merchant_connector_account: domain::MerchantConnectorAccount, merchant_account: &domain::MerchantAccount, @@ -74,6 +76,7 @@ pub async fn construct_webhook_router_data<'a>( merchant_id: merchant_account.get_id().clone(), connector: connector_name.to_string(), customer_id: None, + tenant_id: state.tenant.tenant_id.clone(), payment_id: common_utils::id_type::PaymentId::get_irrelevant_id("source_verification_flow") .get_string_repr() .to_owned(), diff --git a/crates/router/src/services/conversion_impls.rs b/crates/router/src/services/conversion_impls.rs index 9bb88cf4ecc..8572add041e 100644 --- a/crates/router/src/services/conversion_impls.rs +++ b/crates/router/src/services/conversion_impls.rs @@ -1,3 +1,4 @@ +use common_utils::id_type; use error_stack::ResultExt; #[cfg(feature = "frm")] use hyperswitch_domain_models::router_data_v2::flow_common_types::FrmFlowData; @@ -23,17 +24,19 @@ fn get_irrelevant_id_string(id_name: &str, flow_name: &str) -> String { format!("irrelevant {id_name} in {flow_name} flow") } fn get_default_router_data( + tenant_id: id_type::TenantId, flow_name: &str, request: Req, response: Result, ) -> RouterData { RouterData { + tenant_id, flow: std::marker::PhantomData, - merchant_id: common_utils::id_type::MerchantId::get_irrelevant_merchant_id(), + merchant_id: id_type::MerchantId::get_irrelevant_merchant_id(), customer_id: None, connector_customer: None, connector: get_irrelevant_id_string("connector", flow_name), - payment_id: common_utils::id_type::PaymentId::get_irrelevant_id(flow_name) + payment_id: id_type::PaymentId::get_irrelevant_id(flow_name) .get_string_repr() .to_owned(), attempt_id: get_irrelevant_id_string("attempt_id", flow_name), @@ -93,6 +96,7 @@ impl RouterDataConversion for AccessTo let resource_common_data = Self {}; Ok(RouterDataV2 { flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), request: old_router_data.request.clone(), @@ -109,7 +113,12 @@ impl RouterDataConversion for AccessTo let Self {} = new_router_data.resource_common_data; let request = new_router_data.request.clone(); let response = new_router_data.response.clone(); - let router_data = get_default_router_data("access token", request, response); + let router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "access token", + request, + response, + ); Ok(router_data) } } @@ -153,6 +162,7 @@ impl RouterDataConversion for PaymentF }; Ok(RouterDataV2 { flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), request: old_router_data.request.clone(), @@ -196,8 +206,12 @@ impl RouterDataConversion for PaymentF connector_response, payment_method_status, } = new_router_data.resource_common_data; - let mut router_data = - get_default_router_data("payment", new_router_data.request, new_router_data.response); + let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "payment", + new_router_data.request, + new_router_data.response, + ); router_data.merchant_id = merchant_id; router_data.customer_id = customer_id; router_data.connector_customer = connector_customer; @@ -256,6 +270,7 @@ impl RouterDataConversion for RefundFl }; Ok(RouterDataV2 { flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), request: old_router_data.request.clone(), @@ -282,8 +297,12 @@ impl RouterDataConversion for RefundFl connector_request_reference_id, refund_id, } = new_router_data.resource_common_data; - let mut router_data = - get_default_router_data("refund", new_router_data.request, new_router_data.response); + let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "refund", + new_router_data.request, + new_router_data.response, + ); router_data.merchant_id = merchant_id; router_data.customer_id = customer_id; router_data.payment_id = payment_id; @@ -323,6 +342,7 @@ impl RouterDataConversion for Disputes }; Ok(RouterDataV2 { flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), request: old_router_data.request.clone(), @@ -348,6 +368,7 @@ impl RouterDataConversion for Disputes dispute_id, } = new_router_data.resource_common_data; let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), "Disputes", new_router_data.request, new_router_data.response, @@ -386,6 +407,7 @@ impl RouterDataConversion for FrmFlowD minor_amount_captured: old_router_data.minor_amount_captured, }; Ok(RouterDataV2 { + tenant_id: old_router_data.tenant_id.clone(), flow: std::marker::PhantomData, resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), @@ -412,8 +434,12 @@ impl RouterDataConversion for FrmFlowD amount_captured, minor_amount_captured, } = new_router_data.resource_common_data; - let mut router_data = - get_default_router_data("frm", new_router_data.request, new_router_data.response); + let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "frm", + new_router_data.request, + new_router_data.response, + ); router_data.merchant_id = merchant_id; router_data.payment_id = payment_id; @@ -446,6 +472,7 @@ impl RouterDataConversion for FilesFlo }; Ok(RouterDataV2 { flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), request: old_router_data.request.clone(), @@ -466,8 +493,12 @@ impl RouterDataConversion for FilesFlo connector_meta_data, connector_request_reference_id, } = new_router_data.resource_common_data; - let mut router_data = - get_default_router_data("files", new_router_data.request, new_router_data.response); + let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "files", + new_router_data.request, + new_router_data.response, + ); router_data.merchant_id = merchant_id; router_data.payment_id = payment_id; router_data.attempt_id = attempt_id; @@ -489,6 +520,7 @@ impl RouterDataConversion for WebhookS merchant_id: old_router_data.merchant_id.clone(), }; Ok(RouterDataV2 { + tenant_id: old_router_data.tenant_id.clone(), flow: std::marker::PhantomData, resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), @@ -505,6 +537,7 @@ impl RouterDataConversion for WebhookS { let Self { merchant_id } = new_router_data.resource_common_data; let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), "webhook source verify", new_router_data.request, new_router_data.response, @@ -532,6 +565,7 @@ impl RouterDataConversion for MandateR }; Ok(RouterDataV2 { flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), request: old_router_data.request.clone(), @@ -551,6 +585,7 @@ impl RouterDataConversion for MandateR payment_id, } = new_router_data.resource_common_data; let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), "mandate revoke", new_router_data.request, new_router_data.response, @@ -559,7 +594,7 @@ impl RouterDataConversion for MandateR router_data.customer_id = Some(customer_id); router_data.payment_id = payment_id .unwrap_or_else(|| { - common_utils::id_type::PaymentId::get_irrelevant_id("mandate revoke") + id_type::PaymentId::get_irrelevant_id("mandate revoke") .get_string_repr() .to_owned() }) @@ -588,6 +623,7 @@ impl RouterDataConversion for PayoutFl quote_id: old_router_data.quote_id.clone(), }; Ok(RouterDataV2 { + tenant_id: old_router_data.tenant_id.clone(), flow: std::marker::PhantomData, resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), @@ -613,8 +649,12 @@ impl RouterDataConversion for PayoutFl payout_method_data, quote_id, } = new_router_data.resource_common_data; - let mut router_data = - get_default_router_data("payout", new_router_data.request, new_router_data.response); + let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "payout", + new_router_data.request, + new_router_data.response, + ); router_data.merchant_id = merchant_id; router_data.customer_id = customer_id; router_data.connector_customer = connector_customer; @@ -642,6 +682,7 @@ impl RouterDataConversion address: old_router_data.address.clone(), }; Ok(RouterDataV2 { + tenant_id: old_router_data.tenant_id.clone(), flow: std::marker::PhantomData, resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), @@ -662,6 +703,7 @@ impl RouterDataConversion address, } = new_router_data.resource_common_data; let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), "external authentication", new_router_data.request, new_router_data.response, @@ -692,6 +734,7 @@ impl RouterDataConversion for UasFlowD }; Ok(RouterDataV2 { flow: std::marker::PhantomData, + tenant_id: old_router_data.tenant_id.clone(), resource_common_data, connector_auth_type: old_router_data.connector_auth_type.clone(), request: old_router_data.request.clone(), @@ -709,8 +752,12 @@ impl RouterDataConversion for UasFlowD authenticate_by, source_authentication_id, } = new_router_data.resource_common_data; - let mut router_data = - get_default_router_data("uas", new_router_data.request, new_router_data.response); + let mut router_data = get_default_router_data( + new_router_data.tenant_id.clone(), + "uas", + new_router_data.request, + new_router_data.response, + ); router_data.connector = authenticate_by; router_data.authentication_id = Some(source_authentication_id); Ok(router_data) diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index f9c9e6edb26..2ae2af92478 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -914,6 +914,7 @@ impl ForeignFrom<(&RouterData, T2) merchant_id: data.merchant_id.clone(), connector: data.connector.clone(), attempt_id: data.attempt_id.clone(), + tenant_id: data.tenant_id.clone(), status: data.status, payment_method: data.payment_method, connector_auth_type: data.connector_auth_type.clone(), @@ -983,6 +984,7 @@ impl merchant_id: data.merchant_id.clone(), connector: data.connector.clone(), attempt_id: data.attempt_id.clone(), + tenant_id: data.tenant_id.clone(), status: data.status, payment_method: data.payment_method, connector_auth_type: data.connector_auth_type.clone(), diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 47d8add58c3..4d248bbf0b4 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -65,6 +65,7 @@ impl VerifyConnectorData { fn get_router_data( &self, + state: &SessionState, request_data: R1, access_token: Option, ) -> types::RouterData { @@ -81,6 +82,7 @@ impl VerifyConnectorData { attempt_id: attempt_id.clone(), description: None, customer_id: None, + tenant_id: state.tenant.tenant_id.clone(), merchant_id: common_utils::id_type::MerchantId::default(), reference_id: None, access_token, @@ -132,7 +134,7 @@ pub trait VerifyConnector { ) -> errors::RouterResponse<()> { let authorize_data = connector_data.get_payment_authorize_data(); let access_token = Self::get_access_token(state, connector_data.clone()).await?; - let router_data = connector_data.get_router_data(authorize_data, access_token); + let router_data = connector_data.get_router_data(state, authorize_data, access_token); let request = connector_data .connector diff --git a/crates/router/src/types/api/verify_connector/paypal.rs b/crates/router/src/types/api/verify_connector/paypal.rs index f7de86ceebd..d7c4fca748e 100644 --- a/crates/router/src/types/api/verify_connector/paypal.rs +++ b/crates/router/src/types/api/verify_connector/paypal.rs @@ -17,7 +17,7 @@ impl VerifyConnector for connector::Paypal { ) -> errors::CustomResult, errors::ApiErrorResponse> { let token_data: types::AccessTokenRequestData = connector_data.connector_auth.clone().try_into()?; - let router_data = connector_data.get_router_data(token_data, None); + let router_data = connector_data.get_router_data(state, token_data, None); let request = connector_data .connector diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 645b81e7069..8b3372d7dec 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -27,6 +27,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { flow: PhantomData, merchant_id, customer_id: Some(id_type::CustomerId::try_from(Cow::from("aci")).unwrap()), + tenant_id: id_type::TenantId::try_from_string("public".to_string()).unwrap(), connector: "aci".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), attempt_id: uuid::Uuid::new_v4().to_string(), @@ -145,6 +146,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { flow: PhantomData, merchant_id, customer_id: Some(id_type::CustomerId::try_from(Cow::from("aci")).unwrap()), + tenant_id: id_type::TenantId::try_from_string("public".to_string()).unwrap(), connector: "aci".to_string(), payment_id: uuid::Uuid::new_v4().to_string(), attempt_id: uuid::Uuid::new_v4().to_string(), diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 03ea1f42e83..2cf3dc965fd 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -490,6 +490,8 @@ pub trait ConnectorActions: Connector { merchant_id, customer_id: Some(common_utils::generate_customer_id_of_default_length()), connector: self.get_name(), + tenant_id: common_utils::id_type::TenantId::try_from_string("public".to_string()) + .unwrap(), payment_id: uuid::Uuid::new_v4().to_string(), attempt_id: uuid::Uuid::new_v4().to_string(), status: enums::AttemptStatus::default(), From 228a36deeb02049999e5ad10f3511def6561d3ca Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 9 Jan 2025 09:53:39 +0000 Subject: [PATCH 29/30] chore(version): 2025.01.09.1 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8be15b40fc..68a584ed68d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.09.1 + +### Bug Fixes + +- **dummyconnector:** Add tenant id in dummyconnector requests ([#7008](https://github.com/juspay/hyperswitch/pull/7008)) ([`9c983b6`](https://github.com/juspay/hyperswitch/commit/9c983b68bd834e33c5c57d1d050aa5d41cb10f56)) + +**Full Changelog:** [`2025.01.09.0...2025.01.09.1`](https://github.com/juspay/hyperswitch/compare/2025.01.09.0...2025.01.09.1) + +- - - + ## 2025.01.09.0 ### Features From d8d8c400bbda49b9a0cd5edbe37e929ae6d38eb4 Mon Sep 17 00:00:00 2001 From: sumitdahiya125 Date: Thu, 9 Jan 2025 15:02:36 +0530 Subject: [PATCH 30/30] test(cypress): add test for In Memory Cache (#6961) Co-authored-by: Sumit Kumar Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Kartikeya Hegde --- ....js => 00025-BusinessProfileConfigs.cy.js} | 0 .../00028-MemoryCacheConfigs.cy.js | 35 +++++++ cypress-tests/cypress/support/commands.js | 92 +++++++++++++++++++ 3 files changed, 127 insertions(+) rename cypress-tests/cypress/e2e/PaymentTest/{00025-ConfigTest.cy.js => 00025-BusinessProfileConfigs.cy.js} (100%) create mode 100644 cypress-tests/cypress/e2e/PaymentTest/00028-MemoryCacheConfigs.cy.js diff --git a/cypress-tests/cypress/e2e/PaymentTest/00025-ConfigTest.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00025-BusinessProfileConfigs.cy.js similarity index 100% rename from cypress-tests/cypress/e2e/PaymentTest/00025-ConfigTest.cy.js rename to cypress-tests/cypress/e2e/PaymentTest/00025-BusinessProfileConfigs.cy.js diff --git a/cypress-tests/cypress/e2e/PaymentTest/00028-MemoryCacheConfigs.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00028-MemoryCacheConfigs.cy.js new file mode 100644 index 00000000000..afa7ceda0d1 --- /dev/null +++ b/cypress-tests/cypress/e2e/PaymentTest/00028-MemoryCacheConfigs.cy.js @@ -0,0 +1,35 @@ +import State from "../../utils/State"; + +let globalState; + +describe("In Memory Cache Test", () => { + before("seed global state", () => { + cy.task("getGlobalState").then((state) => { + globalState = new State(state); + }); + }); + + after("flush global state", () => { + cy.task("setGlobalState", globalState.data); + }); + + context("Config flows", () => { + const key = "test-key"; + const value = "test value"; + const newValue = "new test value"; + + it("Create Configs", () => { + cy.createConfigs(globalState, key, value); + cy.fetchConfigs(globalState, key, value); + }); + + it("Update Configs", () => { + cy.updateConfigs(globalState, key, newValue); + cy.fetchConfigs(globalState, key, newValue); + }); + + it("delete configs", () => { + cy.deleteConfigs(globalState, key, newValue); + }); + }); +}); diff --git a/cypress-tests/cypress/support/commands.js b/cypress-tests/cypress/support/commands.js index e2e83edaf0c..1b7b41a035d 100644 --- a/cypress-tests/cypress/support/commands.js +++ b/cypress-tests/cypress/support/commands.js @@ -3369,3 +3369,95 @@ Cypress.Commands.add("incrementalAuth", (globalState, data) => { } }); }); + +Cypress.Commands.add("createConfigs", (globalState, key, value) => { + const base_url = globalState.get("baseUrl"); + const api_key = globalState.get("adminApiKey"); + + cy.request({ + method: "POST", + url: `${base_url}/configs/`, + headers: { + "Content-Type": "application/json", + "api-key": api_key, + }, + body: { + key: key, + value: value, + }, + failOnStatusCode: false, + }).then((response) => { + logRequestId(response.headers["x-request-id"]); + + expect(response.status).to.equal(200); + expect(response.body).to.have.property("key").to.equal(key); + expect(response.body).to.have.property("value").to.equal(value); + }); +}); + +Cypress.Commands.add("fetchConfigs", (globalState, key, value) => { + const base_url = globalState.get("baseUrl"); + const api_key = globalState.get("adminApiKey"); + + cy.request({ + method: "GET", + url: `${base_url}/configs/${key}`, + headers: { + "Content-Type": "application/json", + "api-key": api_key, + }, + failOnStatusCode: false, + }).then((response) => { + logRequestId(response.headers["x-request-id"]); + + expect(response.status).to.equal(200); + expect(response.body).to.have.property("key").to.equal(key); + expect(response.body).to.have.property("value").to.equal(value); + }); +}); + +Cypress.Commands.add("updateConfigs", (globalState, key, value) => { + const base_url = globalState.get("baseUrl"); + const api_key = globalState.get("adminApiKey"); + + cy.request({ + method: "POST", + url: `${base_url}/configs/${key}`, + headers: { + "Content-Type": "application/json", + "api-key": api_key, + }, + body: { + key: key, + value: value, + }, + failOnStatusCode: false, + }).then((response) => { + logRequestId(response.headers["x-request-id"]); + + expect(response.status).to.equal(200); + expect(response.body).to.have.property("key").to.equal(key); + expect(response.body).to.have.property("value").to.equal(value); + }); +}); + +Cypress.Commands.add("deleteConfigs", (globalState, key, value) => { + const base_url = globalState.get("baseUrl"); + const api_key = globalState.get("adminApiKey"); + + cy.request({ + method: "DELETE", + url: `${base_url}/configs/${key}`, + headers: { + "Content-Type": "application/json", + "api-key": api_key, + }, + failOnStatusCode: false, + }).then((response) => { + logRequestId(response.headers["x-request-id"]); + + expect(response.status).to.equal(200); + expect(response.body).to.have.property("key").to.equal(key); + expect(response.body).to.have.property("value").to.equal(value); + }); +});