diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index bc83c2388f7e..070682b9c3ad 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/CHANGELOG.md b/CHANGELOG.md index 3de8bcfeb93a..db7b6894e215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,93 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.01.10.0 + +### Testing + +- **cypress:** Add test for In Memory Cache ([#6961](https://github.com/juspay/hyperswitch/pull/6961)) ([`d8d8c40`](https://github.com/juspay/hyperswitch/commit/d8d8c400bbda49b9a0cd5edbe37e929ae6d38eb4)) + +**Full Changelog:** [`2025.01.09.1...2025.01.10.0`](https://github.com/juspay/hyperswitch/compare/2025.01.09.1...2025.01.10.0) + +- - - + +## 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 + +- **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 + +- **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 + +- **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 + +- 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 + +- **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 diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index df7e96431dbc..5e0e07bb6b46 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/config/config.example.toml b/config/config.example.toml index 999266321144..eb30435d5f8f 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 967b847dae51..809edf1bac60 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 4c9b8516b5ad..e3631b96b173 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 75699d0a9674..6f95380d2db3 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/api_models/src/refunds.rs b/crates/api_models/src/refunds.rs index 36a92e3ab03d..ad09c333ea71 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/api_models/src/webhooks.rs b/crates/api_models/src/webhooks.rs index e6f4065eb7a9..f99e3a450b20 100644 --- a/crates/api_models/src/webhooks.rs +++ b/crates/api_models/src/webhooks.rs @@ -120,6 +120,10 @@ pub enum WebhookResponseTracker { status: common_enums::MandateStatus, }, NoEffect, + Relay { + relay_id: common_utils::id_type::RelayId, + status: common_enums::RelayStatus, + }, } impl WebhookResponseTracker { @@ -132,6 +136,7 @@ impl WebhookResponseTracker { Self::NoEffect | Self::Mandate { .. } => None, #[cfg(feature = "payouts")] Self::Payout { .. } => None, + Self::Relay { .. } => None, } } @@ -144,6 +149,7 @@ impl WebhookResponseTracker { Self::NoEffect | Self::Mandate { .. } => None, #[cfg(feature = "payouts")] Self::Payout { .. } => None, + Self::Relay { .. } => None, } } } diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 3b437b703bef..fdda26cc77c7 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/relay.rs b/crates/common_utils/src/id_type/relay.rs index 3ad64729fb73..c818671e0e5e 100644 --- a/crates/common_utils/src/id_type/relay.rs +++ b/crates/common_utils/src/id_type/relay.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + crate::id_type!( RelayId, "A type for relay_id that can be used for relay ids" @@ -11,3 +13,12 @@ crate::impl_queryable_id_type!(RelayId); crate::impl_to_sql_from_sql_id_type!(RelayId); crate::impl_debug_id_type!(RelayId); + +impl FromStr for RelayId { + type Err = error_stack::Report; + + fn from_str(s: &str) -> Result { + let cow_string = std::borrow::Cow::Owned(s.to_string()); + Self::try_from(cow_string) + } +} diff --git a/crates/common_utils/src/id_type/tenant.rs b/crates/common_utils/src/id_type/tenant.rs index 953bf82287af..0584496332a5 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 53761951791c..0dc05b22fd76 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 09d26bd91ef8..f18c66562070 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/diesel_models/src/query/relay.rs b/crates/diesel_models/src/query/relay.rs index 034446fe6b51..217f6fd734de 100644 --- a/crates/diesel_models/src/query/relay.rs +++ b/crates/diesel_models/src/query/relay.rs @@ -1,4 +1,4 @@ -use diesel::{associations::HasTable, ExpressionMethods}; +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; use super::generics; use crate::{ @@ -46,4 +46,18 @@ impl Relay { ) .await } + + pub async fn find_by_profile_id_connector_reference_id( + conn: &PgPooledConn, + profile_id: &common_utils::id_type::ProfileId, + connector_reference_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::profile_id + .eq(profile_id.to_owned()) + .and(dsl::connector_reference_id.eq(connector_reference_id.to_owned())), + ) + .await + } } diff --git a/crates/diesel_models/src/query/role.rs b/crates/diesel_models/src/query/role.rs index 6f6a1404ee2c..2ab58ec23821 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 bb07f6718245..01fa05e925a9 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/refund.rs b/crates/diesel_models/src/refund.rs index 758eba7255ae..b929481d64d5 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/role.rs b/crates/diesel_models/src/role.rs index 8199bd3979ce..16728801933c 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 37a39cb731a8..6e8bdb5ec26b 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, } } @@ -1308,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 3e27f29427bb..d38f684a44d2 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, } } @@ -1255,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/hyperswitch_connectors/src/connectors/airwallex/transformers.rs b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs index 075a7b070d6d..45a7b577a86b 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/hyperswitch_connectors/src/connectors/fiuu/transformers.rs b/crates/hyperswitch_connectors/src/connectors/fiuu/transformers.rs index 14ce13f971b6..e914454e5579 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, diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet.rs b/crates/hyperswitch_connectors/src/connectors/novalnet.rs index 99da9c5caced..0a47c8ccfba8 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet.rs @@ -28,7 +28,7 @@ use hyperswitch_domain_models::{ router_response_types::{PaymentsResponseData, RefundsResponseData}, types::{ PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, - PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData, }, }; use hyperswitch_interfaces::{ @@ -231,6 +231,79 @@ impl ConnectorIntegration impl ConnectorIntegration for Novalnet { + fn get_headers( + &self, + req: &SetupMandateRouterData, + connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &SetupMandateRouterData, + connectors: &Connectors, + ) -> CustomResult { + let endpoint = self.base_url(connectors); + Ok(format!("{}/payment", endpoint)) + } + + fn get_request_body( + &self, + req: &SetupMandateRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_req = novalnet::NovalnetPaymentsRequest::try_from(req)?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &SetupMandateRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + RequestBuilder::new() + .method(Method::Post) + .url(&types::SetupMandateType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::SetupMandateType::get_headers(self, req, connectors)?) + .set_body(types::SetupMandateType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &SetupMandateRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult { + let response: novalnet::NovalnetPaymentsResponse = res + .response + .parse_struct("Novalnet PaymentsAuthorizeResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + event_builder.map(|i| i.set_response_body(&response)); + router_env::logger::info!(connector_response=?response); + RouterData::try_from(ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } } impl ConnectorIntegration for Novalnet { diff --git a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs index 0a70393a13cd..d62d3d2a7554 100644 --- a/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/novalnet/transformers.rs @@ -17,7 +17,7 @@ use hyperswitch_domain_models::{ }, types::{ PaymentsAuthorizeRouterData, PaymentsCancelRouterData, PaymentsCaptureRouterData, - PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, + PaymentsSyncRouterData, RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData, }, }; use hyperswitch_interfaces::errors; @@ -28,8 +28,9 @@ use strum::Display; use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, utils::{ - self, ApplePay, PaymentsAuthorizeRequestData, PaymentsCancelRequestData, - PaymentsCaptureRequestData, PaymentsSyncRequestData, RefundsRequestData, RouterData as _, + self, AddressDetailsData, ApplePay, PaymentsAuthorizeRequestData, + PaymentsCancelRequestData, PaymentsCaptureRequestData, PaymentsSetupMandateRequestData, + PaymentsSyncRequestData, RefundsRequestData, RouterData as _, }, }; @@ -47,6 +48,19 @@ impl From<(StringMinorUnit, T)> for NovalnetRouterData { } } +const MINIMAL_CUSTOMER_DATA_PASSED: i64 = 1; +const CREATE_TOKEN_REQUIRED: i8 = 1; + +const TEST_MODE_ENABLED: i8 = 1; +const TEST_MODE_DISABLED: i8 = 0; + +fn get_test_mode(item: Option) -> i8 { + match item { + Some(true) => TEST_MODE_ENABLED, + Some(false) | None => TEST_MODE_DISABLED, + } +} + #[derive(Debug, Copy, Serialize, Deserialize, Clone)] pub enum NovalNetPaymentTypes { CREDITCARD, @@ -117,11 +131,18 @@ pub struct NovalnetCustom { lang: String, } +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum NovalNetAmount { + StringMinor(StringMinorUnit), + Int(i64), +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct NovalnetPaymentsRequestTransaction { test_mode: i8, payment_type: NovalNetPaymentTypes, - amount: StringMinorUnit, + amount: NovalNetAmount, currency: common_enums::Currency, order_no: String, payment_data: Option, @@ -171,10 +192,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym enums::AuthenticationType::ThreeDs => Some(1), enums::AuthenticationType::NoThreeDs => None, }; - let test_mode = match item.router_data.test_mode { - Some(true) => 1, - Some(false) | None => 0, - }; + let test_mode = get_test_mode(item.router_data.test_mode); let billing = NovalnetPaymentsRequestBilling { house_no: item.router_data.get_optional_billing_line1(), @@ -197,7 +215,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym mobile: item.router_data.get_optional_billing_phone_number(), billing: Some(billing), // no_nc is used to indicate if minimal customer data is passed or not - no_nc: 1, + no_nc: MINIMAL_CUSTOMER_DATA_PASSED, }; let lang = item @@ -209,7 +227,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym let hook_url = item.router_data.request.get_webhook_url()?; let return_url = item.router_data.request.get_router_return_url()?; let create_token = if item.router_data.request.is_mandate_payment() { - Some(1) + Some(CREATE_TOKEN_REQUIRED) } else { None }; @@ -234,7 +252,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym let transaction = NovalnetPaymentsRequestTransaction { test_mode, payment_type: NovalNetPaymentTypes::CREDITCARD, - amount: item.amount.clone(), + amount: NovalNetAmount::StringMinor(item.amount.clone()), currency: item.router_data.request.currency, order_no: item.router_data.connector_request_reference_id.clone(), hook_url: Some(hook_url), @@ -265,7 +283,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym let transaction = NovalnetPaymentsRequestTransaction { test_mode, payment_type: NovalNetPaymentTypes::GOOGLEPAY, - amount: item.amount.clone(), + amount: NovalNetAmount::StringMinor(item.amount.clone()), currency: item.router_data.request.currency, order_no: item.router_data.connector_request_reference_id.clone(), hook_url: Some(hook_url), @@ -287,7 +305,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym let transaction = NovalnetPaymentsRequestTransaction { test_mode, payment_type: NovalNetPaymentTypes::APPLEPAY, - amount: item.amount.clone(), + amount: NovalNetAmount::StringMinor(item.amount.clone()), currency: item.router_data.request.currency, order_no: item.router_data.connector_request_reference_id.clone(), hook_url: Some(hook_url), @@ -331,7 +349,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym let transaction = NovalnetPaymentsRequestTransaction { test_mode, payment_type: NovalNetPaymentTypes::PAYPAL, - amount: item.amount.clone(), + amount: NovalNetAmount::StringMinor(item.amount.clone()), currency: item.router_data.request.currency, order_no: item.router_data.connector_request_reference_id.clone(), hook_url: Some(hook_url), @@ -389,7 +407,7 @@ impl TryFrom<&NovalnetRouterData<&PaymentsAuthorizeRouterData>> for NovalnetPaym let transaction = NovalnetPaymentsRequestTransaction { test_mode, payment_type, - amount: item.amount.clone(), + amount: NovalNetAmount::StringMinor(item.amount.clone()), currency: item.router_data.request.currency, order_no: item.router_data.connector_request_reference_id.clone(), hook_url: Some(hook_url), @@ -1380,3 +1398,194 @@ pub fn get_novalnet_dispute_status(status: WebhookEventType) -> WebhookDisputeSt pub fn option_to_result(opt: Option) -> Result { opt.ok_or(errors::ConnectorError::WebhookBodyDecodingFailed) } + +impl TryFrom<&SetupMandateRouterData> for NovalnetPaymentsRequest { + type Error = error_stack::Report; + fn try_from(item: &SetupMandateRouterData) -> Result { + let auth = NovalnetAuthType::try_from(&item.connector_auth_type)?; + + let merchant = NovalnetPaymentsRequestMerchant { + signature: auth.product_activation_key, + tariff: auth.tariff_id, + }; + + let enforce_3d = match item.auth_type { + enums::AuthenticationType::ThreeDs => Some(1), + enums::AuthenticationType::NoThreeDs => None, + }; + let test_mode = get_test_mode(item.test_mode); + let req_address = item.get_billing_address()?.to_owned(); + + let billing = NovalnetPaymentsRequestBilling { + house_no: item.get_optional_billing_line1(), + street: item.get_optional_billing_line2(), + city: item.get_optional_billing_city().map(Secret::new), + zip: item.get_optional_billing_zip(), + country_code: item.get_optional_billing_country(), + }; + + let customer = NovalnetPaymentsRequestCustomer { + first_name: req_address.get_first_name()?.clone(), + last_name: req_address.get_last_name()?.clone(), + email: item.request.get_email()?.clone(), + mobile: item.get_optional_billing_phone_number(), + billing: Some(billing), + // no_nc is used to indicate if minimal customer data is passed or not + no_nc: MINIMAL_CUSTOMER_DATA_PASSED, + }; + + let lang = item + .request + .get_optional_language_from_browser_info() + .unwrap_or(consts::DEFAULT_LOCALE.to_string().to_string()); + + let custom = NovalnetCustom { lang }; + let hook_url = item.request.get_webhook_url()?; + let return_url = item.request.get_return_url()?; + let create_token = Some(CREATE_TOKEN_REQUIRED); + + match item.request.payment_method_data { + PaymentMethodData::Card(ref req_card) => { + let novalnet_card = NovalNetPaymentData::Card(NovalnetCard { + card_number: req_card.card_number.clone(), + card_expiry_month: req_card.card_exp_month.clone(), + card_expiry_year: req_card.card_exp_year.clone(), + card_cvc: req_card.card_cvc.clone(), + card_holder: req_address.get_full_name()?.clone(), + }); + + let transaction = NovalnetPaymentsRequestTransaction { + test_mode, + payment_type: NovalNetPaymentTypes::CREDITCARD, + amount: NovalNetAmount::Int(0), + currency: item.request.currency, + order_no: item.connector_request_reference_id.clone(), + hook_url: Some(hook_url), + return_url: Some(return_url.clone()), + error_return_url: Some(return_url.clone()), + payment_data: Some(novalnet_card), + enforce_3d, + create_token, + }; + + Ok(Self { + merchant, + transaction, + customer, + custom, + }) + } + + PaymentMethodData::Wallet(ref wallet_data) => match wallet_data { + WalletDataPaymentMethod::GooglePay(ref req_wallet) => { + let novalnet_google_pay: NovalNetPaymentData = + NovalNetPaymentData::GooglePay(NovalnetGooglePay { + wallet_data: Secret::new(req_wallet.tokenization_data.token.clone()), + }); + + let transaction = NovalnetPaymentsRequestTransaction { + test_mode, + payment_type: NovalNetPaymentTypes::GOOGLEPAY, + amount: NovalNetAmount::Int(0), + currency: item.request.currency, + order_no: item.connector_request_reference_id.clone(), + hook_url: Some(hook_url), + return_url: None, + error_return_url: None, + payment_data: Some(novalnet_google_pay), + enforce_3d, + create_token, + }; + + Ok(Self { + merchant, + transaction, + customer, + custom, + }) + } + WalletDataPaymentMethod::ApplePay(payment_method_data) => { + let transaction = NovalnetPaymentsRequestTransaction { + test_mode, + payment_type: NovalNetPaymentTypes::APPLEPAY, + amount: NovalNetAmount::Int(0), + currency: item.request.currency, + order_no: item.connector_request_reference_id.clone(), + hook_url: Some(hook_url), + return_url: None, + error_return_url: None, + payment_data: Some(NovalNetPaymentData::ApplePay(NovalnetApplePay { + wallet_data: Secret::new(payment_method_data.payment_data.clone()), + })), + enforce_3d: None, + create_token, + }; + + Ok(Self { + merchant, + transaction, + customer, + custom, + }) + } + WalletDataPaymentMethod::AliPayQr(_) + | WalletDataPaymentMethod::AliPayRedirect(_) + | WalletDataPaymentMethod::AliPayHkRedirect(_) + | WalletDataPaymentMethod::MomoRedirect(_) + | WalletDataPaymentMethod::KakaoPayRedirect(_) + | WalletDataPaymentMethod::GoPayRedirect(_) + | WalletDataPaymentMethod::GcashRedirect(_) + | WalletDataPaymentMethod::ApplePayRedirect(_) + | WalletDataPaymentMethod::ApplePayThirdPartySdk(_) + | WalletDataPaymentMethod::DanaRedirect {} + | WalletDataPaymentMethod::GooglePayRedirect(_) + | WalletDataPaymentMethod::GooglePayThirdPartySdk(_) + | WalletDataPaymentMethod::MbWayRedirect(_) + | WalletDataPaymentMethod::MobilePayRedirect(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("novalnet"), + ))? + } + WalletDataPaymentMethod::PaypalRedirect(_) => { + let transaction = NovalnetPaymentsRequestTransaction { + test_mode, + payment_type: NovalNetPaymentTypes::PAYPAL, + amount: NovalNetAmount::Int(0), + currency: item.request.currency, + order_no: item.connector_request_reference_id.clone(), + hook_url: Some(hook_url), + return_url: Some(return_url.clone()), + error_return_url: Some(return_url.clone()), + payment_data: None, + enforce_3d: None, + create_token, + }; + Ok(Self { + merchant, + transaction, + customer, + custom, + }) + } + WalletDataPaymentMethod::PaypalSdk(_) + | WalletDataPaymentMethod::Paze(_) + | WalletDataPaymentMethod::SamsungPay(_) + | WalletDataPaymentMethod::TwintRedirect {} + | WalletDataPaymentMethod::VippsRedirect {} + | WalletDataPaymentMethod::TouchNGoRedirect(_) + | WalletDataPaymentMethod::WeChatPayRedirect(_) + | WalletDataPaymentMethod::CashappQr(_) + | WalletDataPaymentMethod::SwishQr(_) + | WalletDataPaymentMethod::WeChatPayQr(_) + | WalletDataPaymentMethod::Mifinity(_) => { + Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("novalnet"), + ))? + } + }, + _ => Err(errors::ConnectorError::NotImplemented( + utils::get_unimplemented_payment_method_error_message("novalnet"), + ))?, + } + } +} diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index 9061a758beab..169e5ae146e4 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1595,6 +1595,9 @@ pub trait PaymentsSetupMandateRequestData { fn get_email(&self) -> Result; fn get_router_return_url(&self) -> Result; fn is_card(&self) -> bool; + fn get_return_url(&self) -> Result; + fn get_webhook_url(&self) -> Result; + fn get_optional_language_from_browser_info(&self) -> Option; } impl PaymentsSetupMandateRequestData for SetupMandateRequestData { @@ -1614,6 +1617,21 @@ impl PaymentsSetupMandateRequestData for SetupMandateRequestData { fn is_card(&self) -> bool { matches!(self.payment_method_data, PaymentMethodData::Card(_)) } + fn get_return_url(&self) -> Result { + self.router_return_url + .clone() + .ok_or_else(missing_field_err("return_url")) + } + fn get_webhook_url(&self) -> Result { + self.webhook_url + .clone() + .ok_or_else(missing_field_err("webhook_url")) + } + fn get_optional_language_from_browser_info(&self) -> Option { + self.browser_info + .clone() + .and_then(|browser_info| browser_info.language) + } } pub trait PaymentMethodTokenizationRequestData { diff --git a/crates/hyperswitch_domain_models/src/relay.rs b/crates/hyperswitch_domain_models/src/relay.rs index 959ac8e7f612..8af58265c396 100644 --- a/crates/hyperswitch_domain_models/src/relay.rs +++ b/crates/hyperswitch_domain_models/src/relay.rs @@ -81,7 +81,7 @@ impl RelayUpdate { match response { Err(error) => Self::ErrorUpdate { error_code: error.code, - error_message: error.message, + error_message: error.reason.unwrap_or(error.message), status: common_enums::RelayStatus::Failure, }, Ok(response) => Self::StatusUpdate { diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 2970ac127ed3..8e4c79a965e7 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 acc7343cb145..7ca106331e42 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/hyperswitch_domain_models/src/router_request_types.rs b/crates/hyperswitch_domain_models/src/router_request_types.rs index aa9fb2b5f1f0..2517559e2fce 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types.rs @@ -871,6 +871,7 @@ pub struct SetupMandateRequestData { pub off_session: Option, pub setup_mandate_details: Option, pub router_return_url: Option, + pub webhook_url: Option, pub browser_info: Option, pub email: Option, pub customer_name: Option>, diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index 19497d6fbb83..3c7ffa16ada3 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/analytics.rs b/crates/router/src/analytics.rs index 8fe30aa59d4a..8fb4f2ac2c19 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/bin/scheduler.rs b/crates/router/src/bin/scheduler.rs index 62c8c03e1a97..e18ed318be17 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/configs/defaults.rs b/crates/router/src/configs/defaults.rs index 74f6502159da..3c38fa2a7751 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/defaults/payment_connector_required_fields.rs b/crates/router/src/configs/defaults/payment_connector_required_fields.rs index 9e42aec4a51d..4f6327d74b7a 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, _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/consts.rs b/crates/router/src/consts.rs index e2784b968ad1..bcc9a5918634 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/admin.rs b/crates/router/src/core/admin.rs index 24a2b25d6ce9..19c3e6c1e228 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/authentication.rs b/crates/router/src/core/authentication.rs index 09cebda49088..bedd7787a730 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 30373d1408f5..4e7e005c946d 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/cache.rs b/crates/router/src/core/cache.rs index 8cda60cf7009..fbe75f3c9c0f 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/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index c413208b2083..e6ff3fdbfca3 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 5e3540311854..21c5de5ed3b6 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 15ceb9b1da21..414bb00e76ec 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 5418d7b7a70b..95736604fc57 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/payment_link.rs b/crates/router/src/core/payment_link.rs index 498a00c240df..477803c92a27 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/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 164c0e9557af..deae78b5540f 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 { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index cbf2cdea5384..1ba6209cf52f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -6564,8 +6564,17 @@ pub async fn payment_external_authentication( &payment_attempt.clone(), payment_connector_name, )); - let webhook_url = - helpers::create_webhook_url(&state.base_url, merchant_id, &authentication_connector); + let mca_id_option = merchant_connector_account.get_mca_id(); // Bind temporary value + let merchant_connector_account_id_or_connector_name = mca_id_option + .as_ref() + .map(|mca_id| mca_id.get_string_repr()) + .unwrap_or(&authentication_connector); + + let webhook_url = helpers::create_webhook_url( + &state.base_url, + merchant_id, + merchant_connector_account_id_or_connector_name, + ); let authentication_details = business_profile .authentication_connector_details diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index b2c90e03b589..262639e4615c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -1241,17 +1241,18 @@ pub fn create_authorize_url( } pub fn create_webhook_url( - router_base_url: &String, + router_base_url: &str, merchant_id: &id_type::MerchantId, - connector_name: impl std::fmt::Display, + merchant_connector_id_or_connector_name: &str, ) -> String { format!( "{}/webhooks/{}/{}", router_base_url, merchant_id.get_string_repr(), - connector_name + merchant_connector_id_or_connector_name, ) } + pub fn create_complete_authorize_url( router_base_url: &String, payment_attempt: &PaymentAttempt, @@ -4035,6 +4036,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 79bcfb6a497d..70db7a581a20 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(), @@ -224,7 +225,7 @@ pub async fn construct_payment_router_data_for_authorize<'a>( let webhook_url = Some(helpers::create_webhook_url( router_base_url, &attempt.merchant_id, - connector_id, + merchant_connector_account.get_id().get_string_repr(), )); let router_return_url = payment_data @@ -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 @@ -2745,11 +2751,17 @@ impl TryFrom> for types::PaymentsAuthoriz attempt, connector_name, )); + let merchant_connector_account_id_or_connector_name = payment_data + .payment_attempt + .merchant_connector_id + .as_ref() + .map(|mca_id| mca_id.get_string_repr()) + .unwrap_or(connector_name); let webhook_url = Some(helpers::create_webhook_url( router_base_url, &attempt.merchant_id, - connector_name, + merchant_connector_account_id_or_connector_name, )); let router_return_url = Some(helpers::create_redirect_url( router_base_url, @@ -3569,6 +3581,18 @@ impl TryFrom> for types::SetupMandateRequ .map(|customer| customer.clone().into_inner()) }); let amount = payment_data.payment_attempt.get_total_amount(); + let merchant_connector_account_id_or_connector_name = payment_data + .payment_attempt + .merchant_connector_id + .as_ref() + .map(|mca_id| mca_id.get_string_repr()) + .unwrap_or(connector_name); + let webhook_url = Some(helpers::create_webhook_url( + router_base_url, + &attempt.merchant_id, + merchant_connector_account_id_or_connector_name, + )); + Ok(Self { currency: payment_data.currency, confirm: true, @@ -3598,6 +3622,7 @@ impl TryFrom> for types::SetupMandateRequ ), metadata: payment_data.payment_intent.metadata.clone().map(Into::into), shipping_cost: payment_data.payment_intent.shipping_cost, + webhook_url, }) } } @@ -3764,11 +3789,16 @@ impl TryFrom> for types::PaymentsPreProce .collect::, _>>() }) .transpose()?; - + let merchant_connector_account_id_or_connector_name = payment_data + .payment_attempt + .merchant_connector_id + .as_ref() + .map(|mca_id| mca_id.get_string_repr()) + .unwrap_or(connector_name); let webhook_url = Some(helpers::create_webhook_url( router_base_url, &attempt.merchant_id, - connector_name, + merchant_connector_account_id_or_connector_name, )); let router_return_url = Some(helpers::create_redirect_url( router_base_url, diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index 22354b4c00fc..e45b4fcead1d 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 b2d6b2ace45c..11be2ebfb5b9 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 @@ -1226,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< @@ -1406,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< @@ -1604,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( @@ -1818,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( @@ -1974,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< @@ -2077,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< @@ -2201,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/refunds.rs b/crates/router/src/core/refunds.rs index 373a268d6918..e78c9471e61e 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/core/relay/utils.rs b/crates/router/src/core/relay/utils.rs index 946f8df7d64e..7c485ab43048 100644 --- a/crates/router/src/core/relay/utils.rs +++ b/crates/router/src/core/relay/utils.rs @@ -32,7 +32,7 @@ pub async fn construct_relay_refund_router_data<'a, F>( let webhook_url = Some(payments::helpers::create_webhook_url( &state.base_url.clone(), merchant_id, - connector_name, + connector_account.get_id().get_string_repr(), )); let supported_connector = &state @@ -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/routing.rs b/crates/router/src/core/routing.rs index 717dfd0c6eba..99bd2b00209b 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 0d66c3b6f17b..159def38621a 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/core/unified_authentication_service.rs b/crates/router/src/core/unified_authentication_service.rs index dbcce5e74cb9..f3fea74c0a83 100644 --- a/crates/router/src/core/unified_authentication_service.rs +++ b/crates/router/src/core/unified_authentication_service.rs @@ -94,6 +94,7 @@ impl UnifiedAuthenticationService for ClickToPay { let pre_auth_router_data: UasPreAuthenticationRouterData = utils::construct_uas_router_data( + state, connector_name.to_string(), payment_method, payment_data.payment_attempt.merchant_id.clone(), @@ -132,16 +133,16 @@ impl UnifiedAuthenticationService for ClickToPay { threeds_server_transaction_id: None, }; - let post_auth_router_data: UasPostAuthenticationRouterData = - utils::construct_uas_router_data( - connector_name.to_string(), - payment_method, - payment_data.payment_attempt.merchant_id.clone(), - None, - post_authentication_data, - merchant_connector_account, - Some(authentication_id.clone()), - )?; + let post_auth_router_data: UasPostAuthenticationRouterData = utils::construct_uas_router_data( + state, + connector_name.to_string(), + payment_method, + payment_data.payment_attempt.merchant_id.clone(), + None, + post_authentication_data, + merchant_connector_account, + Some(authentication_id.clone()), + )?; utils::do_auth_connector_call( state, diff --git a/crates/router/src/core/unified_authentication_service/utils.rs b/crates/router/src/core/unified_authentication_service/utils.rs index 3b4f6cc96683..66774dbd2c17 100644 --- a/crates/router/src/core/unified_authentication_service/utils.rs +++ b/crates/router/src/core/unified_authentication_service/utils.rs @@ -53,7 +53,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, @@ -76,6 +78,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/user.rs b/crates/router/src/core/user.rs index a7c60f33ff77..52d7d6a252ea 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 d8fdff0e6233..19d91b14f01f 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 e897e1b336a2..ca4c062244c8 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/core/utils.rs b/crates/router/src/core/utils.rs index c441e32581e8..5e126310fa6c 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") @@ -285,11 +288,16 @@ pub async fn construct_refund_router_data<'a, F>( .payment_method .get_required_value("payment_method_type") .change_context(errors::ApiErrorResponse::InternalServerError)?; + let merchant_connector_account_id_or_connector_name = payment_attempt + .merchant_connector_id + .as_ref() + .map(|mca_id| mca_id.get_string_repr()) + .unwrap_or(connector_id); let webhook_url = Some(helpers::create_webhook_url( &state.base_url.clone(), merchant_account.get_id(), - connector_id, + merchant_connector_account_id_or_connector_name, )); let test_mode: Option = merchant_connector_account.is_test_mode_on(); @@ -330,6 +338,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 +661,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 +763,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 +862,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 +990,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 +1089,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 +1183,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 ff9849958b51..4c1a5aeb7808 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -22,9 +22,9 @@ use crate::{ core::{ api_locking, errors::{self, ConnectorErrorExt, CustomResult, RouterResponse, StorageErrorExt}, - metrics, payments, - payments::tokenization, - refunds, utils as core_utils, + metrics, + payments::{self, tokenization}, + refunds, relay, utils as core_utils, webhooks::utils::construct_webhook_router_data, }, db::StorageInterface, @@ -62,6 +62,7 @@ pub async fn incoming_webhooks_wrapper( key_store: domain::MerchantKeyStore, connector_name_or_mca_id: &str, body: actix_web::web::Bytes, + is_relay_webhook: bool, ) -> RouterResponse { let start_instant = Instant::now(); let (application_response, webhooks_response_tracker, serialized_req) = @@ -73,6 +74,7 @@ pub async fn incoming_webhooks_wrapper( key_store, connector_name_or_mca_id, body.clone(), + is_relay_webhook, )) .await?; @@ -118,6 +120,7 @@ pub async fn incoming_webhooks_wrapper( Ok(application_response) } +#[allow(clippy::too_many_arguments)] #[instrument(skip_all)] async fn incoming_webhooks_core( state: SessionState, @@ -127,6 +130,7 @@ async fn incoming_webhooks_core( key_store: domain::MerchantKeyStore, connector_name_or_mca_id: &str, body: actix_web::web::Bytes, + is_relay_webhook: bool, ) -> errors::RouterResult<( services::ApplicationResponse, WebhookResponseTracker, @@ -361,120 +365,162 @@ async fn incoming_webhooks_core( id: profile_id.get_string_repr().to_owned(), })?; - let result_response = match flow_type { - api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow( + // If the incoming webhook is a relay webhook, then we need to trigger the relay webhook flow + let result_response = if is_relay_webhook { + let relay_webhook_response = Box::pin(relay_incoming_webhook_flow( state.clone(), - req_state, merchant_account, business_profile, key_store, webhook_details, - source_verified, - &connector, - &request_details, event_type, - )) - .await - .attach_printable("Incoming webhook flow for payments failed"), - - api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow( - state.clone(), - merchant_account, - business_profile, - key_store, - webhook_details, - connector_name.as_str(), source_verified, - event_type, )) .await - .attach_printable("Incoming webhook flow for refunds failed"), + .attach_printable("Incoming webhook flow for relay failed"); - api::WebhookFlow::Dispute => Box::pin(disputes_incoming_webhook_flow( - state.clone(), - merchant_account, - business_profile, - key_store, - webhook_details, - source_verified, - &connector, - &request_details, - event_type, - )) - .await - .attach_printable("Incoming webhook flow for disputes failed"), + // Using early return ensures unsupported webhooks are acknowledged to the connector + if let Some(errors::ApiErrorResponse::NotSupported { .. }) = relay_webhook_response + .as_ref() + .err() + .map(|a| a.current_context()) + { + logger::error!( + webhook_payload =? request_details.body, + "Failed while identifying the event type", + ); - api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow( - state.clone(), - req_state, - merchant_account, - business_profile, - key_store, - webhook_details, - source_verified, - )) - .await - .attach_printable("Incoming bank-transfer webhook flow failed"), + let response = connector + .get_webhook_api_response(&request_details, None) + .switch() + .attach_printable( + "Failed while early return in case of not supported event type in relay webhooks", + )?; + + return Ok(( + response, + WebhookResponseTracker::NoEffect, + serde_json::Value::Null, + )); + }; - api::WebhookFlow::ReturnResponse => Ok(WebhookResponseTracker::NoEffect), + relay_webhook_response + } else { + match flow_type { + api::WebhookFlow::Payment => Box::pin(payments_incoming_webhook_flow( + state.clone(), + req_state, + merchant_account, + business_profile, + key_store, + webhook_details, + source_verified, + &connector, + &request_details, + event_type, + )) + .await + .attach_printable("Incoming webhook flow for payments failed"), - api::WebhookFlow::Mandate => Box::pin(mandates_incoming_webhook_flow( - state.clone(), - merchant_account, - business_profile, - key_store, - webhook_details, - source_verified, - event_type, - )) - .await - .attach_printable("Incoming webhook flow for mandates failed"), + api::WebhookFlow::Refund => Box::pin(refunds_incoming_webhook_flow( + state.clone(), + merchant_account, + business_profile, + key_store, + webhook_details, + connector_name.as_str(), + source_verified, + event_type, + )) + .await + .attach_printable("Incoming webhook flow for refunds failed"), + + api::WebhookFlow::Dispute => Box::pin(disputes_incoming_webhook_flow( + state.clone(), + merchant_account, + business_profile, + key_store, + webhook_details, + source_verified, + &connector, + &request_details, + event_type, + )) + .await + .attach_printable("Incoming webhook flow for disputes failed"), - api::WebhookFlow::ExternalAuthentication => { - Box::pin(external_authentication_incoming_webhook_flow( + api::WebhookFlow::BankTransfer => Box::pin(bank_transfer_webhook_flow( + state.clone(), + req_state, + merchant_account, + business_profile, + key_store, + webhook_details, + source_verified, + )) + .await + .attach_printable("Incoming bank-transfer webhook flow failed"), + + api::WebhookFlow::ReturnResponse => Ok(WebhookResponseTracker::NoEffect), + + api::WebhookFlow::Mandate => Box::pin(mandates_incoming_webhook_flow( + state.clone(), + merchant_account, + business_profile, + key_store, + webhook_details, + source_verified, + event_type, + )) + .await + .attach_printable("Incoming webhook flow for mandates failed"), + + api::WebhookFlow::ExternalAuthentication => { + Box::pin(external_authentication_incoming_webhook_flow( + state.clone(), + req_state, + merchant_account, + key_store, + source_verified, + event_type, + &request_details, + &connector, + object_ref_id, + business_profile, + merchant_connector_account, + )) + .await + .attach_printable("Incoming webhook flow for external authentication failed") + } + api::WebhookFlow::FraudCheck => Box::pin(frm_incoming_webhook_flow( state.clone(), req_state, merchant_account, key_store, source_verified, event_type, - &request_details, - &connector, object_ref_id, business_profile, - merchant_connector_account, )) .await - .attach_printable("Incoming webhook flow for external authentication failed") - } - api::WebhookFlow::FraudCheck => Box::pin(frm_incoming_webhook_flow( - state.clone(), - req_state, - merchant_account, - key_store, - source_verified, - event_type, - object_ref_id, - business_profile, - )) - .await - .attach_printable("Incoming webhook flow for fraud check failed"), + .attach_printable("Incoming webhook flow for fraud check failed"), - #[cfg(feature = "payouts")] - api::WebhookFlow::Payout => Box::pin(payouts_incoming_webhook_flow( - state.clone(), - merchant_account, - business_profile, - key_store, - webhook_details, - event_type, - source_verified, - )) - .await - .attach_printable("Incoming webhook flow for payouts failed"), + #[cfg(feature = "payouts")] + api::WebhookFlow::Payout => Box::pin(payouts_incoming_webhook_flow( + state.clone(), + merchant_account, + business_profile, + key_store, + webhook_details, + event_type, + source_verified, + )) + .await + .attach_printable("Incoming webhook flow for payouts failed"), - _ => Err(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Unsupported Flow Type received in incoming webhooks"), + _ => Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unsupported Flow Type received in incoming webhooks"), + } }; match result_response { @@ -836,6 +882,97 @@ async fn payouts_incoming_webhook_flow( } } +async fn relay_refunds_incoming_webhook_flow( + state: SessionState, + merchant_account: domain::MerchantAccount, + business_profile: domain::Profile, + merchant_key_store: domain::MerchantKeyStore, + webhook_details: api::IncomingWebhookDetails, + event_type: webhooks::IncomingWebhookEvent, + source_verified: bool, +) -> CustomResult { + let db = &*state.store; + let key_manager_state = &(&state).into(); + + let relay_record = match webhook_details.object_reference_id { + webhooks::ObjectReferenceId::RefundId(refund_id_type) => match refund_id_type { + webhooks::RefundIdType::RefundId(refund_id) => { + let relay_id = common_utils::id_type::RelayId::from_str(&refund_id) + .change_context(errors::ValidationError::IncorrectValueProvided { + field_name: "relay_id", + }) + .change_context(errors::ApiErrorResponse::InternalServerError)?; + + db.find_relay_by_id(key_manager_state, &merchant_key_store, &relay_id) + .await + .to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound) + .attach_printable("Failed to fetch the relay record")? + } + webhooks::RefundIdType::ConnectorRefundId(connector_refund_id) => db + .find_relay_by_profile_id_connector_reference_id( + key_manager_state, + &merchant_key_store, + business_profile.get_id(), + &connector_refund_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound) + .attach_printable("Failed to fetch the relay record")?, + }, + _ => Err(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("received a non-refund id when processing relay refund webhooks")?, + }; + + // if source_verified then update relay status else trigger relay force sync + let relay_response = if source_verified { + let relay_update = hyperswitch_domain_models::relay::RelayUpdate::StatusUpdate { + connector_reference_id: None, + status: common_enums::RelayStatus::foreign_try_from(event_type) + .change_context(errors::ApiErrorResponse::WebhookProcessingFailure) + .attach_printable("failed relay refund status mapping from event type")?, + }; + db.update_relay( + key_manager_state, + &merchant_key_store, + relay_record, + relay_update, + ) + .await + .map(api_models::relay::RelayResponse::from) + .to_not_found_response(errors::ApiErrorResponse::WebhookResourceNotFound) + .attach_printable("Failed to update relay")? + } else { + let relay_retrieve_request = api_models::relay::RelayRetrieveRequest { + force_sync: true, + id: relay_record.id, + }; + let relay_force_sync_response = Box::pin(relay::relay_retrieve( + state, + merchant_account, + Some(business_profile.get_id().clone()), + merchant_key_store, + relay_retrieve_request, + )) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to force sync relay")?; + + if let hyperswitch_domain_models::api::ApplicationResponse::Json(response) = + relay_force_sync_response + { + response + } else { + Err(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Unexpected response from force sync relay")? + } + }; + + Ok(WebhookResponseTracker::Relay { + relay_id: relay_response.id, + status: relay_response.status, + }) +} + #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] async fn refunds_incoming_webhook_flow( @@ -938,6 +1075,44 @@ async fn refunds_incoming_webhook_flow( }) } +async fn relay_incoming_webhook_flow( + state: SessionState, + merchant_account: domain::MerchantAccount, + business_profile: domain::Profile, + merchant_key_store: domain::MerchantKeyStore, + webhook_details: api::IncomingWebhookDetails, + event_type: webhooks::IncomingWebhookEvent, + source_verified: bool, +) -> CustomResult { + let flow_type: api::WebhookFlow = event_type.into(); + + let result_response = match flow_type { + webhooks::WebhookFlow::Refund => Box::pin(relay_refunds_incoming_webhook_flow( + state, + merchant_account, + business_profile, + merchant_key_store, + webhook_details, + event_type, + source_verified, + )) + .await + .attach_printable("Incoming webhook flow for relay refund failed")?, + webhooks::WebhookFlow::Payment + | webhooks::WebhookFlow::Payout + | webhooks::WebhookFlow::Dispute + | webhooks::WebhookFlow::Subscription + | webhooks::WebhookFlow::ReturnResponse + | webhooks::WebhookFlow::BankTransfer + | webhooks::WebhookFlow::Mandate + | webhooks::WebhookFlow::ExternalAuthentication + | webhooks::WebhookFlow::FraudCheck => Err(errors::ApiErrorResponse::NotSupported { + message: "Relay webhook flow types not supported".to_string(), + })?, + }; + Ok(result_response) +} + async fn get_payment_attempt_from_object_reference_id( state: &SessionState, object_reference_id: webhooks::ObjectReferenceId, @@ -1658,6 +1833,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 569cd330a079..5e89f3343b54 100644 --- a/crates/router/src/core/webhooks/incoming_v2.rs +++ b/crates/router/src/core/webhooks/incoming_v2.rs @@ -56,6 +56,7 @@ pub async fn incoming_webhooks_wrapper( key_store: domain::MerchantKeyStore, connector_id: &common_utils::id_type::MerchantConnectorAccountId, body: actix_web::web::Bytes, + is_relay_webhook: bool, ) -> RouterResponse { let start_instant = Instant::now(); let (application_response, webhooks_response_tracker, serialized_req) = @@ -68,6 +69,7 @@ pub async fn incoming_webhooks_wrapper( key_store, connector_id, body.clone(), + is_relay_webhook, )) .await?; @@ -124,6 +126,7 @@ async fn incoming_webhooks_core( key_store: domain::MerchantKeyStore, connector_id: &common_utils::id_type::MerchantConnectorAccountId, body: actix_web::web::Bytes, + _is_relay_webhook: bool, ) -> errors::RouterResult<( services::ApplicationResponse, WebhookResponseTracker, @@ -665,6 +668,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 b21ec0056c19..256fae78d4a8 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/db/configs.rs b/crates/router/src/db/configs.rs index 575481793ca1..9b8ab5231b67 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/events.rs b/crates/router/src/db/events.rs index 6bb7de1b7d99..33fcece85a01 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/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 8eec7f04169e..cbced16bdcd8 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/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 4f4a3f1cf00b..1c104b22489f 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/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index 0abbccd2cb35..fc0fa5aca75f 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 9f12ec8e8fd8..aaeba6085a07 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 3a5c8b7da1ee..88962f73233a 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/db/relay.rs b/crates/router/src/db/relay.rs index 46259679c55d..2ead84019d8d 100644 --- a/crates/router/src/db/relay.rs +++ b/crates/router/src/db/relay.rs @@ -35,6 +35,14 @@ pub trait RelayInterface { merchant_key_store: &domain::MerchantKeyStore, relay_id: &common_utils::id_type::RelayId, ) -> CustomResult; + + async fn find_relay_by_profile_id_connector_reference_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + connector_reference_id: &str, + ) -> CustomResult; } #[async_trait::async_trait] @@ -105,6 +113,30 @@ impl RelayInterface for Store { .await .change_context(errors::StorageError::DecryptionError) } + + async fn find_relay_by_profile_id_connector_reference_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + connector_reference_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + diesel_models::relay::Relay::find_by_profile_id_connector_reference_id( + &conn, + profile_id, + connector_reference_id, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + } } #[async_trait::async_trait] @@ -136,6 +168,16 @@ impl RelayInterface for MockDb { ) -> CustomResult { Err(errors::StorageError::MockDbError)? } + + async fn find_relay_by_profile_id_connector_reference_id( + &self, + _key_manager_state: &KeyManagerState, + _merchant_key_store: &domain::MerchantKeyStore, + _profile_id: &common_utils::id_type::ProfileId, + _connector_reference_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } } #[async_trait::async_trait] @@ -178,4 +220,21 @@ impl RelayInterface for KafkaStore { .find_relay_by_id(key_manager_state, merchant_key_store, relay_id) .await } + + async fn find_relay_by_profile_id_connector_reference_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &domain::MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + connector_reference_id: &str, + ) -> CustomResult { + self.diesel_store + .find_relay_by_profile_id_connector_reference_id( + key_manager_state, + merchant_key_store, + profile_id, + connector_reference_id, + ) + .await + } } diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs index 877a4c540774..1006c33aaa0b 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 0da518983265..1df6160f81e5 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/lib.rs b/crates/router/src/lib.rs index 4fe9318f64bb..8c2bca5a82e6 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -142,6 +142,7 @@ pub fn mk_app( .service(routes::Customers::server(state.clone())) .service(routes::Configs::server(state.clone())) .service(routes::MerchantConnectorAccount::server(state.clone())) + .service(routes::RelayWebhooks::server(state.clone())) .service(routes::Webhooks::server(state.clone())) .service(routes::Relay::server(state.clone())); diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 462861d331f4..22f5983e4c39 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -69,7 +69,7 @@ pub use self::app::{ ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, Poll, - Profile, ProfileNew, Refunds, Relay, SessionState, User, Webhooks, + Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f9dbec774528..2a9561c06c81 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()), }) } } @@ -1512,6 +1515,20 @@ impl Webhooks { } } +pub struct RelayWebhooks; + +#[cfg(feature = "oltp")] +impl RelayWebhooks { + pub fn server(state: AppState) -> Scope { + use api_models::webhooks as webhook_type; + web::scope("/webhooks/relay") + .app_data(web::Data::new(state)) + .service(web::resource("/{merchant_id}/{connector_id}").route( + web::post().to(receive_incoming_relay_webhook::), + )) + } +} + #[cfg(all(feature = "oltp", feature = "v2"))] impl Webhooks { pub fn server(config: AppState) -> Scope { diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 9d7ae1874c10..6550778619ae 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -171,6 +171,7 @@ impl From for ApiIdentifier { Flow::FrmFulfillment | Flow::IncomingWebhookReceive + | Flow::IncomingRelayWebhookReceive | Flow::WebhookEventInitialDeliveryAttemptList | Flow::WebhookEventDeliveryAttemptList | Flow::WebhookEventDeliveryRetry => Self::Webhooks, diff --git a/crates/router/src/routes/payment_link.rs b/crates/router/src/routes/payment_link.rs index 71f10fe73e91..361367c7d277 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 25528b21ed85..0234b4fca828 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 2329a48ef2bc..4044630b4da4 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/routes/webhooks.rs b/crates/router/src/routes/webhooks.rs index 5427ac34b4ec..1b3f28fd2568 100644 --- a/crates/router/src/routes/webhooks.rs +++ b/crates/router/src/routes/webhooks.rs @@ -36,6 +36,7 @@ pub async fn receive_incoming_webhook( auth.key_store, &connector_id_or_name, body.clone(), + false, ) }, &auth::MerchantIdAuth(merchant_id), @@ -44,6 +45,89 @@ pub async fn receive_incoming_webhook( .await } +#[cfg(feature = "v1")] +#[instrument(skip_all, fields(flow = ?Flow::IncomingRelayWebhookReceive))] +pub async fn receive_incoming_relay_webhook( + state: web::Data, + req: HttpRequest, + body: web::Bytes, + path: web::Path<( + common_utils::id_type::MerchantId, + common_utils::id_type::MerchantConnectorAccountId, + )>, +) -> impl Responder { + let flow = Flow::IncomingWebhookReceive; + let (merchant_id, connector_id) = path.into_inner(); + let is_relay_webhook = true; + + Box::pin(api::server_wrap( + flow.clone(), + state, + &req, + (), + |state, auth, _, req_state| { + webhooks::incoming_webhooks_wrapper::( + &flow, + state.to_owned(), + req_state, + &req, + auth.merchant_account, + auth.key_store, + connector_id.get_string_repr(), + body.clone(), + is_relay_webhook, + ) + }, + &auth::MerchantIdAuth(merchant_id), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::IncomingRelayWebhookReceive))] +pub async fn receive_incoming_relay_webhook( + state: web::Data, + req: HttpRequest, + body: web::Bytes, + path: web::Path<( + common_utils::id_type::MerchantId, + common_utils::id_type::ProfileId, + common_utils::id_type::MerchantConnectorAccountId, + )>, +) -> impl Responder { + let flow = Flow::IncomingWebhookReceive; + let (merchant_id, profile_id, connector_id) = path.into_inner(); + let is_relay_webhook = true; + + Box::pin(api::server_wrap( + flow.clone(), + state, + &req, + (), + |state, auth, _, req_state| { + webhooks::incoming_webhooks_wrapper::( + &flow, + state.to_owned(), + req_state, + &req, + auth.merchant_account, + auth.profile, + auth.key_store, + &connector_id, + body.clone(), + is_relay_webhook, + ) + }, + &auth::MerchantIdAndProfileIdAuth { + merchant_id, + profile_id, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[instrument(skip_all, fields(flow = ?Flow::IncomingWebhookReceive))] #[cfg(feature = "v2")] pub async fn receive_incoming_webhook( @@ -75,6 +159,7 @@ pub async fn receive_incoming_webhook( auth.key_store, &connector_id, body.clone(), + false, ) }, &auth::MerchantIdAndProfileIdAuth { diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index cac856b2c48b..f90528353cfb 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/services/authorization.rs b/crates/router/src/services/authorization.rs index da296373d802..db3483f8164f 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 c9c64b76143d..df2a14a1a1a0 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/services/conversion_impls.rs b/crates/router/src/services/conversion_impls.rs index 9bb88cf4ecc0..8572add041e8 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 2a2fbe82c3f2..5baca7bb55a1 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 47d8add58c3b..4d248bbf0b48 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 f7de86ceebd2..d7c4fca748ea 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/src/types/domain/types.rs b/crates/router/src/types/domain/types.rs index d4cd9ef62d7d..bb123659060d 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/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index bbcdfc535df8..cb7d3f78f78b 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -662,6 +662,22 @@ impl ForeignTryFrom for storage_enum } } +impl ForeignTryFrom for api_enums::RelayStatus { + type Error = errors::ValidationError; + + fn foreign_try_from( + value: api_models::webhooks::IncomingWebhookEvent, + ) -> Result { + match value { + api_models::webhooks::IncomingWebhookEvent::RefundSuccess => Ok(Self::Success), + api_models::webhooks::IncomingWebhookEvent::RefundFailure => Ok(Self::Failure), + _ => Err(errors::ValidationError::IncorrectValueProvided { + field_name: "incoming_webhook_event_type", + }), + } + } +} + #[cfg(feature = "payouts")] impl ForeignTryFrom for storage_enums::PayoutStatus { type Error = errors::ValidationError; diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index e4046f95a153..f9ef4880924c 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/src/utils/user.rs b/crates/router/src/utils/user.rs index b8ffcf836d78..ef06531b4210 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 ac8ee11fc6a2..7413e66070fb 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/crates/router/tests/cache.rs b/crates/router/tests/cache.rs index 55b92b4aace5..a1f85534b6bc 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 18e9c43ed6ac..8b3372d7dec2 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(), @@ -220,6 +222,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 +273,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 +340,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 +414,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 305b11fe5b3d..2cf3dc965fd6 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(), @@ -602,6 +604,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 +648,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 +693,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 +737,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 +832,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 +873,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 beaacb79fc01..e1fe40b42065 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 1d573d007ba6..49d2e12b819f 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 c014370b24f4..36f969dac1cf 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/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 10183f75d5bc..935efa3c78b0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -537,6 +537,8 @@ pub enum Flow { Relay, /// Relay retrieve flow RelayRetrieve, + /// Incoming Relay Webhook Receive + IncomingRelayWebhookReceive, } /// Trait for providing generic behaviour to flow metric diff --git a/crates/storage_impl/src/redis/cache.rs b/crates/storage_impl/src/redis/cache.rs index 93255fac9144..323d3d6df259 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 42ad2ae0795a..373ac370e2fe 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, diff --git a/cypress-tests/README.md b/cypress-tests/README.md index 0a071aa34a01..87681bb6ea00 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 + } + } +} +``` diff --git a/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js b/cypress-tests/cypress/e2e/PaymentMethodListTest/00000-PaymentMethodListTests.cy.js index 89b27a6b2716..0f754831d46e 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 ae72465b6065..e782b32368b2 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 f7d199164fd4..64e127608a4a 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/PaymentTest/00024-SessionCall.cy.js b/cypress-tests/cypress/e2e/PaymentTest/00024-SessionCall.cy.js new file mode 100644 index 000000000000..8d729a2f77f3 --- /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/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 000000000000..afa7ceda0d13 --- /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/e2e/PaymentUtils/Adyen.js b/cypress-tests/cypress/e2e/PaymentUtils/Adyen.js index 56efbf1f6f21..95fa82f9675a 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/Commons.js b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js index 85718d8f9ba5..998c6b91f7bc 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 b98973250e98..6877d609ac15 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/Iatapay.js b/cypress-tests/cypress/e2e/PaymentUtils/Iatapay.js index faa810e7bad5..477728969558 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: { diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js index 0a8235141dcc..049997c541c3 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Novalnet.js @@ -6,6 +6,31 @@ const successfulThreeDSTestCardDetails = { card_cvc: "123", }; +const successfulNo3DSCardDetails = { + card_number: "4242424242424242", + card_exp_month: "01", + card_exp_year: "50", + card_holder_name: "joseph Doe", + card_cvc: "123", +}; + +const singleUseMandateData = { + customer_acceptance: { + acceptance_type: "offline", + accepted_at: "1963-05-03T04:07:52.723Z", + online: { + ip_address: "125.0.0.1", + user_agent: "amet irure esse", + }, + }, + mandate_type: { + single_use: { + amount: 8000, + currency: "EUR", + }, + }, +}; + export const connectorDetails = { card_pm: { PaymentIntent: { @@ -206,5 +231,249 @@ export const connectorDetails = { }, }, }, + SaveCardConfirmAutoCaptureOffSession: { + Request: { + setup_future_usage: "off_session", + }, + Response: { + status: 200, + trigger_skip: true, + body: { + status: "requires_customer_action", + }, + }, + }, + PaymentIntentOffSession: { + Request: { + currency: "EUR", + amount: 6500, + authentication_type: "no_three_ds", + customer_acceptance: null, + setup_future_usage: "off_session", + }, + Response: { + status: 200, + trigger_skip: true, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", + }, + }, + }, + ZeroAuthPaymentIntent: { + Request: { + amount: 0, + setup_future_usage: "off_session", + currency: "EUR", + }, + Response: { + status: 200, + trigger_skip: true, + body: { + status: "requires_payment_method", + setup_future_usage: "off_session", + }, + }, + }, + ZeroAuthMandate: { + Request: { + payment_method: "card", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + currency: "USD", + mandate_data: singleUseMandateData, + }, + Response: { + status: 200, + trigger_skip: true, + body: { + status: "succeeded", + }, + }, + }, + ZeroAuthConfirmPayment: { + Request: { + payment_type: "setup_mandate", + payment_method: "card", + payment_method_type: "credit", + payment_method_data: { + card: successfulNo3DSCardDetails, + }, + }, + Response: { + status: 501, + body: { + error: { + type: "invalid_request", + message: "Setup Mandate flow for Novalnet is not implemented", + code: "IR_00", + }, + }, + }, + }, + }, + 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", + }, + }, + }, + ], + }, + ], + }, + }, }, }; diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js b/cypress-tests/cypress/e2e/PaymentUtils/Stripe.js index b8dd275afae3..8d4d94fa5635 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/e2e/RoutingTest/00000-PriorityRouting.cy.js b/cypress-tests/cypress/e2e/RoutingTest/00000-PriorityRouting.cy.js index 22e4b3783af1..5699712ec2e9 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 7d7f75e3519b..16b640bc03de 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 304668752cd1..a1621a530ae9 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 74c02d7ac9fd..94fcaed104db 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 6135f26934e4..1b7b41a035d2 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", @@ -2970,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", @@ -2985,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, @@ -3024,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, @@ -3054,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}"` ); } }); @@ -3111,7 +3152,6 @@ Cypress.Commands.add( headers: { Authorization: `Bearer ${globalState.get("userInfoToken")}`, "Content-Type": "application/json", - Cookie: `${globalState.get("cookie")}`, }, failOnStatusCode: false, body: routingBody, @@ -3134,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) => { @@ -3162,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) => { @@ -3331,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); + }); +}); diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index ec58ab08b878..b26b5b2e438d 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" 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 000000000000..74679837d38c --- /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 000000000000..3d350c790f68 --- /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 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 000000000000..58d6b900e2db --- /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 000000000000..13ec3cf48b71 --- /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 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 000000000000..e0ac49d1ecfb --- /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 000000000000..14167a32cfcb --- /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 diff --git a/migrations/2025-01-07-105739_create_index_for_relay/down.sql b/migrations/2025-01-07-105739_create_index_for_relay/down.sql new file mode 100644 index 000000000000..8a75d445466e --- /dev/null +++ b/migrations/2025-01-07-105739_create_index_for_relay/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP INDEX relay_profile_id_connector_reference_id_index; \ No newline at end of file diff --git a/migrations/2025-01-07-105739_create_index_for_relay/up.sql b/migrations/2025-01-07-105739_create_index_for_relay/up.sql new file mode 100644 index 000000000000..5ebe5983c74a --- /dev/null +++ b/migrations/2025-01-07-105739_create_index_for_relay/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +CREATE UNIQUE INDEX relay_profile_id_connector_reference_id_index ON relay (profile_id, connector_reference_id); \ No newline at end of file