From 2f90ffa2114ce41fbbd2406284f172127d0b8edc Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 15 Oct 2025 00:30:59 +0000 Subject: [PATCH 01/16] chore(version): 2025.10.15.0 --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1e2f5c32..b266cf305e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.10.15.0 + +### Features + +- **subscriptions:** Add update subscriptions APIs with payments update call ([#9778](https://github.com/juspay/hyperswitch/pull/9778)) ([`36fbaa0`](https://github.com/juspay/hyperswitch/commit/36fbaa07074a6534c3997825f81604b9499a9db0)) + +### Bug Fixes + +- **connector:** + - [adyenplatform] use YYYY format for expiry year ([#9823](https://github.com/juspay/hyperswitch/pull/9823)) ([`5e5a152`](https://github.com/juspay/hyperswitch/commit/5e5a1522d80c9fadbd10c89d6f82685e4fcfec3e)) + - [Peach Payments] fix connector metadata deserialization ([#9826](https://github.com/juspay/hyperswitch/pull/9826)) ([`859b3b1`](https://github.com/juspay/hyperswitch/commit/859b3b18443b1200bac8672f3469c86793c16ad4)) + +**Full Changelog:** [`2025.10.14.0...2025.10.15.0`](https://github.com/juspay/hyperswitch/compare/2025.10.14.0...2025.10.15.0) + +- - - + ## 2025.10.14.0 ### Features From 59628332de7053c8a4cc3901b02571ed7f0d698b Mon Sep 17 00:00:00 2001 From: Jagan Date: Wed, 15 Oct 2025 12:18:53 +0530 Subject: [PATCH 02/16] refactor(db_interfaces): move db interfaces in router to domain_models (#9830) Co-authored-by: Ankit Kumar Gupta Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Gaurav Rawat <104276743+GauravRawat369@users.noreply.github.com> --- Cargo.lock | 1 + crates/analytics/src/lib.rs | 7 +- crates/common_utils/src/types.rs | 11 +- .../grpc_client/unified_connector_service.rs | 86 +- .../src/business_profile.rs | 69 +- .../hyperswitch_domain_models/src/configs.rs | 39 + .../hyperswitch_domain_models/src/customer.rs | 57 +- crates/hyperswitch_domain_models/src/lib.rs | 1 + .../src/merchant_account.rs | 94 +- .../src/merchant_connector_account.rs | 234 ++- .../src/merchant_key_store.rs | 39 + crates/hyperswitch_interfaces/Cargo.toml | 1 + .../hyperswitch_interfaces/src/api_client.rs | 658 +++++++ crates/hyperswitch_interfaces/src/configs.rs | 192 ++- crates/hyperswitch_interfaces/src/consts.rs | 18 + crates/hyperswitch_interfaces/src/events.rs | 13 +- crates/hyperswitch_interfaces/src/helpers.rs | 7 + crates/hyperswitch_interfaces/src/lib.rs | 7 + crates/hyperswitch_interfaces/src/metrics.rs | 7 + .../src/unified_connector_service.rs | 34 + .../unified_connector_service/transformers.rs | 230 +++ crates/router/Cargo.toml | 2 +- crates/router/src/configs/defaults.rs | 13 - crates/router/src/configs/settings.rs | 82 +- crates/router/src/core/payments/customers.rs | 59 +- .../src/core/payments/flows/psync_flow.rs | 4 +- crates/router/src/core/payments/helpers.rs | 95 +- .../src/core/unified_connector_service.rs | 11 - .../unified_connector_service/transformers.rs | 210 +-- crates/router/src/db.rs | 18 +- crates/router/src/db/business_profile.rs | 455 +---- crates/router/src/db/configs.rs | 279 +-- crates/router/src/db/events.rs | 5 +- crates/router/src/db/kafka_store.rs | 9 +- crates/router/src/db/merchant_account.rs | 824 +-------- .../src/db/merchant_connector_account.rs | 1429 +-------------- crates/router/src/db/merchant_key_store.rs | 320 +--- crates/router/src/events.rs | 13 +- crates/router/src/routes/app.rs | 33 +- crates/router/src/services.rs | 3 +- crates/router/src/services/api.rs | 533 +----- crates/router/src/services/api/client.rs | 86 +- crates/router/src/services/kafka.rs | 3 +- crates/storage_impl/Cargo.toml | 3 +- crates/storage_impl/src/business_profile.rs | 482 ++++++ crates/storage_impl/src/config.rs | 10 +- crates/storage_impl/src/configs.rs | 285 +++ crates/storage_impl/src/database/store.rs | 4 +- crates/storage_impl/src/kv_router_store.rs | 5 +- crates/storage_impl/src/lib.rs | 12 +- crates/storage_impl/src/merchant_account.rs | 874 ++++++++++ .../src/merchant_connector_account.rs | 1531 +++++++++++++++++ crates/storage_impl/src/merchant_key_store.rs | 346 ++++ crates/storage_impl/src/mock_db.rs | 7 + crates/storage_impl/src/utils.rs | 40 + 55 files changed, 5386 insertions(+), 4504 deletions(-) create mode 100644 crates/hyperswitch_domain_models/src/configs.rs create mode 100644 crates/hyperswitch_interfaces/src/api_client.rs create mode 100644 crates/hyperswitch_interfaces/src/helpers.rs create mode 100644 crates/hyperswitch_interfaces/src/unified_connector_service.rs create mode 100644 crates/hyperswitch_interfaces/src/unified_connector_service/transformers.rs create mode 100644 crates/storage_impl/src/business_profile.rs create mode 100644 crates/storage_impl/src/configs.rs create mode 100644 crates/storage_impl/src/merchant_account.rs create mode 100644 crates/storage_impl/src/merchant_connector_account.rs create mode 100644 crates/storage_impl/src/merchant_key_store.rs diff --git a/Cargo.lock b/Cargo.lock index 183c316a37..a0cef58bdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4427,6 +4427,7 @@ dependencies = [ "mime", "reqwest 0.11.27", "router_env", + "rust-grpc-client", "serde", "serde_json", "strum 0.26.3", diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 2df457dde2..5a9e3b6b06 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -22,7 +22,7 @@ pub mod search; mod sqlx; mod types; use api_event::metrics::{ApiEventMetric, ApiEventMetricRow}; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, types::TenantConfig}; use disputes::metrics::{DisputeMetric, DisputeMetricRow}; use enums::AuthInfo; use hyperswitch_interfaces::secrets_interface::{ @@ -969,10 +969,7 @@ impl AnalyticsProvider { } } - pub async fn from_conf( - config: &AnalyticsConfig, - tenant: &dyn storage_impl::config::TenantConfig, - ) -> Self { + pub async fn from_conf(config: &AnalyticsConfig, tenant: &dyn TenantConfig) -> Self { match config { AnalyticsConfig::Sqlx { sqlx, .. } => { Self::Sqlx(SqlxClient::from_conf(sqlx, tenant.get_schema()).await) diff --git a/crates/common_utils/src/types.rs b/crates/common_utils/src/types.rs index 70109c5972..0cc1cf797e 100644 --- a/crates/common_utils/src/types.rs +++ b/crates/common_utils/src/types.rs @@ -50,7 +50,7 @@ use crate::{ }, errors::{CustomResult, ParsingError, PercentageError, ValidationError}, fp_utils::when, - impl_enum_str, + id_type, impl_enum_str, }; /// Represents Percentage Value between 0 and 100 both inclusive @@ -1441,3 +1441,12 @@ impl_enum_str!( }, } ); + +#[allow(missing_docs)] +pub trait TenantConfig: Send + Sync { + fn get_tenant_id(&self) -> &id_type::TenantId; + fn get_schema(&self) -> &str; + fn get_accounts_schema(&self) -> &str; + fn get_redis_key_prefix(&self) -> &str; + fn get_clickhouse_database(&self) -> &str; +} diff --git a/crates/external_services/src/grpc_client/unified_connector_service.rs b/crates/external_services/src/grpc_client/unified_connector_service.rs index b0799ae4ec..a7d7a20542 100644 --- a/crates/external_services/src/grpc_client/unified_connector_service.rs +++ b/crates/external_services/src/grpc_client/unified_connector_service.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; use common_enums::connector_enums::Connector; use common_utils::{consts as common_utils_consts, errors::CustomResult, types::Url}; use error_stack::ResultExt; +pub use hyperswitch_interfaces::unified_connector_service::transformers::UnifiedConnectorServiceError; use masking::{PeekInterface, Secret}; use router_env::logger; use tokio::time::{timeout, Duration}; @@ -22,91 +23,6 @@ use crate::{ utils::deserialize_hashset, }; -/// Unified Connector Service error variants -#[derive(Debug, Clone, thiserror::Error)] -pub enum UnifiedConnectorServiceError { - /// Error occurred while communicating with the gRPC server. - #[error("Error from gRPC Server : {0}")] - ConnectionError(String), - - /// Failed to encode the request to the unified connector service. - #[error("Failed to encode unified connector service request")] - RequestEncodingFailed, - - /// Request encoding failed due to a specific reason. - #[error("Request encoding failed : {0}")] - RequestEncodingFailedWithReason(String), - - /// Failed to deserialize the response from the connector. - #[error("Failed to deserialize connector response")] - ResponseDeserializationFailed, - - /// The connector name provided is invalid or unrecognized. - #[error("An invalid connector name was provided")] - InvalidConnectorName, - - /// Connector name is missing - #[error("Connector name is missing")] - MissingConnectorName, - - /// A required field was missing in the request. - #[error("Missing required field: {field_name}")] - MissingRequiredField { - /// Missing Field - field_name: &'static str, - }, - - /// Multiple required fields were missing in the request. - #[error("Missing required fields: {field_names:?}")] - MissingRequiredFields { - /// Missing Fields - field_names: Vec<&'static str>, - }, - - /// The requested step or feature is not yet implemented. - #[error("This step has not been implemented for: {0}")] - NotImplemented(String), - - /// Parsing of some value or input failed. - #[error("Parsing failed")] - ParsingFailed, - - /// Data format provided is invalid - #[error("Invalid Data format")] - InvalidDataFormat { - /// Field Name for which data is invalid - field_name: &'static str, - }, - - /// Failed to obtain authentication type - #[error("Failed to obtain authentication type")] - FailedToObtainAuthType, - - /// Failed to inject metadata into request headers - #[error("Failed to inject metadata into request headers: {0}")] - HeaderInjectionFailed(String), - - /// Failed to perform Payment Authorize from gRPC Server - #[error("Failed to perform Payment Authorize from gRPC Server")] - PaymentAuthorizeFailure, - - /// Failed to perform Payment Get from gRPC Server - #[error("Failed to perform Payment Get from gRPC Server")] - PaymentGetFailure, - - /// Failed to perform Payment Setup Mandate from gRPC Server - #[error("Failed to perform Setup Mandate from gRPC Server")] - PaymentRegisterFailure, - - /// Failed to perform Payment Repeat Payment from gRPC Server - #[error("Failed to perform Repeat Payment from gRPC Server")] - PaymentRepeatEverythingFailure, - - /// Failed to transform incoming webhook from gRPC Server - #[error("Failed to transform incoming webhook from gRPC Server")] - WebhookTransformFailure, -} - /// Result type for Dynamic Routing pub type UnifiedConnectorServiceResult = CustomResult; /// Contains the Unified Connector Service client diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index 42c2398582..f0c656c38a 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -14,14 +14,17 @@ use common_utils::{ #[cfg(feature = "v2")] use diesel_models::business_profile::RevenueRecoveryAlgorithmData; use diesel_models::business_profile::{ - AuthenticationConnectorDetails, BusinessPaymentLinkConfig, BusinessPayoutLinkConfig, - CardTestingGuardConfig, ExternalVaultConnectorDetails, ProfileUpdateInternal, WebhookDetails, + self as storage_types, AuthenticationConnectorDetails, BusinessPaymentLinkConfig, + BusinessPayoutLinkConfig, CardTestingGuardConfig, ExternalVaultConnectorDetails, + ProfileUpdateInternal, WebhookDetails, }; use error_stack::ResultExt; use masking::{ExposeInterface, PeekInterface, Secret}; use crate::{ + behaviour::Conversion, errors::api_error_response, + merchant_key_store::MerchantKeyStore, type_encryption::{crypto_operation, AsyncLift, CryptoOperation}, }; #[cfg(feature = "v1")] @@ -950,7 +953,7 @@ impl From for ProfileUpdateInternal { #[cfg(feature = "v1")] #[async_trait::async_trait] -impl super::behaviour::Conversion for Profile { +impl Conversion for Profile { type DstType = diesel_models::business_profile::Profile; type NewDstType = diesel_models::business_profile::ProfileNew; @@ -2258,7 +2261,7 @@ impl From for ProfileUpdateInternal { #[cfg(feature = "v2")] #[async_trait::async_trait] -impl super::behaviour::Conversion for Profile { +impl Conversion for Profile { type DstType = diesel_models::business_profile::Profile; type NewDstType = diesel_models::business_profile::ProfileNew; @@ -2506,3 +2509,61 @@ impl super::behaviour::Conversion for Profile { }) } } + +#[async_trait::async_trait] +pub trait ProfileInterface +where + Profile: Conversion, +{ + type Error; + async fn insert_business_profile( + &self, + key_manager_state: &keymanager::KeyManagerState, + merchant_key_store: &MerchantKeyStore, + business_profile: Profile, + ) -> CustomResult; + + async fn find_business_profile_by_profile_id( + &self, + key_manager_state: &keymanager::KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult; + + async fn find_business_profile_by_merchant_id_profile_id( + &self, + key_manager_state: &keymanager::KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult; + + async fn find_business_profile_by_profile_name_merchant_id( + &self, + key_manager_state: &keymanager::KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_name: &str, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult; + + async fn update_profile_by_profile_id( + &self, + key_manager_state: &keymanager::KeyManagerState, + merchant_key_store: &MerchantKeyStore, + current_state: Profile, + profile_update: ProfileUpdate, + ) -> CustomResult; + + async fn delete_profile_by_profile_id_merchant_id( + &self, + profile_id: &common_utils::id_type::ProfileId, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult; + + async fn list_profile_by_merchant_id( + &self, + key_manager_state: &keymanager::KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult, Self::Error>; +} diff --git a/crates/hyperswitch_domain_models/src/configs.rs b/crates/hyperswitch_domain_models/src/configs.rs new file mode 100644 index 0000000000..eadf8e5c81 --- /dev/null +++ b/crates/hyperswitch_domain_models/src/configs.rs @@ -0,0 +1,39 @@ +use common_utils::errors::CustomResult; +use diesel_models::configs as storage; + +#[async_trait::async_trait] +pub trait ConfigInterface { + type Error; + async fn insert_config( + &self, + config: storage::ConfigNew, + ) -> CustomResult; + + async fn find_config_by_key(&self, key: &str) -> CustomResult; + + async fn find_config_by_key_unwrap_or( + &self, + key: &str, + // If the config is not found it will be created with the default value. + default_config: Option, + ) -> CustomResult; + + async fn find_config_by_key_from_db( + &self, + key: &str, + ) -> CustomResult; + + async fn update_config_by_key( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult; + + async fn update_config_in_database( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult; + + async fn delete_config_by_key(&self, key: &str) -> CustomResult; +} diff --git a/crates/hyperswitch_domain_models/src/customer.rs b/crates/hyperswitch_domain_models/src/customer.rs index 6179e22169..84c9510c4b 100644 --- a/crates/hyperswitch_domain_models/src/customer.rs +++ b/crates/hyperswitch_domain_models/src/customer.rs @@ -17,7 +17,8 @@ use diesel_models::{ customers as storage_types, customers::CustomerUpdateInternal, query::customers as query, }; use error_stack::ResultExt; -use masking::{PeekInterface, Secret, SwitchStrategy}; +use masking::{ExposeOptionInterface, PeekInterface, Secret, SwitchStrategy}; +use router_env::{instrument, tracing}; use rustc_hash::FxHashMap; use time::PrimitiveDateTime; @@ -685,3 +686,57 @@ where storage_scheme: MerchantStorageScheme, ) -> CustomResult; } + +#[cfg(feature = "v1")] +#[instrument] +pub async fn update_connector_customer_in_customers( + connector_label: &str, + customer: Option<&Customer>, + connector_customer_id: Option, +) -> Option { + let mut connector_customer_map = customer + .and_then(|customer| customer.connector_customer.clone().expose_option()) + .and_then(|connector_customer| connector_customer.as_object().cloned()) + .unwrap_or_default(); + + let updated_connector_customer_map = connector_customer_id.map(|connector_customer_id| { + let connector_customer_value = serde_json::Value::String(connector_customer_id); + connector_customer_map.insert(connector_label.to_string(), connector_customer_value); + connector_customer_map + }); + + updated_connector_customer_map + .map(serde_json::Value::Object) + .map( + |connector_customer_value| CustomerUpdate::ConnectorCustomer { + connector_customer: Some(pii::SecretSerdeValue::new(connector_customer_value)), + }, + ) +} + +#[cfg(feature = "v2")] +#[instrument] +pub async fn update_connector_customer_in_customers( + merchant_connector_account: &MerchantConnectorAccountTypeDetails, + customer: Option<&Customer>, + connector_customer_id: Option, +) -> Option { + match merchant_connector_account { + MerchantConnectorAccountTypeDetails::MerchantConnectorAccount(account) => { + connector_customer_id.map(|new_conn_cust_id| { + let connector_account_id = account.get_id().clone(); + let mut connector_customer_map = customer + .and_then(|customer| customer.connector_customer.clone()) + .unwrap_or_default(); + connector_customer_map.insert(connector_account_id, new_conn_cust_id); + CustomerUpdate::ConnectorCustomer { + connector_customer: Some(connector_customer_map), + } + }) + } + // TODO: Construct connector_customer for MerchantConnectorDetails if required by connector. + MerchantConnectorAccountTypeDetails::MerchantConnectorDetails(_) => { + todo!("Handle connector_customer construction for MerchantConnectorDetails"); + } + } +} diff --git a/crates/hyperswitch_domain_models/src/lib.rs b/crates/hyperswitch_domain_models/src/lib.rs index 1a3da30c97..1fb3976376 100644 --- a/crates/hyperswitch_domain_models/src/lib.rs +++ b/crates/hyperswitch_domain_models/src/lib.rs @@ -8,6 +8,7 @@ pub mod callback_mapper; pub mod card_testing_guard_data; pub mod cards_info; pub mod chat; +pub mod configs; pub mod connector_endpoints; pub mod consts; pub mod customer; diff --git a/crates/hyperswitch_domain_models/src/merchant_account.rs b/crates/hyperswitch_domain_models/src/merchant_account.rs index 29105e44e2..86b0f9f139 100644 --- a/crates/hyperswitch_domain_models/src/merchant_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_account.rs @@ -14,7 +14,11 @@ use error_stack::ResultExt; use masking::{PeekInterface, Secret}; use router_env::logger; -use crate::type_encryption::{crypto_operation, AsyncLift, CryptoOperation}; +use crate::{ + behaviour::Conversion, + merchant_key_store, + type_encryption::{crypto_operation, AsyncLift, CryptoOperation}, +}; #[cfg(feature = "v1")] #[derive(Clone, Debug, serde::Serialize)] @@ -586,7 +590,7 @@ impl From for MerchantAccountUpdateInternal { #[cfg(feature = "v2")] #[async_trait::async_trait] -impl super::behaviour::Conversion for MerchantAccount { +impl Conversion for MerchantAccount { type DstType = diesel_models::merchant_account::MerchantAccount; type NewDstType = diesel_models::merchant_account::MerchantAccountNew; async fn convert(self) -> CustomResult { @@ -702,7 +706,7 @@ impl super::behaviour::Conversion for MerchantAccount { #[cfg(feature = "v1")] #[async_trait::async_trait] -impl super::behaviour::Conversion for MerchantAccount { +impl Conversion for MerchantAccount { type DstType = diesel_models::merchant_account::MerchantAccount; type NewDstType = diesel_models::merchant_account::MerchantAccountNew; async fn convert(self) -> CustomResult { @@ -878,3 +882,87 @@ impl MerchantAccount { metadata.and_then(|a| a.compatible_connector) } } + +#[async_trait::async_trait] +pub trait MerchantAccountInterface +where + MerchantAccount: Conversion< + DstType = diesel_models::merchant_account::MerchantAccount, + NewDstType = diesel_models::merchant_account::MerchantAccountNew, + >, +{ + type Error; + async fn insert_merchant( + &self, + state: &keymanager::KeyManagerState, + merchant_account: MerchantAccount, + merchant_key_store: &merchant_key_store::MerchantKeyStore, + ) -> CustomResult; + + async fn find_merchant_account_by_merchant_id( + &self, + state: &keymanager::KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_key_store: &merchant_key_store::MerchantKeyStore, + ) -> CustomResult; + + async fn update_all_merchant_account( + &self, + merchant_account: MerchantAccountUpdate, + ) -> CustomResult; + + async fn update_merchant( + &self, + state: &keymanager::KeyManagerState, + this: MerchantAccount, + merchant_account: MerchantAccountUpdate, + merchant_key_store: &merchant_key_store::MerchantKeyStore, + ) -> CustomResult; + + async fn update_specific_fields_in_merchant( + &self, + state: &keymanager::KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_account: MerchantAccountUpdate, + merchant_key_store: &merchant_key_store::MerchantKeyStore, + ) -> CustomResult; + + async fn find_merchant_account_by_publishable_key( + &self, + state: &keymanager::KeyManagerState, + publishable_key: &str, + ) -> CustomResult<(MerchantAccount, merchant_key_store::MerchantKeyStore), Self::Error>; + + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + state: &keymanager::KeyManagerState, + organization_id: &common_utils::id_type::OrganizationId, + ) -> CustomResult, Self::Error>; + + async fn delete_merchant_account_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult; + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + state: &keymanager::KeyManagerState, + merchant_ids: Vec, + ) -> CustomResult, Self::Error>; + + #[cfg(feature = "olap")] + async fn list_merchant_and_org_ids( + &self, + state: &keymanager::KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult< + Vec<( + common_utils::id_type::MerchantId, + common_utils::id_type::OrganizationId, + )>, + Self::Error, + >; +} diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs index d6600d674f..99fdea17cf 100644 --- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs @@ -16,7 +16,10 @@ use diesel_models::merchant_connector_account::{ MerchantConnectorAccountFeatureMetadata as DieselMerchantConnectorAccountFeatureMetadata, RevenueRecoveryMetadata as DieselRevenueRecoveryMetadata, }; -use diesel_models::{enums, merchant_connector_account::MerchantConnectorAccountUpdateInternal}; +use diesel_models::{ + enums, + merchant_connector_account::{self as storage, MerchantConnectorAccountUpdateInternal}, +}; use error_stack::ResultExt; use masking::{PeekInterface, Secret}; use rustc_hash::FxHashMap; @@ -27,6 +30,7 @@ use super::behaviour; use crate::errors::api_error_response; use crate::{ mandates::CommonMandateReference, + merchant_key_store::MerchantKeyStore, router_data, type_encryption::{crypto_operation, CryptoOperation}, }; @@ -492,40 +496,38 @@ pub enum MerchantConnectorAccountUpdate { #[cfg(feature = "v1")] #[async_trait::async_trait] impl behaviour::Conversion for MerchantConnectorAccount { - type DstType = diesel_models::merchant_connector_account::MerchantConnectorAccount; - type NewDstType = diesel_models::merchant_connector_account::MerchantConnectorAccountNew; + type DstType = storage::MerchantConnectorAccount; + type NewDstType = storage::MerchantConnectorAccountNew; async fn convert(self) -> CustomResult { - Ok( - diesel_models::merchant_connector_account::MerchantConnectorAccount { - merchant_id: self.merchant_id, - connector_name: self.connector_name, - connector_account_details: self.connector_account_details.into(), - test_mode: self.test_mode, - disabled: self.disabled, - merchant_connector_id: self.merchant_connector_id.clone(), - id: Some(self.merchant_connector_id), - payment_methods_enabled: self.payment_methods_enabled, - connector_type: self.connector_type, - metadata: self.metadata, - frm_configs: None, - frm_config: self.frm_configs, - business_country: self.business_country, - business_label: self.business_label, - connector_label: self.connector_label, - business_sub_label: self.business_sub_label, - created_at: self.created_at, - modified_at: self.modified_at, - connector_webhook_details: self.connector_webhook_details, - profile_id: Some(self.profile_id), - applepay_verified_domains: self.applepay_verified_domains, - pm_auth_config: self.pm_auth_config, - status: self.status, - connector_wallets_details: self.connector_wallets_details.map(Encryption::from), - additional_merchant_data: self.additional_merchant_data.map(|data| data.into()), - version: self.version, - }, - ) + Ok(storage::MerchantConnectorAccount { + merchant_id: self.merchant_id, + connector_name: self.connector_name, + connector_account_details: self.connector_account_details.into(), + test_mode: self.test_mode, + disabled: self.disabled, + merchant_connector_id: self.merchant_connector_id.clone(), + id: Some(self.merchant_connector_id), + payment_methods_enabled: self.payment_methods_enabled, + connector_type: self.connector_type, + metadata: self.metadata, + frm_configs: None, + frm_config: self.frm_configs, + business_country: self.business_country, + business_label: self.business_label, + connector_label: self.connector_label, + business_sub_label: self.business_sub_label, + created_at: self.created_at, + modified_at: self.modified_at, + connector_webhook_details: self.connector_webhook_details, + profile_id: Some(self.profile_id), + applepay_verified_domains: self.applepay_verified_domains, + pm_auth_config: self.pm_auth_config, + status: self.status, + connector_wallets_details: self.connector_wallets_details.map(Encryption::from), + additional_merchant_data: self.additional_merchant_data.map(|data| data.into()), + version: self.version, + }) } async fn convert_back( @@ -628,35 +630,33 @@ impl behaviour::Conversion for MerchantConnectorAccount { #[cfg(feature = "v2")] #[async_trait::async_trait] impl behaviour::Conversion for MerchantConnectorAccount { - type DstType = diesel_models::merchant_connector_account::MerchantConnectorAccount; - type NewDstType = diesel_models::merchant_connector_account::MerchantConnectorAccountNew; + type DstType = storage::MerchantConnectorAccount; + type NewDstType = storage::MerchantConnectorAccountNew; async fn convert(self) -> CustomResult { - Ok( - diesel_models::merchant_connector_account::MerchantConnectorAccount { - id: self.id, - merchant_id: self.merchant_id, - connector_name: self.connector_name, - connector_account_details: self.connector_account_details.into(), - disabled: self.disabled, - payment_methods_enabled: self.payment_methods_enabled, - connector_type: self.connector_type, - metadata: self.metadata, - frm_config: self.frm_configs, - connector_label: self.connector_label, - created_at: self.created_at, - modified_at: self.modified_at, - connector_webhook_details: self.connector_webhook_details, - profile_id: self.profile_id, - applepay_verified_domains: self.applepay_verified_domains, - pm_auth_config: self.pm_auth_config, - status: self.status, - connector_wallets_details: self.connector_wallets_details.map(Encryption::from), - additional_merchant_data: self.additional_merchant_data.map(|data| data.into()), - version: self.version, - feature_metadata: self.feature_metadata.map(From::from), - }, - ) + Ok(storage::MerchantConnectorAccount { + id: self.id, + merchant_id: self.merchant_id, + connector_name: self.connector_name, + connector_account_details: self.connector_account_details.into(), + disabled: self.disabled, + payment_methods_enabled: self.payment_methods_enabled, + connector_type: self.connector_type, + metadata: self.metadata, + frm_config: self.frm_configs, + connector_label: self.connector_label, + created_at: self.created_at, + modified_at: self.modified_at, + connector_webhook_details: self.connector_webhook_details, + profile_id: self.profile_id, + applepay_verified_domains: self.applepay_verified_domains, + pm_auth_config: self.pm_auth_config, + status: self.status, + connector_wallets_details: self.connector_wallets_details.map(Encryption::from), + additional_merchant_data: self.additional_merchant_data.map(|data| data.into()), + version: self.version, + feature_metadata: self.feature_metadata.map(From::from), + }) } async fn convert_back( @@ -998,3 +998,117 @@ impl From Self { revenue_recovery } } } + +#[async_trait::async_trait] +pub trait MerchantConnectorAccountInterface +where + MerchantConnectorAccount: behaviour::Conversion< + DstType = storage::MerchantConnectorAccount, + NewDstType = storage::MerchantConnectorAccountNew, + >, +{ + type Error; + #[cfg(feature = "v1")] + async fn find_merchant_connector_account_by_merchant_id_connector_label( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + connector_label: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult; + + #[cfg(feature = "v1")] + async fn find_merchant_connector_account_by_profile_id_connector_name( + &self, + state: &KeyManagerState, + profile_id: &id_type::ProfileId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult; + + #[cfg(feature = "v1")] + async fn find_merchant_connector_account_by_merchant_id_connector_name( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult, Self::Error>; + + async fn insert_merchant_connector_account( + &self, + state: &KeyManagerState, + t: MerchantConnectorAccount, + key_store: &MerchantKeyStore, + ) -> CustomResult; + + #[cfg(feature = "v1")] + async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + merchant_connector_id: &id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn find_merchant_connector_account_by_id( + &self, + state: &KeyManagerState, + id: &id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult; + + async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + get_disabled: bool, + key_store: &MerchantKeyStore, + ) -> CustomResult; + + #[cfg(all(feature = "olap", feature = "v2"))] + async fn list_connector_account_by_profile_id( + &self, + state: &KeyManagerState, + profile_id: &id_type::ProfileId, + key_store: &MerchantKeyStore, + ) -> CustomResult, Self::Error>; + + async fn list_enabled_connector_accounts_by_profile_id( + &self, + state: &KeyManagerState, + profile_id: &id_type::ProfileId, + key_store: &MerchantKeyStore, + connector_type: common_enums::ConnectorType, + ) -> CustomResult, Self::Error>; + + async fn update_merchant_connector_account( + &self, + state: &KeyManagerState, + this: MerchantConnectorAccount, + merchant_connector_account: MerchantConnectorAccountUpdateInternal, + key_store: &MerchantKeyStore, + ) -> CustomResult; + + async fn update_multiple_merchant_connector_accounts( + &self, + this: Vec<( + MerchantConnectorAccount, + MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), Self::Error>; + + #[cfg(feature = "v1")] + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + &self, + merchant_id: &id_type::MerchantId, + merchant_connector_id: &id_type::MerchantConnectorAccountId, + ) -> CustomResult; + + #[cfg(feature = "v2")] + async fn delete_merchant_connector_account_by_id( + &self, + id: &id_type::MerchantConnectorAccountId, + ) -> CustomResult; +} diff --git a/crates/hyperswitch_domain_models/src/merchant_key_store.rs b/crates/hyperswitch_domain_models/src/merchant_key_store.rs index d24e23c579..ebef0b4706 100644 --- a/crates/hyperswitch_domain_models/src/merchant_key_store.rs +++ b/crates/hyperswitch_domain_models/src/merchant_key_store.rs @@ -68,3 +68,42 @@ impl super::behaviour::Conversion for MerchantKeyStore { }) } } + +#[async_trait::async_trait] +pub trait MerchantKeyStoreInterface { + type Error; + async fn insert_merchant_key_store( + &self, + state: &KeyManagerState, + merchant_key_store: MerchantKeyStore, + key: &Secret>, + ) -> CustomResult; + + async fn get_merchant_key_store_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + key: &Secret>, + ) -> CustomResult; + + async fn delete_merchant_key_store_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult; + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + state: &KeyManagerState, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, Self::Error>; + + async fn get_all_key_stores( + &self, + state: &KeyManagerState, + key: &Secret>, + from: u32, + to: u32, + ) -> CustomResult, Self::Error>; +} diff --git a/crates/hyperswitch_interfaces/Cargo.toml b/crates/hyperswitch_interfaces/Cargo.toml index 6b216d5c86..199715a146 100644 --- a/crates/hyperswitch_interfaces/Cargo.toml +++ b/crates/hyperswitch_interfaces/Cargo.toml @@ -29,6 +29,7 @@ serde_json = "1.0.140" strum = { version = "0.26", features = ["derive"] } thiserror = "1.0.69" time = "0.3.41" +unified-connector-service-client = { git = "https://github.com/juspay/connector-service", rev = "f719688943adf7bc17bb93dcb43f27485c17a96e", package = "rust-grpc-client" } # First party crates hyperswitch_domain_models = { version = "0.1.0", path = "../hyperswitch_domain_models", default-features = false } diff --git a/crates/hyperswitch_interfaces/src/api_client.rs b/crates/hyperswitch_interfaces/src/api_client.rs new file mode 100644 index 0000000000..32c007c2ca --- /dev/null +++ b/crates/hyperswitch_interfaces/src/api_client.rs @@ -0,0 +1,658 @@ +use std::{ + fmt::Debug, + time::{Duration, Instant}, +}; + +use common_enums::ApiClientError; +use common_utils::{ + consts::{X_CONNECTOR_NAME, X_FLOW_NAME, X_REQUEST_ID}, + errors::CustomResult, + request::{Request, RequestContent}, +}; +use error_stack::{report, ResultExt}; +use http::Method; +use hyperswitch_domain_models::{ + errors::api_error_response, + router_data::{ErrorResponse, RouterData}, +}; +use masking::Maskable; +use reqwest::multipart::Form; +use router_env::{instrument, logger, tracing, tracing_actix_web::RequestId}; +use serde_json::json; + +use crate::{ + configs, + connector_integration_interface::{ + BoxedConnectorIntegrationInterface, ConnectorEnum, RouterDataConversion, + }, + consts, + errors::ConnectorError, + events, + events::connector_api_logs::ConnectorEvent, + metrics, types, + types::Proxy, + unified_connector_service, +}; + +/// A trait representing a converter for connector names to their corresponding enum variants. +pub trait ConnectorConverter: Send + Sync { + /// Get the connector enum variant by its name + fn get_connector_enum_by_name( + &self, + connector: &str, + ) -> CustomResult; +} +/// A trait representing a builder for HTTP requests. +pub trait RequestBuilder: Send + Sync { + /// Build a JSON request + fn json(&mut self, body: serde_json::Value); + /// Build a URL encoded form request + fn url_encoded_form(&mut self, body: serde_json::Value); + /// Set the timeout duration for the request + fn timeout(&mut self, timeout: Duration); + /// Build a multipart request + fn multipart(&mut self, form: Form); + /// Add a header to the request + fn header(&mut self, key: String, value: Maskable) -> CustomResult<(), ApiClientError>; + /// Send the request and return a future that resolves to the response + fn send( + self, + ) -> CustomResult< + Box> + 'static>, + ApiClientError, + >; +} + +/// A trait representing an API client capable of making HTTP requests. +#[async_trait::async_trait] +pub trait ApiClient: dyn_clone::DynClone +where + Self: Send + Sync, +{ + /// Create a new request with the specified HTTP method and URL + fn request( + &self, + method: Method, + url: String, + ) -> CustomResult, ApiClientError>; + + /// Create a new request with the specified HTTP method, URL, and client certificate + fn request_with_certificate( + &self, + method: Method, + url: String, + certificate: Option>, + certificate_key: Option>, + ) -> CustomResult, ApiClientError>; + + /// Send a request and return the response + async fn send_request( + &self, + state: &dyn ApiClientWrapper, + request: Request, + option_timeout_secs: Option, + forward_to_kafka: bool, + ) -> CustomResult; + + /// Add a request ID to the client for tracking purposes + fn add_request_id(&mut self, request_id: RequestId); + + /// Get the current request ID, if any + fn get_request_id(&self) -> Option; + + /// Get the current request ID as a string, if any + fn get_request_id_str(&self) -> Option; + + /// Add a flow name to the client for tracking purposes + fn add_flow_name(&mut self, flow_name: String); +} + +dyn_clone::clone_trait_object!(ApiClient); + +/// A wrapper trait to get the ApiClient and Proxy from the state +pub trait ApiClientWrapper: Send + Sync { + /// Get the ApiClient instance + fn get_api_client(&self) -> &dyn ApiClient; + /// Get the Proxy configuration + fn get_proxy(&self) -> Proxy; + /// Get the request ID as String if any + fn get_request_id_str(&self) -> Option; + /// Get the request ID as &RequestId if any + fn get_request_id(&self) -> Option; + /// Get the tenant information + fn get_tenant(&self) -> configs::Tenant; + /// Get connectors configuration + fn get_connectors(&self) -> configs::Connectors; + /// Get the event handler + fn event_handler(&self) -> &dyn events::EventHandlerInterface; +} + +/// Handle the flow by interacting with connector module +/// `connector_request` is applicable only in case if the `CallConnectorAction` is `Trigger` +/// In other cases, It will be created if required, even if it is not passed +#[instrument(skip_all, fields(connector_name, payment_method))] +pub async fn execute_connector_processing_step< + 'b, + 'a, + T, + ResourceCommonData: Clone + RouterDataConversion + 'static, + Req: Debug + Clone + 'static, + Resp: Debug + Clone + 'static, +>( + state: &dyn ApiClientWrapper, + connector_integration: BoxedConnectorIntegrationInterface, + req: &'b RouterData, + call_connector_action: common_enums::CallConnectorAction, + connector_request: Option, + return_raw_connector_response: Option, +) -> CustomResult, ConnectorError> +where + T: Clone + Debug + 'static, + // BoxedConnectorIntegration: 'b, +{ + // If needed add an error stack as follows + // connector_integration.build_request(req).attach_printable("Failed to build request"); + tracing::Span::current().record("connector_name", &req.connector); + tracing::Span::current().record("payment_method", req.payment_method.to_string()); + logger::debug!(connector_request=?connector_request); + let mut router_data = req.clone(); + match call_connector_action { + common_enums::CallConnectorAction::HandleResponse(res) => { + let response = types::Response { + headers: None, + response: res.into(), + status_code: 200, + }; + connector_integration.handle_response(req, None, response) + } + common_enums::CallConnectorAction::UCSHandleResponse(transform_data_bytes) => { + handle_ucs_response(router_data, transform_data_bytes) + } + common_enums::CallConnectorAction::Avoid => Ok(router_data), + common_enums::CallConnectorAction::StatusUpdate { + status, + error_code, + error_message, + } => { + router_data.status = status; + let error_response = if error_code.is_some() | error_message.is_some() { + Some(ErrorResponse { + code: error_code.unwrap_or(consts::NO_ERROR_CODE.to_string()), + message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), + status_code: 200, // This status code is ignored in redirection response it will override with 302 status code. + reason: None, + attempt_status: None, + connector_transaction_id: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + connector_metadata: None, + }) + } else { + None + }; + router_data.response = error_response.map(Err).unwrap_or(router_data.response); + Ok(router_data) + } + common_enums::CallConnectorAction::Trigger => { + metrics::CONNECTOR_CALL_COUNT.add( + 1, + router_env::metric_attributes!( + ("connector", req.connector.to_string()), + ( + "flow", + get_flow_name::().unwrap_or_else(|_| "UnknownFlow".to_string()) + ), + ), + ); + + let connector_request = match connector_request { + Some(connector_request) => Some(connector_request), + None => connector_integration + .build_request(req, &state.get_connectors()) + .inspect_err(|error| { + if matches!( + error.current_context(), + &ConnectorError::RequestEncodingFailed + | &ConnectorError::RequestEncodingFailedWithReason(_) + ) { + metrics::REQUEST_BUILD_FAILURE.add( + 1, + router_env::metric_attributes!(( + "connector", + req.connector.clone() + )), + ) + } + })?, + }; + + match connector_request { + Some(mut request) => { + let masked_request_body = match &request.body { + Some(request) => match request { + RequestContent::Json(i) + | RequestContent::FormUrlEncoded(i) + | RequestContent::Xml(i) => i + .masked_serialize() + .unwrap_or(json!({ "error": "failed to mask serialize"})), + RequestContent::FormData((_, i)) => i + .masked_serialize() + .unwrap_or(json!({ "error": "failed to mask serialize"})), + RequestContent::RawBytes(_) => json!({"request_type": "RAW_BYTES"}), + }, + None => serde_json::Value::Null, + }; + let flow_name = + get_flow_name::().unwrap_or_else(|_| "UnknownFlow".to_string()); + request.headers.insert(( + X_FLOW_NAME.to_string(), + Maskable::Masked(masking::Secret::new(flow_name.to_string())), + )); + let connector_name = req.connector.clone(); + request.headers.insert(( + X_CONNECTOR_NAME.to_string(), + Maskable::Masked(masking::Secret::new(connector_name.clone().to_string())), + )); + state.get_request_id().as_ref().map(|id| { + let request_id = id.to_string(); + request.headers.insert(( + X_REQUEST_ID.to_string(), + Maskable::Normal(request_id.clone()), + )); + request_id + }); + let request_url = request.url.clone(); + let request_method = request.method; + let current_time = Instant::now(); + let response = + call_connector_api(state, request, "execute_connector_processing_step") + .await; + let external_latency = current_time.elapsed().as_millis(); + logger::info!(raw_connector_request=?masked_request_body); + let status_code = response + .as_ref() + .map(|i| { + i.as_ref() + .map_or_else(|value| value.status_code, |value| value.status_code) + }) + .unwrap_or_default(); + let mut connector_event = ConnectorEvent::new( + state.get_tenant().tenant_id.clone(), + req.connector.clone(), + std::any::type_name::(), + masked_request_body, + request_url, + request_method, + req.payment_id.clone(), + req.merchant_id.clone(), + state.get_request_id().as_ref(), + external_latency, + req.refund_id.clone(), + req.dispute_id.clone(), + status_code, + ); + + match response { + Ok(body) => { + let response = match body { + Ok(body) => { + let connector_http_status_code = Some(body.status_code); + let handle_response_result = connector_integration + .handle_response( + req, + Some(&mut connector_event), + body.clone(), + ) + .inspect_err(|error| { + if error.current_context() + == &ConnectorError::ResponseDeserializationFailed + { + metrics::RESPONSE_DESERIALIZATION_FAILURE.add( + 1, + router_env::metric_attributes!(( + "connector", + req.connector.clone(), + )), + ) + } + }); + match handle_response_result { + Ok(mut data) => { + state + .event_handler() + .log_connector_event(&connector_event); + data.connector_http_status_code = + connector_http_status_code; + // Add up multiple external latencies in case of multiple external calls within the same request. + data.external_latency = Some( + data.external_latency + .map_or(external_latency, |val| { + val + external_latency + }), + ); + + store_raw_connector_response_if_required( + return_raw_connector_response, + &mut data, + &body, + )?; + + Ok(data) + } + Err(err) => { + connector_event + .set_error(json!({"error": err.to_string()})); + + state + .event_handler() + .log_connector_event(&connector_event); + Err(err) + } + }? + } + Err(body) => { + router_data.connector_http_status_code = Some(body.status_code); + router_data.external_latency = Some( + router_data + .external_latency + .map_or(external_latency, |val| val + external_latency), + ); + metrics::CONNECTOR_ERROR_RESPONSE_COUNT.add( + 1, + router_env::metric_attributes!(( + "connector", + req.connector.clone(), + )), + ); + + store_raw_connector_response_if_required( + return_raw_connector_response, + &mut router_data, + &body, + )?; + + let error = match body.status_code { + 500..=511 => { + let error_res = connector_integration + .get_5xx_error_response( + body, + Some(&mut connector_event), + )?; + state + .event_handler() + .log_connector_event(&connector_event); + error_res + } + _ => { + let error_res = connector_integration + .get_error_response( + body, + Some(&mut connector_event), + )?; + if let Some(status) = error_res.attempt_status { + router_data.status = status; + }; + state + .event_handler() + .log_connector_event(&connector_event); + error_res + } + }; + + router_data.response = Err(error); + + router_data + } + }; + Ok(response) + } + Err(error) => { + connector_event.set_error(json!({"error": error.to_string()})); + state.event_handler().log_connector_event(&connector_event); + if error.current_context().is_upstream_timeout() { + let error_response = ErrorResponse { + code: consts::REQUEST_TIMEOUT_ERROR_CODE.to_string(), + message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), + reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), + status_code: 504, + attempt_status: None, + connector_transaction_id: None, + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + connector_metadata: None, + }; + router_data.response = Err(error_response); + router_data.connector_http_status_code = Some(504); + router_data.external_latency = Some( + router_data + .external_latency + .map_or(external_latency, |val| val + external_latency), + ); + Ok(router_data) + } else { + Err(error + .change_context(ConnectorError::ProcessingStepFailed(None))) + } + } + } + } + None => Ok(router_data), + } + } + } +} + +/// Handle UCS webhook response processing +pub fn handle_ucs_response( + router_data: RouterData, + transform_data_bytes: Vec, +) -> CustomResult, ConnectorError> +where + T: Clone + Debug + 'static, + Req: Debug + Clone + 'static, + Resp: Debug + Clone + 'static, +{ + let webhook_transform_data: unified_connector_service::WebhookTransformData = + serde_json::from_slice(&transform_data_bytes) + .change_context(ConnectorError::ResponseDeserializationFailed) + .attach_printable("Failed to deserialize UCS webhook transform data")?; + + let webhook_content = webhook_transform_data + .webhook_content + .ok_or(ConnectorError::ResponseDeserializationFailed) + .attach_printable("UCS webhook transform data missing webhook_content")?; + + let payment_get_response = match webhook_content.content { + Some(unified_connector_service_client::payments::webhook_response_content::Content::PaymentsResponse(payments_response)) => { + Ok(payments_response) + }, + Some(unified_connector_service_client::payments::webhook_response_content::Content::RefundsResponse(_)) => { + Err(ConnectorError::ProcessingStepFailed(Some("UCS webhook contains refund response but payment processing was expected".to_string().into())).into()) + }, + Some(unified_connector_service_client::payments::webhook_response_content::Content::DisputesResponse(_)) => { + Err(ConnectorError::ProcessingStepFailed(Some("UCS webhook contains dispute response but payment processing was expected".to_string().into())).into()) + }, + Some(unified_connector_service_client::payments::webhook_response_content::Content::IncompleteTransformation(_)) => { + Err(ConnectorError::ProcessingStepFailed(Some("UCS webhook contains incomplete transformation but payment processing was expected".to_string().into())).into()) + }, + None => { + Err(ConnectorError::ResponseDeserializationFailed) + .attach_printable("UCS webhook content missing payments_response") + } + }?; + + let (router_data_response, status_code) = + unified_connector_service::handle_unified_connector_service_response_for_payment_get( + payment_get_response.clone(), + ) + .change_context(ConnectorError::ProcessingStepFailed(None)) + .attach_printable("Failed to process UCS webhook response using PSync handler")?; + + let mut updated_router_data = router_data; + let router_data_response = router_data_response.map(|(response, status)| { + updated_router_data.status = status; + response + }); + + let _ = router_data_response.map_err(|error_response| { + updated_router_data.response = Err(error_response); + }); + updated_router_data.raw_connector_response = payment_get_response + .raw_connector_response + .map(masking::Secret::new); + updated_router_data.connector_http_status_code = Some(status_code); + + Ok(updated_router_data) +} + +/// Calls the connector API and handles the response +#[instrument(skip_all)] +pub async fn call_connector_api( + state: &dyn ApiClientWrapper, + request: Request, + flow_name: &str, +) -> CustomResult, ApiClientError> { + let current_time = Instant::now(); + let headers = request.headers.clone(); + let url = request.url.clone(); + let response = state + .get_api_client() + .send_request(state, request, None, true) + .await; + + match response.as_ref() { + Ok(resp) => { + let status_code = resp.status().as_u16(); + let elapsed_time = current_time.elapsed(); + logger::info!( + ?headers, + url, + status_code, + flow=?flow_name, + ?elapsed_time + ); + } + Err(err) => { + logger::info!( + call_connector_api_error=?err + ); + } + } + + handle_response(response).await +} + +/// Handle the response from the API call +#[instrument(skip_all)] +pub async fn handle_response( + response: CustomResult, +) -> CustomResult, ApiClientError> { + response + .map(|response| async { + logger::info!(?response); + let status_code = response.status().as_u16(); + let headers = Some(response.headers().to_owned()); + match status_code { + 200..=202 | 302 | 204 => { + // If needed add log line + // logger:: error!( error_parsing_response=?err); + let response = response + .bytes() + .await + .change_context(ApiClientError::ResponseDecodingFailed) + .attach_printable("Error while waiting for response")?; + Ok(Ok(types::Response { + headers, + response, + status_code, + })) + } + + status_code @ 500..=599 => { + let bytes = response.bytes().await.map_err(|error| { + report!(error) + .change_context(ApiClientError::ResponseDecodingFailed) + .attach_printable("Client error response received") + })?; + // let error = match status_code { + // 500 => ApiClientError::InternalServerErrorReceived, + // 502 => ApiClientError::BadGatewayReceived, + // 503 => ApiClientError::ServiceUnavailableReceived, + // 504 => ApiClientError::GatewayTimeoutReceived, + // _ => ApiClientError::UnexpectedServerResponse, + // }; + Ok(Err(types::Response { + headers, + response: bytes, + status_code, + })) + } + + status_code @ 400..=499 => { + let bytes = response.bytes().await.map_err(|error| { + report!(error) + .change_context(ApiClientError::ResponseDecodingFailed) + .attach_printable("Client error response received") + })?; + /* let error = match status_code { + 400 => ApiClientError::BadRequestReceived(bytes), + 401 => ApiClientError::UnauthorizedReceived(bytes), + 403 => ApiClientError::ForbiddenReceived, + 404 => ApiClientError::NotFoundReceived(bytes), + 405 => ApiClientError::MethodNotAllowedReceived, + 408 => ApiClientError::RequestTimeoutReceived, + 422 => ApiClientError::UnprocessableEntityReceived(bytes), + 429 => ApiClientError::TooManyRequestsReceived, + _ => ApiClientError::UnexpectedServerResponse, + }; + Err(report!(error).attach_printable("Client error response received")) + */ + Ok(Err(types::Response { + headers, + response: bytes, + status_code, + })) + } + + _ => Err(report!(ApiClientError::UnexpectedServerResponse) + .attach_printable("Unexpected response from server")), + } + })? + .await +} + +/// Store the raw connector response in the router data if required +pub fn store_raw_connector_response_if_required( + return_raw_connector_response: Option, + router_data: &mut RouterData, + body: &types::Response, +) -> CustomResult<(), ConnectorError> +where + T: Clone + Debug + 'static, + Req: Debug + Clone + 'static, + Resp: Debug + Clone + 'static, +{ + if return_raw_connector_response == Some(true) { + let mut decoded = String::from_utf8(body.response.as_ref().to_vec()) + .change_context(ConnectorError::ResponseDeserializationFailed)?; + if decoded.starts_with('\u{feff}') { + decoded = decoded.trim_start_matches('\u{feff}').to_string(); + } + router_data.raw_connector_response = Some(masking::Secret::new(decoded)); + } + Ok(()) +} + +/// Get the flow name from the type +#[inline] +pub fn get_flow_name() -> CustomResult { + Ok(std::any::type_name::() + .to_string() + .rsplit("::") + .next() + .ok_or(api_error_response::ApiErrorResponse::InternalServerError) + .attach_printable("Flow stringify failed")? + .to_string()) +} diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index 4b4594aebf..4e28441016 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -1 +1,191 @@ -pub use hyperswitch_domain_models::connector_endpoints::Connectors; +use common_utils::{crypto::Encryptable, errors::CustomResult, id_type}; +pub use hyperswitch_domain_models::{ + connector_endpoints::Connectors, errors::api_error_response, merchant_connector_account, +}; +use masking::{PeekInterface, Secret}; +use serde::Deserialize; + +#[allow(missing_docs)] +#[derive(Debug, Clone)] +pub struct Tenant { + pub tenant_id: id_type::TenantId, + pub base_url: String, + pub schema: String, + pub accounts_schema: String, + pub redis_key_prefix: String, + pub clickhouse_database: String, + pub user: TenantUserConfig, +} + +#[allow(missing_docs)] +#[derive(Debug, Deserialize, Clone)] +pub struct TenantUserConfig { + pub control_center_url: String, +} + +impl common_utils::types::TenantConfig for Tenant { + fn get_tenant_id(&self) -> &id_type::TenantId { + &self.tenant_id + } + fn get_accounts_schema(&self) -> &str { + self.accounts_schema.as_str() + } + fn get_schema(&self) -> &str { + self.schema.as_str() + } + fn get_redis_key_prefix(&self) -> &str { + self.redis_key_prefix.as_str() + } + fn get_clickhouse_database(&self) -> &str { + self.clickhouse_database.as_str() + } +} + +#[allow(missing_docs)] +// Todo: Global tenant should not be part of tenant config(https://github.com/juspay/hyperswitch/issues/7237) +#[derive(Debug, Deserialize, Clone)] +pub struct GlobalTenant { + #[serde(default = "id_type::TenantId::get_default_global_tenant_id")] + pub tenant_id: id_type::TenantId, + pub schema: String, + pub redis_key_prefix: String, + pub clickhouse_database: String, +} + +// Todo: Global tenant should not be part of tenant config +impl common_utils::types::TenantConfig for GlobalTenant { + fn get_tenant_id(&self) -> &id_type::TenantId { + &self.tenant_id + } + fn get_accounts_schema(&self) -> &str { + self.schema.as_str() + } + fn get_schema(&self) -> &str { + self.schema.as_str() + } + fn get_redis_key_prefix(&self) -> &str { + self.redis_key_prefix.as_str() + } + fn get_clickhouse_database(&self) -> &str { + self.clickhouse_database.as_str() + } +} + +impl Default for 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(missing_docs)] +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(default)] +pub struct InternalMerchantIdProfileIdAuthSettings { + pub enabled: bool, + pub internal_api_key: Secret, +} + +#[allow(missing_docs)] +#[derive(Debug, Deserialize, Clone, Default)] +#[serde(default)] +pub struct InternalServicesConfig { + pub payments_base_url: String, +} + +#[allow(missing_docs)] +#[derive(Debug, Clone)] +pub enum MerchantConnectorAccountType { + DbVal(Box), + CacheVal(api_models::admin::MerchantConnectorDetails), +} + +#[allow(missing_docs)] +impl MerchantConnectorAccountType { + pub fn get_metadata(&self) -> Option> { + match self { + Self::DbVal(val) => val.metadata.to_owned(), + Self::CacheVal(val) => val.metadata.to_owned(), + } + } + + pub fn get_connector_account_details(&self) -> serde_json::Value { + match self { + Self::DbVal(val) => val.connector_account_details.peek().to_owned(), + Self::CacheVal(val) => val.connector_account_details.peek().to_owned(), + } + } + + pub fn get_connector_wallets_details(&self) -> Option> { + match self { + Self::DbVal(val) => val.connector_wallets_details.as_deref().cloned(), + Self::CacheVal(_) => None, + } + } + + pub fn is_disabled(&self) -> bool { + match self { + Self::DbVal(ref inner) => inner.disabled.unwrap_or(false), + // Cached merchant connector account, only contains the account details, + // the merchant connector account must only be cached if it's not disabled + Self::CacheVal(_) => false, + } + } + + #[cfg(feature = "v1")] + pub fn is_test_mode_on(&self) -> Option { + match self { + Self::DbVal(val) => val.test_mode, + Self::CacheVal(_) => None, + } + } + + #[cfg(feature = "v2")] + pub fn is_test_mode_on(&self) -> Option { + None + } + + pub fn get_mca_id(&self) -> Option { + match self { + Self::DbVal(db_val) => Some(db_val.get_id()), + Self::CacheVal(_) => None, + } + } + + #[cfg(feature = "v1")] + pub fn get_connector_name(&self) -> Option { + match self { + Self::DbVal(db_val) => Some(db_val.connector_name.to_string()), + Self::CacheVal(_) => None, + } + } + + #[cfg(feature = "v2")] + pub fn get_connector_name(&self) -> Option { + match self { + Self::DbVal(db_val) => Some(db_val.connector_name), + Self::CacheVal(_) => None, + } + } + + pub fn get_additional_merchant_data(&self) -> Option>> { + match self { + Self::DbVal(db_val) => db_val.additional_merchant_data.clone(), + Self::CacheVal(_) => None, + } + } + + pub fn get_webhook_details( + &self, + ) -> CustomResult>, api_error_response::ApiErrorResponse> + { + match self { + Self::DbVal(db_val) => Ok(db_val.connector_webhook_details.as_ref()), + Self::CacheVal(_) => Ok(None), + } + } +} diff --git a/crates/hyperswitch_interfaces/src/consts.rs b/crates/hyperswitch_interfaces/src/consts.rs index 2ac6ac3c10..ec9dc08ae7 100644 --- a/crates/hyperswitch_interfaces/src/consts.rs +++ b/crates/hyperswitch_interfaces/src/consts.rs @@ -11,3 +11,21 @@ pub const ACCEPT_HEADER: &str = "text/html,application/json"; /// User agent for request send from backend server pub const USER_AGENT: &str = "Hyperswitch-Backend-Server"; + +/// Request timeout error code +pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT"; + +/// error message for timed out request +pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time"; + +/// Header value indicating that signature-key-based authentication is used. +pub const UCS_AUTH_SIGNATURE_KEY: &str = "signature-key"; + +/// Header value indicating that body-key-based authentication is used. +pub const UCS_AUTH_BODY_KEY: &str = "body-key"; + +/// Header value indicating that header-key-based authentication is used. +pub const UCS_AUTH_HEADER_KEY: &str = "header-key"; + +/// Header value indicating that currency-auth-key-based authentication is used. +pub const UCS_AUTH_CURRENCY_AUTH_KEY: &str = "currency-auth-key"; diff --git a/crates/hyperswitch_interfaces/src/events.rs b/crates/hyperswitch_interfaces/src/events.rs index 54f24c2ec1..c03ac35d32 100644 --- a/crates/hyperswitch_interfaces/src/events.rs +++ b/crates/hyperswitch_interfaces/src/events.rs @@ -1,4 +1,15 @@ -//! Events interface +use crate::events::connector_api_logs::ConnectorEvent; pub mod connector_api_logs; pub mod routing_api_logs; + +/// Event handling interface +#[async_trait::async_trait] +pub trait EventHandlerInterface: dyn_clone::DynClone +where + Self: Send + Sync, +{ + /// Logs connector events + #[track_caller] + fn log_connector_event(&self, event: &ConnectorEvent); +} diff --git a/crates/hyperswitch_interfaces/src/helpers.rs b/crates/hyperswitch_interfaces/src/helpers.rs new file mode 100644 index 0000000000..9bde56eda6 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/helpers.rs @@ -0,0 +1,7 @@ +/// Trait for converting from one foreign type to another +pub trait ForeignTryFrom: Sized { + /// Custom error for conversion failure + type Error; + /// Convert from a foreign type to the current type and return an error if the conversion fails + fn foreign_try_from(from: F) -> Result; +} diff --git a/crates/hyperswitch_interfaces/src/lib.rs b/crates/hyperswitch_interfaces/src/lib.rs index b9c16f6d82..6f7fb34eae 100644 --- a/crates/hyperswitch_interfaces/src/lib.rs +++ b/crates/hyperswitch_interfaces/src/lib.rs @@ -2,6 +2,8 @@ #![warn(missing_docs, missing_debug_implementations)] pub mod api; +/// API client interface module +pub mod api_client; pub mod authentication; /// Configuration related functionalities pub mod configs; @@ -16,12 +18,17 @@ pub mod conversion_impls; pub mod disputes; pub mod encryption_interface; pub mod errors; +/// Event handling interface pub mod events; +/// helper utils +pub mod helpers; /// connector integrity check interface pub mod integrity; pub mod metrics; pub mod secrets_interface; pub mod types; +/// ucs handlers +pub mod unified_connector_service; pub mod webhooks; /// Crm interface diff --git a/crates/hyperswitch_interfaces/src/metrics.rs b/crates/hyperswitch_interfaces/src/metrics.rs index 84aa6d10be..595cd5d49e 100644 --- a/crates/hyperswitch_interfaces/src/metrics.rs +++ b/crates/hyperswitch_interfaces/src/metrics.rs @@ -5,3 +5,10 @@ use router_env::{counter_metric, global_meter}; global_meter!(GLOBAL_METER, "ROUTER_API"); counter_metric!(UNIMPLEMENTED_FLOW, GLOBAL_METER); + +counter_metric!(CONNECTOR_CALL_COUNT, GLOBAL_METER); // Attributes needed + +counter_metric!(RESPONSE_DESERIALIZATION_FAILURE, GLOBAL_METER); +counter_metric!(CONNECTOR_ERROR_RESPONSE_COUNT, GLOBAL_METER); +// Connector Level Metric +counter_metric!(REQUEST_BUILD_FAILURE, GLOBAL_METER); diff --git a/crates/hyperswitch_interfaces/src/unified_connector_service.rs b/crates/hyperswitch_interfaces/src/unified_connector_service.rs new file mode 100644 index 0000000000..be8d6cc7c8 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/unified_connector_service.rs @@ -0,0 +1,34 @@ +use common_enums::AttemptStatus; +use common_utils::errors::CustomResult; +use hyperswitch_domain_models::{ + router_data::ErrorResponse, router_response_types::PaymentsResponseData, +}; +use unified_connector_service_client::payments as payments_grpc; + +use crate::helpers::ForeignTryFrom; + +/// Unified Connector Service (UCS) related transformers +pub mod transformers; + +pub use transformers::WebhookTransformData; + +/// Type alias for return type used by unified connector service response handlers +type UnifiedConnectorServiceResult = CustomResult< + ( + Result<(PaymentsResponseData, AttemptStatus), ErrorResponse>, + u16, + ), + transformers::UnifiedConnectorServiceError, +>; + +#[allow(missing_docs)] +pub fn handle_unified_connector_service_response_for_payment_get( + response: payments_grpc::PaymentServiceGetResponse, +) -> UnifiedConnectorServiceResult { + let status_code = transformers::convert_connector_service_status_code(response.status_code)?; + + let router_data_response = + Result::<(PaymentsResponseData, AttemptStatus), ErrorResponse>::foreign_try_from(response)?; + + Ok((router_data_response, status_code)) +} diff --git a/crates/hyperswitch_interfaces/src/unified_connector_service/transformers.rs b/crates/hyperswitch_interfaces/src/unified_connector_service/transformers.rs new file mode 100644 index 0000000000..128814f05a --- /dev/null +++ b/crates/hyperswitch_interfaces/src/unified_connector_service/transformers.rs @@ -0,0 +1,230 @@ +use common_enums::AttemptStatus; +use hyperswitch_domain_models::{ + router_data::ErrorResponse, router_response_types::PaymentsResponseData, +}; + +use crate::{helpers::ForeignTryFrom, unified_connector_service::payments_grpc}; + +/// Unified Connector Service error variants +#[derive(Debug, Clone, thiserror::Error)] +pub enum UnifiedConnectorServiceError { + /// Error occurred while communicating with the gRPC server. + #[error("Error from gRPC Server : {0}")] + ConnectionError(String), + + /// Failed to encode the request to the unified connector service. + #[error("Failed to encode unified connector service request")] + RequestEncodingFailed, + + /// Request encoding failed due to a specific reason. + #[error("Request encoding failed : {0}")] + RequestEncodingFailedWithReason(String), + + /// Failed to deserialize the response from the connector. + #[error("Failed to deserialize connector response")] + ResponseDeserializationFailed, + + /// The connector name provided is invalid or unrecognized. + #[error("An invalid connector name was provided")] + InvalidConnectorName, + + /// Connector name is missing + #[error("Connector name is missing")] + MissingConnectorName, + + /// A required field was missing in the request. + #[error("Missing required field: {field_name}")] + MissingRequiredField { + /// Missing Field + field_name: &'static str, + }, + + /// Multiple required fields were missing in the request. + #[error("Missing required fields: {field_names:?}")] + MissingRequiredFields { + /// Missing Fields + field_names: Vec<&'static str>, + }, + + /// The requested step or feature is not yet implemented. + #[error("This step has not been implemented for: {0}")] + NotImplemented(String), + + /// Parsing of some value or input failed. + #[error("Parsing failed")] + ParsingFailed, + + /// Data format provided is invalid + #[error("Invalid Data format")] + InvalidDataFormat { + /// Field Name for which data is invalid + field_name: &'static str, + }, + + /// Failed to obtain authentication type + #[error("Failed to obtain authentication type")] + FailedToObtainAuthType, + + /// Failed to inject metadata into request headers + #[error("Failed to inject metadata into request headers: {0}")] + HeaderInjectionFailed(String), + + /// Failed to perform Payment Authorize from gRPC Server + #[error("Failed to perform Payment Authorize from gRPC Server")] + PaymentAuthorizeFailure, + + /// Failed to perform Payment Get from gRPC Server + #[error("Failed to perform Payment Get from gRPC Server")] + PaymentGetFailure, + + /// Failed to perform Payment Setup Mandate from gRPC Server + #[error("Failed to perform Setup Mandate from gRPC Server")] + PaymentRegisterFailure, + + /// Failed to perform Payment Repeat Payment from gRPC Server + #[error("Failed to perform Repeat Payment from gRPC Server")] + PaymentRepeatEverythingFailure, + + /// Failed to transform incoming webhook from gRPC Server + #[error("Failed to transform incoming webhook from gRPC Server")] + WebhookTransformFailure, +} + +#[allow(missing_docs)] +/// Webhook transform data structure containing UCS response information +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct WebhookTransformData { + pub event_type: api_models::webhooks::IncomingWebhookEvent, + pub source_verified: bool, + pub webhook_content: Option, + pub response_ref_id: Option, +} + +impl ForeignTryFrom + for Result<(PaymentsResponseData, AttemptStatus), ErrorResponse> +{ + type Error = error_stack::Report; + + fn foreign_try_from( + response: payments_grpc::PaymentServiceGetResponse, + ) -> Result { + let connector_response_reference_id = + response.response_ref_id.as_ref().and_then(|identifier| { + identifier + .id_type + .clone() + .and_then(|id_type| match id_type { + payments_grpc::identifier::IdType::Id(id) => Some(id), + payments_grpc::identifier::IdType::EncodedData(encoded_data) => { + Some(encoded_data) + } + payments_grpc::identifier::IdType::NoResponseIdMarker(_) => None, + }) + }); + + let status_code = convert_connector_service_status_code(response.status_code)?; + + let resource_id: hyperswitch_domain_models::router_request_types::ResponseId = match response.transaction_id.as_ref().and_then(|id| id.id_type.clone()) { + Some(payments_grpc::identifier::IdType::Id(id)) => hyperswitch_domain_models::router_request_types::ResponseId::ConnectorTransactionId(id), + Some(payments_grpc::identifier::IdType::EncodedData(encoded_data)) => hyperswitch_domain_models::router_request_types::ResponseId::EncodedData(encoded_data), + Some(payments_grpc::identifier::IdType::NoResponseIdMarker(_)) | None => hyperswitch_domain_models::router_request_types::ResponseId::NoResponseId, + }; + + let response = if response.error_code.is_some() { + let attempt_status = match response.status() { + payments_grpc::PaymentStatus::AttemptStatusUnspecified => None, + _ => Some(AttemptStatus::foreign_try_from(response.status())?), + }; + + Err(ErrorResponse { + code: response.error_code().to_owned(), + message: response.error_message().to_owned(), + reason: Some(response.error_message().to_owned()), + status_code, + attempt_status, + connector_transaction_id: connector_response_reference_id, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + connector_metadata: None, + }) + } else { + let status = AttemptStatus::foreign_try_from(response.status())?; + + Ok(( + PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data: Box::new(None), + mandate_reference: Box::new(response.mandate_reference.map(|grpc_mandate| { + hyperswitch_domain_models::router_response_types::MandateReference { + connector_mandate_id: grpc_mandate.mandate_id, + payment_method_id: None, + mandate_metadata: None, + connector_mandate_request_reference_id: None, + } + })), + connector_metadata: None, + network_txn_id: response.network_txn_id.clone(), + connector_response_reference_id, + incremental_authorization_allowed: None, + charges: None, + }, + status, + )) + }; + + Ok(response) + } +} + +impl ForeignTryFrom for AttemptStatus { + type Error = error_stack::Report; + + fn foreign_try_from(grpc_status: payments_grpc::PaymentStatus) -> Result { + match grpc_status { + payments_grpc::PaymentStatus::Started => Ok(Self::Started), + payments_grpc::PaymentStatus::AuthenticationFailed => Ok(Self::AuthenticationFailed), + payments_grpc::PaymentStatus::RouterDeclined => Ok(Self::RouterDeclined), + payments_grpc::PaymentStatus::AuthenticationPending => Ok(Self::AuthenticationPending), + payments_grpc::PaymentStatus::AuthenticationSuccessful => { + Ok(Self::AuthenticationSuccessful) + } + payments_grpc::PaymentStatus::Authorized => Ok(Self::Authorized), + payments_grpc::PaymentStatus::AuthorizationFailed => Ok(Self::AuthorizationFailed), + payments_grpc::PaymentStatus::Charged => Ok(Self::Charged), + payments_grpc::PaymentStatus::Authorizing => Ok(Self::Authorizing), + payments_grpc::PaymentStatus::CodInitiated => Ok(Self::CodInitiated), + payments_grpc::PaymentStatus::Voided => Ok(Self::Voided), + payments_grpc::PaymentStatus::VoidInitiated => Ok(Self::VoidInitiated), + payments_grpc::PaymentStatus::CaptureInitiated => Ok(Self::CaptureInitiated), + payments_grpc::PaymentStatus::CaptureFailed => Ok(Self::CaptureFailed), + payments_grpc::PaymentStatus::VoidFailed => Ok(Self::VoidFailed), + payments_grpc::PaymentStatus::AutoRefunded => Ok(Self::AutoRefunded), + payments_grpc::PaymentStatus::PartialCharged => Ok(Self::PartialCharged), + payments_grpc::PaymentStatus::PartialChargedAndChargeable => { + Ok(Self::PartialChargedAndChargeable) + } + payments_grpc::PaymentStatus::Unresolved => Ok(Self::Unresolved), + payments_grpc::PaymentStatus::Pending => Ok(Self::Pending), + payments_grpc::PaymentStatus::Failure => Ok(Self::Failure), + payments_grpc::PaymentStatus::PaymentMethodAwaited => Ok(Self::PaymentMethodAwaited), + payments_grpc::PaymentStatus::ConfirmationAwaited => Ok(Self::ConfirmationAwaited), + payments_grpc::PaymentStatus::DeviceDataCollectionPending => { + Ok(Self::DeviceDataCollectionPending) + } + payments_grpc::PaymentStatus::AttemptStatusUnspecified => Ok(Self::Unresolved), + } + } +} + +#[allow(missing_docs)] +pub fn convert_connector_service_status_code( + status_code: u32, +) -> Result> { + u16::try_from(status_code).map_err(|err| { + UnifiedConnectorServiceError::RequestEncodingFailedWithReason(format!( + "Failed to convert connector service status code to u16: {err}" + )) + .into() + }) +} diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index b3486257ef..2a3cf49f6c 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -24,7 +24,7 @@ stripe = [] release = ["stripe", "email", "accounts_cache", "kv_store", "vergen", "recon", "external_services/aws_kms", "external_services/aws_s3", "keymanager_mtls", "keymanager_create", "encryption_service", "dynamic_routing", "payout_retry"] oltp = ["storage_impl/oltp"] kv_store = ["scheduler/kv_store"] -accounts_cache = [] +accounts_cache = ["storage_impl/accounts_cache"] vergen = ["router_env/vergen"] dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "hyperswitch_interfaces/dummy_connector", "kgraph_utils/dummy_connector", "payment_methods/dummy_connector", "hyperswitch_domain_models/dummy_connector","hyperswitch_connectors/dummy_connector"] external_access_dc = ["dummy_connector"] diff --git a/crates/router/src/configs/defaults.rs b/crates/router/src/configs/defaults.rs index dee7dfbb03..703631da6d 100644 --- a/crates/router/src/configs/defaults.rs +++ b/crates/router/src/configs/defaults.rs @@ -1,7 +1,5 @@ use std::collections::HashSet; -use common_utils::id_type; - #[cfg(feature = "payouts")] pub mod payout_required_fields; @@ -124,17 +122,6 @@ impl Default for super::settings::KvConfig { } } -impl Default for super::settings::GlobalTenant { - fn default() -> Self { - Self { - tenant_id: id_type::TenantId::get_default_global_tenant_id(), - schema: String::from("global"), - redis_key_prefix: String::from("global"), - clickhouse_database: String::from("global"), - } - } -} - #[allow(clippy::derivable_impls)] impl Default for super::settings::ApiKeys { fn default() -> Self { diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 743c308192..13bcee2724 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -26,8 +26,11 @@ use external_services::{ }, superposition::SuperpositionClientConfig, }; -pub use hyperswitch_interfaces::configs::Connectors; -use hyperswitch_interfaces::{ +pub use hyperswitch_interfaces::{ + configs::{ + Connectors, GlobalTenant, InternalMerchantIdProfileIdAuthSettings, InternalServicesConfig, + Tenant, TenantUserConfig, + }, secrets_interface::secret_state::{ RawSecret, SecretState, SecretStateContainer, SecuredSecret, }, @@ -345,68 +348,6 @@ pub struct L2L3DataConfig { pub enabled: bool, } -#[derive(Debug, Clone)] -pub struct Tenant { - pub tenant_id: id_type::TenantId, - pub base_url: String, - pub schema: String, - pub accounts_schema: String, - pub redis_key_prefix: String, - pub clickhouse_database: String, - pub user: TenantUserConfig, -} - -#[derive(Debug, Deserialize, Clone)] -pub struct TenantUserConfig { - pub control_center_url: String, -} - -impl storage_impl::config::TenantConfig for Tenant { - fn get_tenant_id(&self) -> &id_type::TenantId { - &self.tenant_id - } - fn get_accounts_schema(&self) -> &str { - self.accounts_schema.as_str() - } - fn get_schema(&self) -> &str { - self.schema.as_str() - } - fn get_redis_key_prefix(&self) -> &str { - self.redis_key_prefix.as_str() - } - fn get_clickhouse_database(&self) -> &str { - self.clickhouse_database.as_str() - } -} - -// Todo: Global tenant should not be part of tenant config(https://github.com/juspay/hyperswitch/issues/7237) -#[derive(Debug, Deserialize, Clone)] -pub struct GlobalTenant { - #[serde(default = "id_type::TenantId::get_default_global_tenant_id")] - pub tenant_id: id_type::TenantId, - pub schema: String, - pub redis_key_prefix: String, - pub clickhouse_database: String, -} -// Todo: Global tenant should not be part of tenant config -impl storage_impl::config::TenantConfig for GlobalTenant { - fn get_tenant_id(&self) -> &id_type::TenantId { - &self.tenant_id - } - fn get_accounts_schema(&self) -> &str { - self.schema.as_str() - } - fn get_schema(&self) -> &str { - self.schema.as_str() - } - fn get_redis_key_prefix(&self) -> &str { - self.redis_key_prefix.as_str() - } - fn get_clickhouse_database(&self) -> &str { - self.clickhouse_database.as_str() - } -} - #[derive(Debug, Deserialize, Clone, Default)] pub struct UnmaskedHeaders { #[serde(deserialize_with = "deserialize_hashset")] @@ -872,13 +813,6 @@ pub struct MerchantIdAuthSettings { pub merchant_id_auth_enabled: bool, } -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(default)] -pub struct InternalMerchantIdProfileIdAuthSettings { - pub enabled: bool, - pub internal_api_key: Secret, -} - #[derive(Debug, Clone, Default, Deserialize)] #[serde(default)] pub struct ProxyStatusMapping { @@ -987,12 +921,6 @@ pub struct NetworkTokenizationSupportedConnectors { pub connector_list: HashSet, } -#[derive(Debug, Deserialize, Clone, Default)] -#[serde(default)] -pub struct InternalServicesConfig { - pub payments_base_url: String, -} - impl Settings { pub fn new() -> ApplicationResult { Self::with_config_path(None) diff --git a/crates/router/src/core/payments/customers.rs b/crates/router/src/core/payments/customers.rs index 08e3195559..3edc2436b4 100644 --- a/crates/router/src/core/payments/customers.rs +++ b/crates/router/src/core/payments/customers.rs @@ -1,6 +1,5 @@ -use common_utils::pii; +pub use hyperswitch_domain_models::customer::update_connector_customer_in_customers; use hyperswitch_interfaces::api::ConnectorSpecifications; -use masking::ExposeOptionInterface; use router_env::{instrument, tracing}; use crate::{ @@ -11,7 +10,7 @@ use crate::{ logger, routes::{metrics, SessionState}, services, - types::{self, api, domain, storage}, + types::{self, api, domain}, }; #[instrument(skip_all)] @@ -130,57 +129,3 @@ pub fn should_call_connector_create_customer<'a>( } } } - -#[cfg(feature = "v1")] -#[instrument] -pub async fn update_connector_customer_in_customers( - connector_label: &str, - customer: Option<&domain::Customer>, - connector_customer_id: Option, -) -> Option { - let mut connector_customer_map = customer - .and_then(|customer| customer.connector_customer.clone().expose_option()) - .and_then(|connector_customer| connector_customer.as_object().cloned()) - .unwrap_or_default(); - - let updated_connector_customer_map = connector_customer_id.map(|connector_customer_id| { - let connector_customer_value = serde_json::Value::String(connector_customer_id); - connector_customer_map.insert(connector_label.to_string(), connector_customer_value); - connector_customer_map - }); - - updated_connector_customer_map - .map(serde_json::Value::Object) - .map( - |connector_customer_value| storage::CustomerUpdate::ConnectorCustomer { - connector_customer: Some(pii::SecretSerdeValue::new(connector_customer_value)), - }, - ) -} - -#[cfg(feature = "v2")] -#[instrument] -pub async fn update_connector_customer_in_customers( - merchant_connector_account: &domain::MerchantConnectorAccountTypeDetails, - customer: Option<&domain::Customer>, - connector_customer_id: Option, -) -> Option { - match merchant_connector_account { - domain::MerchantConnectorAccountTypeDetails::MerchantConnectorAccount(account) => { - connector_customer_id.map(|new_conn_cust_id| { - let connector_account_id = account.get_id().clone(); - let mut connector_customer_map = customer - .and_then(|customer| customer.connector_customer.clone()) - .unwrap_or_default(); - connector_customer_map.insert(connector_account_id, new_conn_cust_id); - storage::CustomerUpdate::ConnectorCustomer { - connector_customer: Some(connector_customer_map), - } - }) - } - // TODO: Construct connector_customer for MerchantConnectorDetails if required by connector. - domain::MerchantConnectorAccountTypeDetails::MerchantConnectorDetails(_) => { - todo!("Handle connector_customer construction for MerchantConnectorDetails"); - } - } -} diff --git a/crates/router/src/core/payments/flows/psync_flow.rs b/crates/router/src/core/payments/flows/psync_flow.rs index e54a5fd173..8525fc8557 100644 --- a/crates/router/src/core/payments/flows/psync_flow.rs +++ b/crates/router/src/core/payments/flows/psync_flow.rs @@ -6,6 +6,7 @@ use common_utils::{id_type, ucs_types}; use error_stack::ResultExt; use external_services::grpc_client; use hyperswitch_domain_models::payments as domain_payments; +use hyperswitch_interfaces::unified_connector_service::handle_unified_connector_service_response_for_payment_get; use masking::Secret; use unified_connector_service_client::payments as payments_grpc; @@ -16,8 +17,7 @@ use crate::{ errors::{ApiErrorResponse, ConnectorErrorExt, RouterResult}, payments::{self, access_token, helpers, transformers, PaymentData}, unified_connector_service::{ - build_unified_connector_service_auth_metadata, - handle_unified_connector_service_response_for_payment_get, ucs_logging_wrapper, + build_unified_connector_service_auth_metadata, ucs_logging_wrapper, }, }, routes::SessionState, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 601d9bc5f9..84b8bed45b 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -41,8 +41,9 @@ use hyperswitch_domain_models::{ }, router_data::KlarnaSdkResponse, }; -use hyperswitch_interfaces::{ +pub use hyperswitch_interfaces::{ api::ConnectorSpecifications, + configs::MerchantConnectorAccountType, integrity::{CheckIntegrity, FlowIntegrity, GetIntegrityObject}, }; use josekit::jwe; @@ -4247,98 +4248,6 @@ pub async fn insert_merchant_connector_creds_to_config( } } -#[derive(Clone)] -pub enum MerchantConnectorAccountType { - DbVal(Box), - CacheVal(api_models::admin::MerchantConnectorDetails), -} - -impl MerchantConnectorAccountType { - pub fn get_metadata(&self) -> Option> { - match self { - Self::DbVal(val) => val.metadata.to_owned(), - Self::CacheVal(val) => val.metadata.to_owned(), - } - } - - pub fn get_connector_account_details(&self) -> serde_json::Value { - match self { - Self::DbVal(val) => val.connector_account_details.peek().to_owned(), - Self::CacheVal(val) => val.connector_account_details.peek().to_owned(), - } - } - - pub fn get_connector_wallets_details(&self) -> Option> { - match self { - Self::DbVal(val) => val.connector_wallets_details.as_deref().cloned(), - Self::CacheVal(_) => None, - } - } - - pub fn is_disabled(&self) -> bool { - match self { - Self::DbVal(ref inner) => inner.disabled.unwrap_or(false), - // Cached merchant connector account, only contains the account details, - // the merchant connector account must only be cached if it's not disabled - Self::CacheVal(_) => false, - } - } - - #[cfg(feature = "v1")] - pub fn is_test_mode_on(&self) -> Option { - match self { - Self::DbVal(val) => val.test_mode, - Self::CacheVal(_) => None, - } - } - - #[cfg(feature = "v2")] - pub fn is_test_mode_on(&self) -> Option { - None - } - - pub fn get_mca_id(&self) -> Option { - match self { - Self::DbVal(db_val) => Some(db_val.get_id()), - Self::CacheVal(_) => None, - } - } - - #[cfg(feature = "v1")] - pub fn get_connector_name(&self) -> Option { - match self { - Self::DbVal(db_val) => Some(db_val.connector_name.to_string()), - Self::CacheVal(_) => None, - } - } - - #[cfg(feature = "v2")] - pub fn get_connector_name(&self) -> Option { - match self { - Self::DbVal(db_val) => Some(db_val.connector_name), - Self::CacheVal(_) => None, - } - } - - pub fn get_additional_merchant_data( - &self, - ) -> Option>> { - match self { - Self::DbVal(db_val) => db_val.additional_merchant_data.clone(), - Self::CacheVal(_) => None, - } - } - - pub fn get_webhook_details( - &self, - ) -> CustomResult>, errors::ApiErrorResponse> { - match self { - Self::DbVal(db_val) => Ok(db_val.connector_webhook_details.as_ref()), - Self::CacheVal(_) => Ok(None), - } - } -} - /// Query for merchant connector account either by business label or profile id /// If profile_id is passed use it, or use connector_label to query merchant connector account #[instrument(skip_all)] diff --git a/crates/router/src/core/unified_connector_service.rs b/crates/router/src/core/unified_connector_service.rs index d2848d34e4..255256b89b 100644 --- a/crates/router/src/core/unified_connector_service.rs +++ b/crates/router/src/core/unified_connector_service.rs @@ -756,17 +756,6 @@ pub fn handle_unified_connector_service_response_for_payment_authorize( Ok((router_data_response, status_code)) } -pub fn handle_unified_connector_service_response_for_payment_get( - response: payments_grpc::PaymentServiceGetResponse, -) -> UnifiedConnectorServiceResult { - let status_code = transformers::convert_connector_service_status_code(response.status_code)?; - - let router_data_response = - Result::<(PaymentsResponseData, AttemptStatus), ErrorResponse>::foreign_try_from(response)?; - - Ok((router_data_response, status_code)) -} - pub fn handle_unified_connector_service_response_for_payment_register( response: payments_grpc::PaymentServiceRegisterResponse, ) -> UnifiedConnectorServiceResult { diff --git a/crates/router/src/core/unified_connector_service/transformers.rs b/crates/router/src/core/unified_connector_service/transformers.rs index 357771fbb2..28de14d090 100644 --- a/crates/router/src/core/unified_connector_service/transformers.rs +++ b/crates/router/src/core/unified_connector_service/transformers.rs @@ -17,6 +17,12 @@ use hyperswitch_domain_models::{ }, router_response_types::{PaymentsResponseData, RedirectForm}, }; +pub use hyperswitch_interfaces::{ + helpers::ForeignTryFrom, + unified_connector_service::{ + transformers::convert_connector_service_status_code, WebhookTransformData, + }, +}; use masking::{ExposeInterface, PeekInterface}; use router_env::tracing; use unified_connector_service_client::payments::{ @@ -27,9 +33,9 @@ use url::Url; use crate::{ core::{errors, unified_connector_service}, - types::transformers::ForeignTryFrom, + types::transformers, }; -impl ForeignTryFrom<&RouterData> +impl transformers::ForeignTryFrom<&RouterData> for payments_grpc::PaymentServiceGetRequest { type Error = error_stack::Report; @@ -81,8 +87,10 @@ impl ForeignTryFrom<&RouterData> } } -impl ForeignTryFrom<&RouterData> - for payments_grpc::PaymentServiceAuthorizeRequest +impl + transformers::ForeignTryFrom< + &RouterData, + > for payments_grpc::PaymentServiceAuthorizeRequest { type Error = error_stack::Report; @@ -200,7 +208,7 @@ impl ForeignTryFrom<&RouterData, > for payments_grpc::PaymentServiceAuthorizeRequest { @@ -329,8 +337,10 @@ impl } } -impl ForeignTryFrom<&RouterData> - for payments_grpc::PaymentServiceRegisterRequest +impl + transformers::ForeignTryFrom< + &RouterData, + > for payments_grpc::PaymentServiceRegisterRequest { type Error = error_stack::Report; @@ -433,8 +443,10 @@ impl ForeignTryFrom<&RouterData> - for payments_grpc::PaymentServiceRepeatEverythingRequest +impl + transformers::ForeignTryFrom< + &RouterData, + > for payments_grpc::PaymentServiceRepeatEverythingRequest { type Error = error_stack::Report; @@ -513,7 +525,7 @@ impl ForeignTryFrom<&RouterData +impl transformers::ForeignTryFrom for Result<(PaymentsResponseData, AttemptStatus), ErrorResponse> { type Error = error_stack::Report; @@ -609,84 +621,7 @@ impl ForeignTryFrom } } -impl ForeignTryFrom - for Result<(PaymentsResponseData, AttemptStatus), ErrorResponse> -{ - type Error = error_stack::Report; - - fn foreign_try_from( - response: payments_grpc::PaymentServiceGetResponse, - ) -> Result { - let connector_response_reference_id = - response.response_ref_id.as_ref().and_then(|identifier| { - identifier - .id_type - .clone() - .and_then(|id_type| match id_type { - payments_grpc::identifier::IdType::Id(id) => Some(id), - payments_grpc::identifier::IdType::EncodedData(encoded_data) => { - Some(encoded_data) - } - payments_grpc::identifier::IdType::NoResponseIdMarker(_) => None, - }) - }); - - let status_code = convert_connector_service_status_code(response.status_code)?; - - let resource_id: hyperswitch_domain_models::router_request_types::ResponseId = match response.transaction_id.as_ref().and_then(|id| id.id_type.clone()) { - Some(payments_grpc::identifier::IdType::Id(id)) => hyperswitch_domain_models::router_request_types::ResponseId::ConnectorTransactionId(id), - Some(payments_grpc::identifier::IdType::EncodedData(encoded_data)) => hyperswitch_domain_models::router_request_types::ResponseId::EncodedData(encoded_data), - Some(payments_grpc::identifier::IdType::NoResponseIdMarker(_)) | None => hyperswitch_domain_models::router_request_types::ResponseId::NoResponseId, - }; - - let response = if response.error_code.is_some() { - let attempt_status = match response.status() { - payments_grpc::PaymentStatus::AttemptStatusUnspecified => None, - _ => Some(AttemptStatus::foreign_try_from(response.status())?), - }; - - Err(ErrorResponse { - code: response.error_code().to_owned(), - message: response.error_message().to_owned(), - reason: Some(response.error_message().to_owned()), - status_code, - attempt_status, - connector_transaction_id: connector_response_reference_id, - network_decline_code: None, - network_advice_code: None, - network_error_message: None, - connector_metadata: None, - }) - } else { - let status = AttemptStatus::foreign_try_from(response.status())?; - - Ok(( - PaymentsResponseData::TransactionResponse { - resource_id, - redirection_data: Box::new(None), - mandate_reference: Box::new(response.mandate_reference.map(|grpc_mandate| { - hyperswitch_domain_models::router_response_types::MandateReference { - connector_mandate_id: grpc_mandate.mandate_id, - payment_method_id: None, - mandate_metadata: None, - connector_mandate_request_reference_id: None, - } - })), - connector_metadata: None, - network_txn_id: response.network_txn_id.clone(), - connector_response_reference_id, - incremental_authorization_allowed: None, - charges: None, - }, - status, - )) - }; - - Ok(response) - } -} - -impl ForeignTryFrom +impl transformers::ForeignTryFrom for Result<(PaymentsResponseData, AttemptStatus), ErrorResponse> { type Error = error_stack::Report; @@ -774,7 +709,7 @@ impl ForeignTryFrom } } -impl ForeignTryFrom +impl transformers::ForeignTryFrom for Result<(PaymentsResponseData, AttemptStatus), ErrorResponse> { type Error = error_stack::Report; @@ -845,7 +780,7 @@ impl ForeignTryFrom } } -impl ForeignTryFrom for payments_grpc::Currency { +impl transformers::ForeignTryFrom for payments_grpc::Currency { type Error = error_stack::Report; fn foreign_try_from(currency: common_enums::Currency) -> Result { @@ -858,7 +793,7 @@ impl ForeignTryFrom for payments_grpc::Currency { } } -impl ForeignTryFrom for payments_grpc::CardNetwork { +impl transformers::ForeignTryFrom for payments_grpc::CardNetwork { type Error = error_stack::Report; fn foreign_try_from(card_network: common_enums::CardNetwork) -> Result { @@ -883,7 +818,7 @@ impl ForeignTryFrom for payments_grpc::CardNetwork { } } -impl ForeignTryFrom +impl transformers::ForeignTryFrom for payments_grpc::PaymentAddress { type Error = error_stack::Report; @@ -1005,7 +940,7 @@ impl ForeignTryFrom } } -impl ForeignTryFrom for payments_grpc::AuthenticationType { +impl transformers::ForeignTryFrom for payments_grpc::AuthenticationType { type Error = error_stack::Report; fn foreign_try_from(auth_type: AuthenticationType) -> Result { @@ -1016,8 +951,10 @@ impl ForeignTryFrom for payments_grpc::AuthenticationType { } } -impl ForeignTryFrom - for payments_grpc::BrowserInformation +impl + transformers::ForeignTryFrom< + hyperswitch_domain_models::router_request_types::BrowserInformation, + > for payments_grpc::BrowserInformation { type Error = error_stack::Report; @@ -1044,7 +981,7 @@ impl ForeignTryFrom for payments_grpc::CaptureMethod { +impl transformers::ForeignTryFrom for payments_grpc::CaptureMethod { type Error = error_stack::Report; fn foreign_try_from(capture_method: storage_enums::CaptureMethod) -> Result { @@ -1058,7 +995,7 @@ impl ForeignTryFrom for payments_grpc::CaptureMeth } } -impl ForeignTryFrom for payments_grpc::AuthenticationData { +impl transformers::ForeignTryFrom for payments_grpc::AuthenticationData { type Error = error_stack::Report; fn foreign_try_from(authentication_data: AuthenticationData) -> Result { @@ -1076,47 +1013,7 @@ impl ForeignTryFrom for payments_grpc::AuthenticationData { } } -impl ForeignTryFrom for AttemptStatus { - type Error = error_stack::Report; - - fn foreign_try_from(grpc_status: payments_grpc::PaymentStatus) -> Result { - match grpc_status { - payments_grpc::PaymentStatus::Started => Ok(Self::Started), - payments_grpc::PaymentStatus::AuthenticationFailed => Ok(Self::AuthenticationFailed), - payments_grpc::PaymentStatus::RouterDeclined => Ok(Self::RouterDeclined), - payments_grpc::PaymentStatus::AuthenticationPending => Ok(Self::AuthenticationPending), - payments_grpc::PaymentStatus::AuthenticationSuccessful => { - Ok(Self::AuthenticationSuccessful) - } - payments_grpc::PaymentStatus::Authorized => Ok(Self::Authorized), - payments_grpc::PaymentStatus::AuthorizationFailed => Ok(Self::AuthorizationFailed), - payments_grpc::PaymentStatus::Charged => Ok(Self::Charged), - payments_grpc::PaymentStatus::Authorizing => Ok(Self::Authorizing), - payments_grpc::PaymentStatus::CodInitiated => Ok(Self::CodInitiated), - payments_grpc::PaymentStatus::Voided => Ok(Self::Voided), - payments_grpc::PaymentStatus::VoidInitiated => Ok(Self::VoidInitiated), - payments_grpc::PaymentStatus::CaptureInitiated => Ok(Self::CaptureInitiated), - payments_grpc::PaymentStatus::CaptureFailed => Ok(Self::CaptureFailed), - payments_grpc::PaymentStatus::VoidFailed => Ok(Self::VoidFailed), - payments_grpc::PaymentStatus::AutoRefunded => Ok(Self::AutoRefunded), - payments_grpc::PaymentStatus::PartialCharged => Ok(Self::PartialCharged), - payments_grpc::PaymentStatus::PartialChargedAndChargeable => { - Ok(Self::PartialChargedAndChargeable) - } - payments_grpc::PaymentStatus::Unresolved => Ok(Self::Unresolved), - payments_grpc::PaymentStatus::Pending => Ok(Self::Pending), - payments_grpc::PaymentStatus::Failure => Ok(Self::Failure), - payments_grpc::PaymentStatus::PaymentMethodAwaited => Ok(Self::PaymentMethodAwaited), - payments_grpc::PaymentStatus::ConfirmationAwaited => Ok(Self::ConfirmationAwaited), - payments_grpc::PaymentStatus::DeviceDataCollectionPending => { - Ok(Self::DeviceDataCollectionPending) - } - payments_grpc::PaymentStatus::AttemptStatusUnspecified => Ok(Self::Unresolved), - } - } -} - -impl ForeignTryFrom for RedirectForm { +impl transformers::ForeignTryFrom for RedirectForm { type Error = error_stack::Report; fn foreign_try_from(value: payments_grpc::RedirectForm) -> Result { @@ -1145,7 +1042,7 @@ impl ForeignTryFrom for RedirectForm { } } -impl ForeignTryFrom for Method { +impl transformers::ForeignTryFrom for Method { type Error = error_stack::Report; fn foreign_try_from(value: payments_grpc::HttpMethod) -> Result { @@ -1163,7 +1060,7 @@ impl ForeignTryFrom for Method { } } -impl ForeignTryFrom for payments_grpc::FutureUsage { +impl transformers::ForeignTryFrom for payments_grpc::FutureUsage { type Error = error_stack::Report; fn foreign_try_from(future_usage: storage_enums::FutureUsage) -> Result { @@ -1174,7 +1071,7 @@ impl ForeignTryFrom for payments_grpc::FutureUsage { } } -impl ForeignTryFrom +impl transformers::ForeignTryFrom for payments_grpc::CustomerAcceptance { type Error = error_stack::Report; @@ -1208,8 +1105,10 @@ impl ForeignTryFrom } } -impl ForeignTryFrom<&hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails<'_>> - for payments_grpc::RequestDetails +impl + transformers::ForeignTryFrom< + &hyperswitch_interfaces::webhooks::IncomingWebhookRequestDetails<'_>, + > for payments_grpc::RequestDetails { type Error = error_stack::Report; @@ -1252,15 +1151,6 @@ impl ForeignTryFrom<&hyperswitch_interfaces::webhooks::IncomingWebhookRequestDet } } -/// Webhook transform data structure containing UCS response information -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct WebhookTransformData { - pub event_type: api_models::webhooks::IncomingWebhookEvent, - pub source_verified: bool, - pub webhook_content: Option, - pub response_ref_id: Option, -} - /// Transform UCS webhook response into webhook event data pub fn transform_ucs_webhook_response( response: PaymentServiceTransformResponse, @@ -1290,7 +1180,10 @@ pub fn build_webhook_transform_request( merchant_id: &str, connector_id: &str, ) -> Result> { - let request_details_grpc = payments_grpc::RequestDetails::foreign_try_from(request_details) + let request_details_grpc = + >::foreign_try_from( + request_details, + ) .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to transform webhook request details to gRPC format")?; @@ -1308,14 +1201,3 @@ pub fn build_webhook_transform_request( access_token: None, }) } - -pub fn convert_connector_service_status_code( - status_code: u32, -) -> Result> { - u16::try_from(status_code).map_err(|err| { - UnifiedConnectorServiceError::RequestEncodingFailedWithReason(format!( - "Failed to convert connector service status code to u16: {err}" - )) - .into() - }) -} diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 4aef4efd3e..bc307de04f 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -97,7 +97,7 @@ pub trait StorageInterface: + address::AddressInterface + api_keys::ApiKeyInterface + blocklist_lookup::BlocklistLookupInterface - + configs::ConfigInterface + + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface + dashboard_metadata::DashboardMetadataInterface @@ -109,9 +109,9 @@ pub trait StorageInterface: + FraudCheckInterface + locker_mock_up::LockerMockUpInterface + mandate::MandateInterface - + merchant_account::MerchantAccountInterface + + merchant_account::MerchantAccountInterface + merchant_connector_account::ConnectorAccessToken - + merchant_connector_account::MerchantConnectorAccountInterface + + merchant_connector_account::MerchantConnectorAccountInterface + PaymentAttemptInterface + PaymentIntentInterface + PaymentMethodInterface @@ -124,12 +124,12 @@ pub trait StorageInterface: + refund::RefundInterface + reverse_lookup::ReverseLookupInterface + CardsInfoInterface - + merchant_key_store::MerchantKeyStoreInterface + + merchant_key_store::MerchantKeyStoreInterface + MasterKeyInterface + payment_link::PaymentLinkInterface + RedisConnInterface + RequestIdStore - + business_profile::ProfileInterface + + business_profile::ProfileInterface + routing_algorithm::RoutingAlgorithmInterface + gsm::GsmInterface + unified_translations::UnifiedTranslationsInterface @@ -175,10 +175,10 @@ pub trait AccountsStorageInterface: + Sync + dyn_clone::DynClone + OrganizationInterface - + merchant_account::MerchantAccountInterface - + business_profile::ProfileInterface - + merchant_connector_account::MerchantConnectorAccountInterface - + merchant_key_store::MerchantKeyStoreInterface + + merchant_account::MerchantAccountInterface + + business_profile::ProfileInterface + + merchant_connector_account::MerchantConnectorAccountInterface + + merchant_key_store::MerchantKeyStoreInterface + dashboard_metadata::DashboardMetadataInterface + 'static { diff --git a/crates/router/src/db/business_profile.rs b/crates/router/src/db/business_profile.rs index 1a087c2903..ecd8b75610 100644 --- a/crates/router/src/db/business_profile.rs +++ b/crates/router/src/db/business_profile.rs @@ -1,453 +1,4 @@ -use common_utils::{ext_traits::AsyncExt, types::keymanager::KeyManagerState}; -use error_stack::{report, ResultExt}; -use router_env::{instrument, tracing}; - -use super::Store; -use crate::{ - connection, - core::errors::{self, CustomResult}, - db::MockDb, - types::{ - domain::{ - self, - behaviour::{Conversion, ReverseConversion}, - }, - storage, - }, +pub use hyperswitch_domain_models::{ + business_profile::{self, ProfileInterface}, + errors::api_error_response, }; - -#[async_trait::async_trait] -pub trait ProfileInterface -where - domain::Profile: Conversion, -{ - async fn insert_business_profile( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - business_profile: domain::Profile, - ) -> CustomResult; - - async fn find_business_profile_by_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - profile_id: &common_utils::id_type::ProfileId, - ) -> CustomResult; - - async fn find_business_profile_by_merchant_id_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - merchant_id: &common_utils::id_type::MerchantId, - profile_id: &common_utils::id_type::ProfileId, - ) -> CustomResult; - - async fn find_business_profile_by_profile_name_merchant_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - profile_name: &str, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult; - - async fn update_profile_by_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - current_state: domain::Profile, - profile_update: domain::ProfileUpdate, - ) -> CustomResult; - - async fn delete_profile_by_profile_id_merchant_id( - &self, - profile_id: &common_utils::id_type::ProfileId, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult; - - async fn list_profile_by_merchant_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult, errors::StorageError>; -} - -#[async_trait::async_trait] -impl ProfileInterface for Store { - #[instrument(skip_all)] - async fn insert_business_profile( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - business_profile: domain::Profile, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - business_profile - .construct_new() - .await - .change_context(errors::StorageError::EncryptionError)? - .insert(&conn) - .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) - } - - #[instrument(skip_all)] - async fn find_business_profile_by_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - profile_id: &common_utils::id_type::ProfileId, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::Profile::find_by_profile_id(&conn, profile_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 fn find_business_profile_by_merchant_id_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - merchant_id: &common_utils::id_type::MerchantId, - profile_id: &common_utils::id_type::ProfileId, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::Profile::find_by_merchant_id_profile_id(&conn, merchant_id, profile_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) - } - - #[instrument(skip_all)] - async fn find_business_profile_by_profile_name_merchant_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - profile_name: &str, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::Profile::find_by_profile_name_merchant_id(&conn, profile_name, merchant_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) - } - - #[instrument(skip_all)] - async fn update_profile_by_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - current_state: domain::Profile, - profile_update: domain::ProfileUpdate, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - Conversion::convert(current_state) - .await - .change_context(errors::StorageError::EncryptionError)? - .update_by_profile_id(&conn, storage::ProfileUpdateInternal::from(profile_update)) - .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) - } - - #[instrument(skip_all)] - async fn delete_profile_by_profile_id_merchant_id( - &self, - profile_id: &common_utils::id_type::ProfileId, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - storage::Profile::delete_by_profile_id_merchant_id(&conn, profile_id, merchant_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - - #[instrument(skip_all)] - async fn list_profile_by_merchant_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::Profile::list_profile_by_merchant_id(&conn, merchant_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|business_profiles| async { - let mut domain_business_profiles = Vec::with_capacity(business_profiles.len()); - for business_profile in business_profiles.into_iter() { - domain_business_profiles.push( - business_profile - .convert( - key_manager_state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ); - } - Ok(domain_business_profiles) - }) - .await - } -} - -#[async_trait::async_trait] -impl ProfileInterface for MockDb { - async fn insert_business_profile( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - business_profile: domain::Profile, - ) -> CustomResult { - let stored_business_profile = Conversion::convert(business_profile) - .await - .change_context(errors::StorageError::EncryptionError)?; - - self.business_profiles - .lock() - .await - .push(stored_business_profile.clone()); - - stored_business_profile - .convert( - key_manager_state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - async fn find_business_profile_by_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - profile_id: &common_utils::id_type::ProfileId, - ) -> CustomResult { - self.business_profiles - .lock() - .await - .iter() - .find(|business_profile| business_profile.get_id() == profile_id) - .cloned() - .async_map(|business_profile| async { - business_profile - .convert( - key_manager_state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - .transpose()? - .ok_or( - errors::StorageError::ValueNotFound(format!( - "No business profile found for profile_id = {profile_id:?}" - )) - .into(), - ) - } - - async fn find_business_profile_by_merchant_id_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - merchant_id: &common_utils::id_type::MerchantId, - profile_id: &common_utils::id_type::ProfileId, - ) -> CustomResult { - self.business_profiles - .lock() - .await - .iter() - .find(|business_profile| { - business_profile.merchant_id == *merchant_id - && business_profile.get_id() == profile_id - }) - .cloned() - .async_map(|business_profile| async { - business_profile - .convert( - key_manager_state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - .transpose()? - .ok_or( - errors::StorageError::ValueNotFound(format!( - "No business profile found for merchant_id = {merchant_id:?} and profile_id = {profile_id:?}" - )) - .into(), - ) - } - - async fn update_profile_by_profile_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - current_state: domain::Profile, - profile_update: domain::ProfileUpdate, - ) -> CustomResult { - let profile_id = current_state.get_id().to_owned(); - self.business_profiles - .lock() - .await - .iter_mut() - .find(|business_profile| business_profile.get_id() == current_state.get_id()) - .async_map(|business_profile| async { - let profile_updated = storage::ProfileUpdateInternal::from(profile_update) - .apply_changeset( - Conversion::convert(current_state) - .await - .change_context(errors::StorageError::EncryptionError)?, - ); - *business_profile = profile_updated.clone(); - - profile_updated - .convert( - key_manager_state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - .transpose()? - .ok_or( - errors::StorageError::ValueNotFound(format!( - "No business profile found for profile_id = {profile_id:?}", - )) - .into(), - ) - } - - async fn delete_profile_by_profile_id_merchant_id( - &self, - profile_id: &common_utils::id_type::ProfileId, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - let mut business_profiles = self.business_profiles.lock().await; - let index = business_profiles - .iter() - .position(|business_profile| { - business_profile.get_id() == profile_id - && business_profile.merchant_id == *merchant_id - }) - .ok_or::(errors::StorageError::ValueNotFound(format!( - "No business profile found for profile_id = {profile_id:?} and merchant_id = {merchant_id:?}" - )))?; - business_profiles.remove(index); - Ok(true) - } - - async fn list_profile_by_merchant_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult, errors::StorageError> { - let business_profiles = self - .business_profiles - .lock() - .await - .iter() - .filter(|business_profile| business_profile.merchant_id == *merchant_id) - .cloned() - .collect::>(); - - let mut domain_business_profiles = Vec::with_capacity(business_profiles.len()); - - for business_profile in business_profiles { - let domain_profile = business_profile - .convert( - key_manager_state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?; - domain_business_profiles.push(domain_profile); - } - - Ok(domain_business_profiles) - } - - async fn find_business_profile_by_profile_name_merchant_id( - &self, - key_manager_state: &KeyManagerState, - merchant_key_store: &domain::MerchantKeyStore, - profile_name: &str, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - self.business_profiles - .lock() - .await - .iter() - .find(|business_profile| { - business_profile.profile_name == profile_name - && business_profile.merchant_id == *merchant_id - }) - .cloned() - .async_map(|business_profile| async { - business_profile - .convert( - key_manager_state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - .transpose()? - .ok_or( - errors::StorageError::ValueNotFound(format!( - "No business profile found for profile_name = {profile_name} and merchant_id = {merchant_id:?}" - - )) - .into(), - ) - } -} diff --git a/crates/router/src/db/configs.rs b/crates/router/src/db/configs.rs index 2ffea09939..dadb55f2cf 100644 --- a/crates/router/src/db/configs.rs +++ b/crates/router/src/db/configs.rs @@ -1,277 +1,4 @@ -use diesel_models::configs::ConfigUpdateInternal; -use error_stack::report; -use router_env::{instrument, tracing}; -use storage_impl::redis::cache::{self, CacheKind, CONFIG_CACHE}; - -use super::{MockDb, Store}; -use crate::{ - connection, - core::errors::{self, CustomResult}, - db::StorageInterface, - types::storage, +pub use hyperswitch_domain_models::{ + configs::{self, ConfigInterface}, + errors::api_error_response, }; - -#[async_trait::async_trait] -pub trait ConfigInterface { - async fn insert_config( - &self, - config: storage::ConfigNew, - ) -> CustomResult; - - async fn find_config_by_key( - &self, - key: &str, - ) -> CustomResult; - - async fn find_config_by_key_unwrap_or( - &self, - key: &str, - // If the config is not found it will be created with the default value. - default_config: Option, - ) -> CustomResult; - - async fn find_config_by_key_from_db( - &self, - key: &str, - ) -> CustomResult; - - async fn update_config_by_key( - &self, - key: &str, - config_update: storage::ConfigUpdate, - ) -> CustomResult; - - async fn update_config_in_database( - &self, - key: &str, - config_update: storage::ConfigUpdate, - ) -> CustomResult; - - async fn delete_config_by_key( - &self, - key: &str, - ) -> CustomResult; -} - -#[async_trait::async_trait] -impl ConfigInterface for Store { - #[instrument(skip_all)] - async fn insert_config( - &self, - config: storage::ConfigNew, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - let inserted = config - .insert(&conn) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - cache::redact_from_redis_and_publish( - self.get_cache_store().as_ref(), - [CacheKind::Config((&inserted.key).into())], - ) - .await?; - - Ok(inserted) - } - - #[instrument(skip_all)] - async fn update_config_in_database( - &self, - key: &str, - config_update: storage::ConfigUpdate, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - storage::Config::update_by_key(&conn, key, config_update) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - - //update in DB and remove in redis and cache - #[instrument(skip_all)] - async fn update_config_by_key( - &self, - key: &str, - config_update: storage::ConfigUpdate, - ) -> CustomResult { - cache::publish_and_redact(self, CacheKind::Config(key.into()), || { - self.update_config_in_database(key, config_update) - }) - .await - } - - #[instrument(skip_all)] - async fn find_config_by_key_from_db( - &self, - key: &str, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - storage::Config::find_by_key(&conn, key) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - } - - //check in cache, then redis then finally DB, and on the way back populate redis and cache - #[instrument(skip_all)] - async fn find_config_by_key( - &self, - key: &str, - ) -> CustomResult { - let find_config_by_key_from_db = || async { - let conn = connection::pg_connection_write(self).await?; - storage::Config::find_by_key(&conn, key) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - cache::get_or_populate_in_memory(self, key, find_config_by_key_from_db, &CONFIG_CACHE).await - } - - #[instrument(skip_all)] - async fn find_config_by_key_unwrap_or( - &self, - key: &str, - // If the config is not found it will be cached with the default value. - default_config: Option, - ) -> CustomResult { - let find_else_unwrap_or = || async { - let conn = connection::pg_connection_write(self).await?; - match storage::Config::find_by_key(&conn, key) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - { - Ok(a) => Ok(a), - Err(err) => { - if err.current_context().is_db_not_found() { - default_config - .map(|c| { - storage::ConfigNew { - key: key.to_string(), - config: c, - } - .into() - }) - .ok_or(err) - } else { - Err(err) - } - } - } - }; - - cache::get_or_populate_in_memory(self, key, find_else_unwrap_or, &CONFIG_CACHE).await - } - - #[instrument(skip_all)] - async fn delete_config_by_key( - &self, - key: &str, - ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; - let deleted = storage::Config::delete_by_key(&conn, key) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - cache::redact_from_redis_and_publish( - self.get_cache_store().as_ref(), - [CacheKind::Config((&deleted.key).into())], - ) - .await?; - - Ok(deleted) - } -} - -#[async_trait::async_trait] -impl ConfigInterface for MockDb { - #[instrument(skip_all)] - async fn insert_config( - &self, - config: storage::ConfigNew, - ) -> CustomResult { - let mut configs = self.configs.lock().await; - - let config_new = storage::Config { - key: config.key, - config: config.config, - }; - configs.push(config_new.clone()); - Ok(config_new) - } - - async fn update_config_in_database( - &self, - key: &str, - config_update: storage::ConfigUpdate, - ) -> CustomResult { - self.update_config_by_key(key, config_update).await - } - - async fn update_config_by_key( - &self, - key: &str, - config_update: storage::ConfigUpdate, - ) -> CustomResult { - let result = self - .configs - .lock() - .await - .iter_mut() - .find(|c| c.key == key) - .ok_or_else(|| { - errors::StorageError::ValueNotFound("cannot find config to update".to_string()) - .into() - }) - .map(|c| { - let config_updated = - ConfigUpdateInternal::from(config_update).create_config(c.clone()); - *c = config_updated.clone(); - config_updated - }); - - result - } - - async fn delete_config_by_key( - &self, - key: &str, - ) -> CustomResult { - let mut configs = self.configs.lock().await; - let result = configs - .iter() - .position(|c| c.key == key) - .map(|index| configs.remove(index)) - .ok_or_else(|| { - errors::StorageError::ValueNotFound("cannot find config to delete".to_string()) - .into() - }); - - result - } - - async fn find_config_by_key( - &self, - key: &str, - ) -> CustomResult { - let configs = self.configs.lock().await; - let config = configs.iter().find(|c| c.key == key).cloned(); - - config.ok_or_else(|| { - errors::StorageError::ValueNotFound("cannot find config".to_string()).into() - }) - } - - async fn find_config_by_key_unwrap_or( - &self, - key: &str, - _default_config: Option, - ) -> CustomResult { - self.find_config_by_key(key).await - } - - async fn find_config_by_key_from_db( - &self, - key: &str, - ) -> CustomResult { - self.find_config_by_key(key).await - } -} diff --git a/crates/router/src/db/events.rs b/crates/router/src/db/events.rs index c80e9a24f9..10c1b8ddb2 100644 --- a/crates/router/src/db/events.rs +++ b/crates/router/src/db/events.rs @@ -862,10 +862,7 @@ mod tests { use crate::{ core::webhooks as webhooks_core, - db::{ - events::EventInterface, merchant_key_store::MerchantKeyStoreInterface, - MasterKeyInterface, MockDb, - }, + db::{events::EventInterface, merchant_key_store::MerchantKeyStoreInterface, MockDb}, routes::{ self, app::{settings::Settings, StorageImpl}, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 08e7b18678..22c3d1022a 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -5,7 +5,7 @@ use common_enums::enums::MerchantStorageScheme; use common_utils::{ errors::CustomResult, id_type, - types::{keymanager::KeyManagerState, user::ThemeLineage}, + types::{keymanager::KeyManagerState, user::ThemeLineage, TenantConfig}, }; #[cfg(feature = "v2")] use diesel_models::ephemeral_key::{ClientSecretType, ClientSecretTypeNew}; @@ -42,7 +42,7 @@ use scheduler::{ SchedulerInterface, }; use serde::Serialize; -use storage_impl::{config::TenantConfig, redis::kv_store::RedisConnInterface}; +use storage_impl::redis::kv_store::RedisConnInterface; use time::PrimitiveDateTime; use super::{ @@ -318,6 +318,7 @@ impl CardsInfoInterface for KafkaStore { #[async_trait::async_trait] impl ConfigInterface for KafkaStore { + type Error = errors::StorageError; async fn insert_config( &self, config: storage::ConfigNew, @@ -1060,6 +1061,7 @@ impl PaymentLinkInterface for KafkaStore { #[async_trait::async_trait] impl MerchantAccountInterface for KafkaStore { + type Error = errors::StorageError; async fn insert_merchant( &self, state: &KeyManagerState, @@ -1237,6 +1239,7 @@ impl FileMetadataInterface for KafkaStore { #[async_trait::async_trait] impl MerchantConnectorAccountInterface for KafkaStore { + type Error = errors::StorageError; async fn update_multiple_merchant_connector_accounts( &self, merchant_connector_accounts: Vec<( @@ -2948,6 +2951,7 @@ impl RefundInterface for KafkaStore { #[async_trait::async_trait] impl MerchantKeyStoreInterface for KafkaStore { + type Error = errors::StorageError; async fn insert_merchant_key_store( &self, state: &KeyManagerState, @@ -3005,6 +3009,7 @@ impl MerchantKeyStoreInterface for KafkaStore { #[async_trait::async_trait] impl ProfileInterface for KafkaStore { + type Error = errors::StorageError; async fn insert_business_profile( &self, key_manager_state: &KeyManagerState, diff --git a/crates/router/src/db/merchant_account.rs b/crates/router/src/db/merchant_account.rs index 47851ae263..0611d063d3 100644 --- a/crates/router/src/db/merchant_account.rs +++ b/crates/router/src/db/merchant_account.rs @@ -1,823 +1 @@ -#[cfg(feature = "olap")] -use std::collections::HashMap; - -use common_utils::{ext_traits::AsyncExt, types::keymanager::KeyManagerState}; -use diesel_models::MerchantAccountUpdateInternal; -use error_stack::{report, ResultExt}; -use router_env::{instrument, tracing}; -#[cfg(feature = "accounts_cache")] -use storage_impl::redis::cache::{self, CacheKind, ACCOUNTS_CACHE}; - -use super::{MasterKeyInterface, MockDb, Store}; -use crate::{ - connection, - core::errors::{self, CustomResult}, - db::merchant_key_store::MerchantKeyStoreInterface, - types::{ - domain::{ - self, - behaviour::{Conversion, ReverseConversion}, - }, - storage, - }, -}; - -#[async_trait::async_trait] -pub trait MerchantAccountInterface -where - domain::MerchantAccount: - Conversion, -{ - async fn insert_merchant( - &self, - state: &KeyManagerState, - merchant_account: domain::MerchantAccount, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - async fn find_merchant_account_by_merchant_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - async fn update_all_merchant_account( - &self, - merchant_account: storage::MerchantAccountUpdate, - ) -> CustomResult; - - async fn update_merchant( - &self, - state: &KeyManagerState, - this: domain::MerchantAccount, - merchant_account: storage::MerchantAccountUpdate, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - async fn update_specific_fields_in_merchant( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_account: storage::MerchantAccountUpdate, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - async fn find_merchant_account_by_publishable_key( - &self, - state: &KeyManagerState, - publishable_key: &str, - ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError>; - - #[cfg(feature = "olap")] - async fn list_merchant_accounts_by_organization_id( - &self, - state: &KeyManagerState, - organization_id: &common_utils::id_type::OrganizationId, - ) -> CustomResult, errors::StorageError>; - - async fn delete_merchant_account_by_merchant_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult; - - #[cfg(feature = "olap")] - async fn list_multiple_merchant_accounts( - &self, - state: &KeyManagerState, - merchant_ids: Vec, - ) -> CustomResult, errors::StorageError>; - - #[cfg(feature = "olap")] - async fn list_merchant_and_org_ids( - &self, - state: &KeyManagerState, - limit: u32, - offset: Option, - ) -> CustomResult< - Vec<( - common_utils::id_type::MerchantId, - common_utils::id_type::OrganizationId, - )>, - errors::StorageError, - >; -} - -#[async_trait::async_trait] -impl MerchantAccountInterface for Store { - #[instrument(skip_all)] - async fn insert_merchant( - &self, - state: &KeyManagerState, - merchant_account: domain::MerchantAccount, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - merchant_account - .construct_new() - .await - .change_context(errors::StorageError::EncryptionError)? - .insert(&conn) - .await - .map_err(|error| report!(errors::StorageError::from(error)))? - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[instrument(skip_all)] - async fn find_merchant_account_by_merchant_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let fetch_func = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::MerchantAccount::find_by_merchant_id(&conn, merchant_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(not(feature = "accounts_cache"))] - { - fetch_func() - .await? - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_id.to_owned().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[cfg(feature = "accounts_cache")] - { - cache::get_or_populate_in_memory( - self, - merchant_id.get_string_repr(), - fetch_func, - &ACCOUNTS_CACHE, - ) - .await? - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - } - - #[instrument(skip_all)] - async fn update_merchant( - &self, - state: &KeyManagerState, - this: domain::MerchantAccount, - merchant_account: storage::MerchantAccountUpdate, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - - let updated_merchant_account = Conversion::convert(this) - .await - .change_context(errors::StorageError::EncryptionError)? - .update(&conn, merchant_account.into()) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - #[cfg(feature = "accounts_cache")] - { - publish_and_redact_merchant_account_cache(self, &updated_merchant_account).await?; - } - updated_merchant_account - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[instrument(skip_all)] - async fn update_specific_fields_in_merchant( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_account: storage::MerchantAccountUpdate, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - let updated_merchant_account = storage::MerchantAccount::update_with_specific_fields( - &conn, - merchant_id, - merchant_account.into(), - ) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - #[cfg(feature = "accounts_cache")] - { - publish_and_redact_merchant_account_cache(self, &updated_merchant_account).await?; - } - updated_merchant_account - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[instrument(skip_all)] - async fn find_merchant_account_by_publishable_key( - &self, - state: &KeyManagerState, - publishable_key: &str, - ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> - { - let fetch_by_pub_key_func = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - - storage::MerchantAccount::find_by_publishable_key(&conn, publishable_key) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - let merchant_account; - #[cfg(not(feature = "accounts_cache"))] - { - merchant_account = fetch_by_pub_key_func().await?; - } - - #[cfg(feature = "accounts_cache")] - { - merchant_account = cache::get_or_populate_in_memory( - self, - publishable_key, - fetch_by_pub_key_func, - &ACCOUNTS_CACHE, - ) - .await?; - } - let key_store = self - .get_merchant_key_store_by_merchant_id( - state, - merchant_account.get_id(), - &self.get_master_key().to_vec().into(), - ) - .await?; - let domain_merchant_account = merchant_account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?; - Ok((domain_merchant_account, key_store)) - } - - #[cfg(feature = "olap")] - #[instrument(skip_all)] - async fn list_merchant_accounts_by_organization_id( - &self, - state: &KeyManagerState, - organization_id: &common_utils::id_type::OrganizationId, - ) -> CustomResult, errors::StorageError> { - use futures::future::try_join_all; - let conn = connection::pg_accounts_connection_read(self).await?; - - let encrypted_merchant_accounts = - storage::MerchantAccount::list_by_organization_id(&conn, organization_id) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - let db_master_key = self.get_master_key().to_vec().into(); - - let merchant_key_stores = - try_join_all(encrypted_merchant_accounts.iter().map(|merchant_account| { - self.get_merchant_key_store_by_merchant_id( - state, - merchant_account.get_id(), - &db_master_key, - ) - })) - .await?; - - let merchant_accounts = try_join_all( - encrypted_merchant_accounts - .into_iter() - .zip(merchant_key_stores.iter()) - .map(|(merchant_account, key_store)| async { - merchant_account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }), - ) - .await?; - - Ok(merchant_accounts) - } - - #[instrument(skip_all)] - async fn delete_merchant_account_by_merchant_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - - let is_deleted_func = || async { - storage::MerchantAccount::delete_by_merchant_id(&conn, merchant_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - let is_deleted; - - #[cfg(not(feature = "accounts_cache"))] - { - is_deleted = is_deleted_func().await?; - } - - #[cfg(feature = "accounts_cache")] - { - let merchant_account = - storage::MerchantAccount::find_by_merchant_id(&conn, merchant_id) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - is_deleted = is_deleted_func().await?; - - publish_and_redact_merchant_account_cache(self, &merchant_account).await?; - } - - Ok(is_deleted) - } - - #[cfg(feature = "olap")] - #[instrument(skip_all)] - async fn list_multiple_merchant_accounts( - &self, - state: &KeyManagerState, - merchant_ids: Vec, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_accounts_connection_read(self).await?; - - let encrypted_merchant_accounts = - storage::MerchantAccount::list_multiple_merchant_accounts(&conn, merchant_ids) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - let db_master_key = self.get_master_key().to_vec().into(); - - let merchant_key_stores = self - .list_multiple_key_stores( - state, - encrypted_merchant_accounts - .iter() - .map(|merchant_account| merchant_account.get_id()) - .cloned() - .collect(), - &db_master_key, - ) - .await?; - - let key_stores_by_id: HashMap<_, _> = merchant_key_stores - .iter() - .map(|key_store| (key_store.merchant_id.to_owned(), key_store)) - .collect(); - - let merchant_accounts = - futures::future::try_join_all(encrypted_merchant_accounts.into_iter().map( - |merchant_account| async { - let key_store = key_stores_by_id.get(merchant_account.get_id()).ok_or( - errors::StorageError::ValueNotFound(format!( - "merchant_key_store with merchant_id = {:?}", - merchant_account.get_id() - )), - )?; - merchant_account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }, - )) - .await?; - - Ok(merchant_accounts) - } - - #[cfg(feature = "olap")] - #[instrument(skip_all)] - async fn list_merchant_and_org_ids( - &self, - _state: &KeyManagerState, - limit: u32, - offset: Option, - ) -> CustomResult< - Vec<( - common_utils::id_type::MerchantId, - common_utils::id_type::OrganizationId, - )>, - errors::StorageError, - > { - let conn = connection::pg_accounts_connection_read(self).await?; - let encrypted_merchant_accounts = - storage::MerchantAccount::list_all_merchant_accounts(&conn, limit, offset) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - let merchant_and_org_ids = encrypted_merchant_accounts - .into_iter() - .map(|merchant_account| { - let merchant_id = merchant_account.get_id().clone(); - let org_id = merchant_account.organization_id; - (merchant_id, org_id) - }) - .collect(); - Ok(merchant_and_org_ids) - } - - async fn update_all_merchant_account( - &self, - merchant_account: storage::MerchantAccountUpdate, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_read(self).await?; - - let db_func = || async { - storage::MerchantAccount::update_all_merchant_accounts( - &conn, - MerchantAccountUpdateInternal::from(merchant_account), - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - let total; - #[cfg(not(feature = "accounts_cache"))] - { - let ma = db_func().await?; - total = ma.len(); - } - - #[cfg(feature = "accounts_cache")] - { - let ma = db_func().await?; - publish_and_redact_all_merchant_account_cache(self, &ma).await?; - total = ma.len(); - } - - Ok(total) - } -} - -#[async_trait::async_trait] -impl MerchantAccountInterface for MockDb { - #[allow(clippy::panic)] - async fn insert_merchant( - &self, - state: &KeyManagerState, - merchant_account: domain::MerchantAccount, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let mut accounts = self.merchant_accounts.lock().await; - let account = Conversion::convert(merchant_account) - .await - .change_context(errors::StorageError::EncryptionError)?; - accounts.push(account.clone()); - - account - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[allow(clippy::panic)] - async fn find_merchant_account_by_merchant_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let accounts = self.merchant_accounts.lock().await; - accounts - .iter() - .find(|account| account.get_id() == merchant_id) - .cloned() - .ok_or(errors::StorageError::ValueNotFound(format!( - "Merchant ID: {merchant_id:?} not found", - )))? - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - async fn update_merchant( - &self, - state: &KeyManagerState, - merchant_account: domain::MerchantAccount, - merchant_account_update: storage::MerchantAccountUpdate, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let merchant_id = merchant_account.get_id().to_owned(); - let mut accounts = self.merchant_accounts.lock().await; - accounts - .iter_mut() - .find(|account| account.get_id() == merchant_account.get_id()) - .async_map(|account| async { - let update = MerchantAccountUpdateInternal::from(merchant_account_update) - .apply_changeset( - Conversion::convert(merchant_account) - .await - .change_context(errors::StorageError::EncryptionError)?, - ); - *account = update.clone(); - update - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - .transpose()? - .ok_or( - errors::StorageError::ValueNotFound(format!( - "Merchant ID: {merchant_id:?} not found", - )) - .into(), - ) - } - - async fn update_specific_fields_in_merchant( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_account_update: storage::MerchantAccountUpdate, - merchant_key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let mut accounts = self.merchant_accounts.lock().await; - accounts - .iter_mut() - .find(|account| account.get_id() == merchant_id) - .async_map(|account| async { - let update = MerchantAccountUpdateInternal::from(merchant_account_update) - .apply_changeset(account.clone()); - *account = update.clone(); - update - .convert( - state, - merchant_key_store.key.get_inner(), - merchant_key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - .transpose()? - .ok_or( - errors::StorageError::ValueNotFound(format!( - "Merchant ID: {merchant_id:?} not found", - )) - .into(), - ) - } - - async fn find_merchant_account_by_publishable_key( - &self, - state: &KeyManagerState, - publishable_key: &str, - ) -> CustomResult<(domain::MerchantAccount, domain::MerchantKeyStore), errors::StorageError> - { - let accounts = self.merchant_accounts.lock().await; - let account = accounts - .iter() - .find(|account| { - account - .publishable_key - .as_ref() - .is_some_and(|key| key == publishable_key) - }) - .ok_or(errors::StorageError::ValueNotFound(format!( - "Publishable Key: {publishable_key} not found", - )))?; - let key_store = self - .get_merchant_key_store_by_merchant_id( - state, - account.get_id(), - &self.get_master_key().to_vec().into(), - ) - .await?; - let merchant_account = account - .clone() - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?; - Ok((merchant_account, key_store)) - } - - async fn update_all_merchant_account( - &self, - merchant_account_update: storage::MerchantAccountUpdate, - ) -> CustomResult { - let mut accounts = self.merchant_accounts.lock().await; - Ok(accounts.iter_mut().fold(0, |acc, account| { - let update = MerchantAccountUpdateInternal::from(merchant_account_update.clone()) - .apply_changeset(account.clone()); - *account = update; - acc + 1 - })) - } - - async fn delete_merchant_account_by_merchant_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - let mut accounts = self.merchant_accounts.lock().await; - accounts.retain(|x| x.get_id() != merchant_id); - Ok(true) - } - - #[cfg(feature = "olap")] - async fn list_merchant_accounts_by_organization_id( - &self, - state: &KeyManagerState, - organization_id: &common_utils::id_type::OrganizationId, - ) -> CustomResult, errors::StorageError> { - let accounts = self.merchant_accounts.lock().await; - let futures = accounts - .iter() - .filter(|account| account.organization_id == *organization_id) - .map(|account| async { - let key_store = self - .get_merchant_key_store_by_merchant_id( - state, - account.get_id(), - &self.get_master_key().to_vec().into(), - ) - .await; - match key_store { - Ok(key) => account - .clone() - .convert(state, key.key.get_inner(), key.merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DecryptionError), - Err(err) => Err(err), - } - }); - futures::future::join_all(futures) - .await - .into_iter() - .collect() - } - - #[cfg(feature = "olap")] - async fn list_multiple_merchant_accounts( - &self, - state: &KeyManagerState, - merchant_ids: Vec, - ) -> CustomResult, errors::StorageError> { - let accounts = self.merchant_accounts.lock().await; - let futures = accounts - .iter() - .filter(|account| merchant_ids.contains(account.get_id())) - .map(|account| async { - let key_store = self - .get_merchant_key_store_by_merchant_id( - state, - account.get_id(), - &self.get_master_key().to_vec().into(), - ) - .await; - match key_store { - Ok(key) => account - .clone() - .convert(state, key.key.get_inner(), key.merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DecryptionError), - Err(err) => Err(err), - } - }); - futures::future::join_all(futures) - .await - .into_iter() - .collect() - } - - #[cfg(feature = "olap")] - async fn list_merchant_and_org_ids( - &self, - _state: &KeyManagerState, - limit: u32, - offset: Option, - ) -> CustomResult< - Vec<( - common_utils::id_type::MerchantId, - common_utils::id_type::OrganizationId, - )>, - errors::StorageError, - > { - let accounts = self.merchant_accounts.lock().await; - let limit = limit.try_into().unwrap_or(accounts.len()); - let offset = offset.unwrap_or(0).try_into().unwrap_or(0); - - let merchant_and_org_ids = accounts - .iter() - .skip(offset) - .take(limit) - .map(|account| (account.get_id().clone(), account.organization_id.clone())) - .collect::>(); - - Ok(merchant_and_org_ids) - } -} - -#[cfg(feature = "accounts_cache")] -async fn publish_and_redact_merchant_account_cache( - store: &dyn super::StorageInterface, - merchant_account: &storage::MerchantAccount, -) -> CustomResult<(), errors::StorageError> { - let publishable_key = merchant_account - .publishable_key - .as_ref() - .map(|publishable_key| CacheKind::Accounts(publishable_key.into())); - - #[cfg(feature = "v1")] - let cgraph_key = merchant_account.default_profile.as_ref().map(|profile_id| { - CacheKind::CGraph( - format!( - "cgraph_{}_{}", - merchant_account.get_id().get_string_repr(), - profile_id.get_string_repr(), - ) - .into(), - ) - }); - - // TODO: we will not have default profile in v2 - #[cfg(feature = "v2")] - let cgraph_key = None; - - let mut cache_keys = vec![CacheKind::Accounts( - merchant_account.get_id().get_string_repr().into(), - )]; - - cache_keys.extend(publishable_key.into_iter()); - cache_keys.extend(cgraph_key.into_iter()); - - cache::redact_from_redis_and_publish(store.get_cache_store().as_ref(), cache_keys).await?; - Ok(()) -} - -#[cfg(feature = "accounts_cache")] -async fn publish_and_redact_all_merchant_account_cache( - store: &dyn super::StorageInterface, - merchant_accounts: &[storage::MerchantAccount], -) -> CustomResult<(), errors::StorageError> { - let merchant_ids = merchant_accounts - .iter() - .map(|merchant_account| merchant_account.get_id().get_string_repr().to_string()); - let publishable_keys = merchant_accounts - .iter() - .filter_map(|m| m.publishable_key.clone()); - - let cache_keys: Vec> = merchant_ids - .chain(publishable_keys) - .map(|s| CacheKind::Accounts(s.into())) - .collect(); - - cache::redact_from_redis_and_publish(store.get_cache_store().as_ref(), cache_keys).await?; - Ok(()) -} +pub use hyperswitch_domain_models::merchant_account::{self, MerchantAccountInterface}; diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index b9618e2c63..8495c344bb 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -1,27 +1,13 @@ -use async_bb8_diesel::AsyncConnection; -use common_utils::{ - encryption::Encryption, - ext_traits::{AsyncExt, ByteSliceExt, Encode}, - types::keymanager::KeyManagerState, -}; -use error_stack::{report, ResultExt}; +use common_utils::ext_traits::{ByteSliceExt, Encode}; +use error_stack::ResultExt; +pub use hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccountInterface; use router_env::{instrument, tracing}; -#[cfg(feature = "accounts_cache")] -use storage_impl::redis::cache; use storage_impl::redis::kv_store::RedisConnInterface; use super::{MockDb, Store}; use crate::{ - connection, core::errors::{self, CustomResult}, - types::{ - self, - domain::{ - self, - behaviour::{Conversion, ReverseConversion}, - }, - storage, - }, + types, }; #[async_trait::async_trait] @@ -114,1411 +100,6 @@ impl ConnectorAccessToken for MockDb { } } -#[async_trait::async_trait] -pub trait MerchantConnectorAccountInterface -where - domain::MerchantConnectorAccount: Conversion< - DstType = storage::MerchantConnectorAccount, - NewDstType = storage::MerchantConnectorAccountNew, - >, -{ - #[cfg(feature = "v1")] - async fn find_merchant_connector_account_by_merchant_id_connector_label( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - connector_label: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - #[cfg(feature = "v1")] - async fn find_merchant_connector_account_by_profile_id_connector_name( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - connector_name: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - #[cfg(feature = "v1")] - async fn find_merchant_connector_account_by_merchant_id_connector_name( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - connector_name: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult, errors::StorageError>; - - async fn insert_merchant_connector_account( - &self, - state: &KeyManagerState, - t: domain::MerchantConnectorAccount, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - #[cfg(feature = "v1")] - async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - #[cfg(feature = "v2")] - async fn find_merchant_connector_account_by_id( - &self, - state: &KeyManagerState, - id: &common_utils::id_type::MerchantConnectorAccountId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - get_disabled: bool, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - #[cfg(all(feature = "olap", feature = "v2"))] - async fn list_connector_account_by_profile_id( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult, errors::StorageError>; - - async fn list_enabled_connector_accounts_by_profile_id( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - key_store: &domain::MerchantKeyStore, - connector_type: common_enums::ConnectorType, - ) -> CustomResult, errors::StorageError>; - - async fn update_merchant_connector_account( - &self, - state: &KeyManagerState, - this: domain::MerchantConnectorAccount, - merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult; - - async fn update_multiple_merchant_connector_accounts( - &self, - this: Vec<( - domain::MerchantConnectorAccount, - storage::MerchantConnectorAccountUpdateInternal, - )>, - ) -> CustomResult<(), errors::StorageError>; - - #[cfg(feature = "v1")] - async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, - ) -> CustomResult; - - #[cfg(feature = "v2")] - async fn delete_merchant_connector_account_by_id( - &self, - id: &common_utils::id_type::MerchantConnectorAccountId, - ) -> CustomResult; -} - -#[async_trait::async_trait] -impl MerchantConnectorAccountInterface for Store { - #[cfg(feature = "v1")] - #[instrument(skip_all)] - async fn find_merchant_connector_account_by_merchant_id_connector_label( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - connector_label: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let find_call = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::MerchantConnectorAccount::find_by_merchant_id_connector( - &conn, - merchant_id, - connector_label, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(not(feature = "accounts_cache"))] - { - find_call() - .await? - .convert(state, key_store.key.get_inner(), merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DeserializationFailed) - } - - #[cfg(feature = "accounts_cache")] - { - cache::get_or_populate_in_memory( - self, - &format!("{}_{}", merchant_id.get_string_repr(), connector_label), - find_call, - &cache::ACCOUNTS_CACHE, - ) - .await - .async_and_then(|item| async { - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - } - } - - #[cfg(feature = "v1")] - #[instrument(skip_all)] - async fn find_merchant_connector_account_by_profile_id_connector_name( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - connector_name: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let find_call = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::MerchantConnectorAccount::find_by_profile_id_connector_name( - &conn, - profile_id, - connector_name, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(not(feature = "accounts_cache"))] - { - find_call() - .await? - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DeserializationFailed) - } - - #[cfg(feature = "accounts_cache")] - { - cache::get_or_populate_in_memory( - self, - &format!("{}_{}", profile_id.get_string_repr(), connector_name), - find_call, - &cache::ACCOUNTS_CACHE, - ) - .await - .async_and_then(|item| async { - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - } - } - - #[cfg(feature = "v1")] - #[instrument(skip_all)] - async fn find_merchant_connector_account_by_merchant_id_connector_name( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - connector_name: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::MerchantConnectorAccount::find_by_merchant_id_connector_name( - &conn, - merchant_id, - connector_name, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|items| async { - let mut output = Vec::with_capacity(items.len()); - for item in items.into_iter() { - output.push( - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ) - } - Ok(output) - }) - .await - } - - #[instrument(skip_all)] - #[cfg(feature = "v1")] - async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let find_call = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( - &conn, - merchant_id, - merchant_connector_id, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(not(feature = "accounts_cache"))] - { - find_call() - .await? - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[cfg(feature = "accounts_cache")] - { - cache::get_or_populate_in_memory( - self, - &format!( - "{}_{}", - merchant_id.get_string_repr(), - merchant_connector_id.get_string_repr() - ), - find_call, - &cache::ACCOUNTS_CACHE, - ) - .await? - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - } - - #[instrument(skip_all)] - #[cfg(feature = "v2")] - async fn find_merchant_connector_account_by_id( - &self, - state: &KeyManagerState, - id: &common_utils::id_type::MerchantConnectorAccountId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let find_call = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::MerchantConnectorAccount::find_by_id(&conn, id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(not(feature = "accounts_cache"))] - { - find_call() - .await? - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[cfg(feature = "accounts_cache")] - { - cache::get_or_populate_in_memory( - self, - id.get_string_repr(), - find_call, - &cache::ACCOUNTS_CACHE, - ) - .await? - .convert( - state, - key_store.key.get_inner(), - common_utils::types::keymanager::Identifier::Merchant( - key_store.merchant_id.clone(), - ), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - } - - #[instrument(skip_all)] - async fn insert_merchant_connector_account( - &self, - state: &KeyManagerState, - t: domain::MerchantConnectorAccount, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - t.construct_new() - .await - .change_context(errors::StorageError::EncryptionError)? - .insert(&conn) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|item| async { - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - } - - async fn list_enabled_connector_accounts_by_profile_id( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - key_store: &domain::MerchantKeyStore, - connector_type: common_enums::ConnectorType, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_accounts_connection_read(self).await?; - - storage::MerchantConnectorAccount::list_enabled_by_profile_id( - &conn, - profile_id, - connector_type, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|items| async { - let mut output = Vec::with_capacity(items.len()); - for item in items.into_iter() { - output.push( - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ) - } - Ok(output) - }) - .await - } - - #[instrument(skip_all)] - async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - get_disabled: bool, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_read(self).await?; - let merchant_connector_account_vec = - storage::MerchantConnectorAccount::find_by_merchant_id( - &conn, - merchant_id, - get_disabled, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|items| async { - let mut output = Vec::with_capacity(items.len()); - for item in items.into_iter() { - output.push( - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ) - } - Ok(output) - }) - .await?; - Ok(domain::MerchantConnectorAccounts::new( - merchant_connector_account_vec, - )) - } - - #[instrument(skip_all)] - #[cfg(all(feature = "olap", feature = "v2"))] - async fn list_connector_account_by_profile_id( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_accounts_connection_read(self).await?; - storage::MerchantConnectorAccount::list_by_profile_id(&conn, profile_id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|items| async { - let mut output = Vec::with_capacity(items.len()); - for item in items.into_iter() { - output.push( - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ) - } - Ok(output) - }) - .await - } - - #[instrument(skip_all)] - async fn update_multiple_merchant_connector_accounts( - &self, - merchant_connector_accounts: Vec<( - domain::MerchantConnectorAccount, - storage::MerchantConnectorAccountUpdateInternal, - )>, - ) -> CustomResult<(), errors::StorageError> { - let conn = connection::pg_accounts_connection_write(self).await?; - - async fn update_call( - connection: &diesel_models::PgPooledConn, - (merchant_connector_account, mca_update): ( - domain::MerchantConnectorAccount, - storage::MerchantConnectorAccountUpdateInternal, - ), - ) -> Result<(), error_stack::Report> { - Conversion::convert(merchant_connector_account) - .await - .change_context(errors::StorageError::EncryptionError)? - .update(connection, mca_update) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - Ok(()) - } - - conn.transaction_async(|connection_pool| async move { - for (merchant_connector_account, update_merchant_connector_account) in - merchant_connector_accounts - { - #[cfg(feature = "v1")] - let _connector_name = merchant_connector_account.connector_name.clone(); - - #[cfg(feature = "v2")] - let _connector_name = merchant_connector_account.connector_name.to_string(); - - let _profile_id = merchant_connector_account.profile_id.clone(); - - let _merchant_id = merchant_connector_account.merchant_id.clone(); - let _merchant_connector_id = merchant_connector_account.get_id().clone(); - - let update = update_call( - &connection_pool, - ( - merchant_connector_account, - update_merchant_connector_account, - ), - ); - - #[cfg(feature = "accounts_cache")] - // Redact all caches as any of might be used because of backwards compatibility - Box::pin(cache::publish_and_redact_multiple( - self, - [ - cache::CacheKind::Accounts( - format!("{}_{}", _profile_id.get_string_repr(), _connector_name).into(), - ), - cache::CacheKind::Accounts( - format!( - "{}_{}", - _merchant_id.get_string_repr(), - _merchant_connector_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::CGraph( - format!( - "cgraph_{}_{}", - _merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - ], - || update, - )) - .await - .map_err(|error| { - // Returning `DatabaseConnectionError` after logging the actual error because - // -> it is not possible to get the underlying from `error_stack::Report` - // -> it is not possible to write a `From` impl to convert the `diesel::result::Error` to `error_stack::Report` - // because of Rust's orphan rules - router_env::logger::error!( - ?error, - "DB transaction for updating multiple merchant connector account failed" - ); - errors::StorageError::DatabaseConnectionError - })?; - - #[cfg(not(feature = "accounts_cache"))] - { - update.await.map_err(|error| { - // Returning `DatabaseConnectionError` after logging the actual error because - // -> it is not possible to get the underlying from `error_stack::Report` - // -> it is not possible to write a `From` impl to convert the `diesel::result::Error` to `error_stack::Report` - // because of Rust's orphan rules - router_env::logger::error!( - ?error, - "DB transaction for updating multiple merchant connector account failed" - ); - errors::StorageError::DatabaseConnectionError - })?; - } - } - Ok::<_, errors::StorageError>(()) - }) - .await?; - Ok(()) - } - - #[instrument(skip_all)] - #[cfg(feature = "v1")] - async fn update_merchant_connector_account( - &self, - state: &KeyManagerState, - this: domain::MerchantConnectorAccount, - merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let _connector_name = this.connector_name.clone(); - let _profile_id = this.profile_id.clone(); - - let _merchant_id = this.merchant_id.clone(); - let _merchant_connector_id = this.merchant_connector_id.clone(); - - let update_call = || async { - let conn = connection::pg_accounts_connection_write(self).await?; - Conversion::convert(this) - .await - .change_context(errors::StorageError::EncryptionError)? - .update(&conn, merchant_connector_account) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|item| async { - item.convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - }; - - #[cfg(feature = "accounts_cache")] - { - // Redact all caches as any of might be used because of backwards compatibility - cache::publish_and_redact_multiple( - self, - [ - cache::CacheKind::Accounts( - format!("{}_{}", _profile_id.get_string_repr(), _connector_name).into(), - ), - cache::CacheKind::Accounts( - format!( - "{}_{}", - _merchant_id.get_string_repr(), - _merchant_connector_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::CGraph( - format!( - "cgraph_{}_{}", - _merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::PmFiltersCGraph( - format!( - "pm_filters_cgraph_{}_{}", - _merchant_id.get_string_repr(), - _profile_id.get_string_repr(), - ) - .into(), - ), - ], - update_call, - ) - .await - } - - #[cfg(not(feature = "accounts_cache"))] - { - update_call().await - } - } - - #[instrument(skip_all)] - #[cfg(feature = "v2")] - async fn update_merchant_connector_account( - &self, - state: &KeyManagerState, - this: domain::MerchantConnectorAccount, - merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let _connector_name = this.connector_name; - let _profile_id = this.profile_id.clone(); - - let _merchant_id = this.merchant_id.clone(); - let _merchant_connector_id = this.get_id().clone(); - - let update_call = || async { - let conn = connection::pg_accounts_connection_write(self).await?; - Conversion::convert(this) - .await - .change_context(errors::StorageError::EncryptionError)? - .update(&conn, merchant_connector_account) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - .async_and_then(|item| async { - item.convert( - state, - key_store.key.get_inner(), - common_utils::types::keymanager::Identifier::Merchant( - key_store.merchant_id.clone(), - ), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - }; - - #[cfg(feature = "accounts_cache")] - { - // Redact all caches as any of might be used because of backwards compatibility - cache::publish_and_redact_multiple( - self, - [ - cache::CacheKind::Accounts( - format!("{}_{}", _profile_id.get_string_repr(), _connector_name).into(), - ), - cache::CacheKind::Accounts( - _merchant_connector_id.get_string_repr().to_string().into(), - ), - cache::CacheKind::CGraph( - format!( - "cgraph_{}_{}", - _merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::PmFiltersCGraph( - format!( - "pm_filters_cgraph_{}_{}", - _merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - ], - update_call, - ) - .await - } - - #[cfg(not(feature = "accounts_cache"))] - { - update_call().await - } - } - - #[instrument(skip_all)] - #[cfg(feature = "v1")] - async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - let delete_call = || async { - storage::MerchantConnectorAccount::delete_by_merchant_id_merchant_connector_id( - &conn, - merchant_id, - merchant_connector_id, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(feature = "accounts_cache")] - { - // We need to fetch mca here because the key that's saved in cache in - // {merchant_id}_{connector_label}. - // Used function from storage model to reuse the connection that made here instead of - // creating new. - - let mca = storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( - &conn, - merchant_id, - merchant_connector_id, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - let _profile_id = mca.profile_id.ok_or(errors::StorageError::ValueNotFound( - "profile_id".to_string(), - ))?; - - cache::publish_and_redact_multiple( - self, - [ - cache::CacheKind::Accounts( - format!( - "{}_{}", - mca.merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::CGraph( - format!( - "cgraph_{}_{}", - mca.merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::PmFiltersCGraph( - format!( - "pm_filters_cgraph_{}_{}", - mca.merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - ], - delete_call, - ) - .await - } - - #[cfg(not(feature = "accounts_cache"))] - { - delete_call().await - } - } - - #[instrument(skip_all)] - #[cfg(feature = "v2")] - async fn delete_merchant_connector_account_by_id( - &self, - id: &common_utils::id_type::MerchantConnectorAccountId, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - let delete_call = || async { - storage::MerchantConnectorAccount::delete_by_id(&conn, id) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(feature = "accounts_cache")] - { - // We need to fetch mca here because the key that's saved in cache in - // {merchant_id}_{connector_label}. - // Used function from storage model to reuse the connection that made here instead of - // creating new. - - let mca = storage::MerchantConnectorAccount::find_by_id(&conn, id) - .await - .map_err(|error| report!(errors::StorageError::from(error)))?; - - let _profile_id = mca.profile_id; - - cache::publish_and_redact_multiple( - self, - [ - cache::CacheKind::Accounts( - format!( - "{}_{}", - mca.merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::CGraph( - format!( - "cgraph_{}_{}", - mca.merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - cache::CacheKind::PmFiltersCGraph( - format!( - "pm_filters_cgraph_{}_{}", - mca.merchant_id.get_string_repr(), - _profile_id.get_string_repr() - ) - .into(), - ), - ], - delete_call, - ) - .await - } - - #[cfg(not(feature = "accounts_cache"))] - { - delete_call().await - } - } -} - -#[async_trait::async_trait] -impl MerchantConnectorAccountInterface for MockDb { - async fn update_multiple_merchant_connector_accounts( - &self, - _merchant_connector_accounts: Vec<( - domain::MerchantConnectorAccount, - storage::MerchantConnectorAccountUpdateInternal, - )>, - ) -> CustomResult<(), errors::StorageError> { - // No need to implement this function for `MockDb` as this function will be removed after the - // apple pay certificate migration - Err(errors::StorageError::MockDbError)? - } - #[cfg(feature = "v1")] - async fn find_merchant_connector_account_by_merchant_id_connector_label( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - connector: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - match self - .merchant_connector_accounts - .lock() - .await - .iter() - .find(|account| { - account.merchant_id == *merchant_id - && account.connector_label == Some(connector.to_string()) - }) - .cloned() - .async_map(|account| async { - account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - { - Some(result) => result, - None => { - return Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account".to_string(), - ) - .into()) - } - } - } - - async fn list_enabled_connector_accounts_by_profile_id( - &self, - _state: &KeyManagerState, - _profile_id: &common_utils::id_type::ProfileId, - _key_store: &domain::MerchantKeyStore, - _connector_type: common_enums::ConnectorType, - ) -> CustomResult, errors::StorageError> { - Err(errors::StorageError::MockDbError)? - } - - #[cfg(feature = "v1")] - async fn find_merchant_connector_account_by_merchant_id_connector_name( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - connector_name: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult, errors::StorageError> { - let accounts = self - .merchant_connector_accounts - .lock() - .await - .iter() - .filter(|account| { - account.merchant_id == *merchant_id && account.connector_name == connector_name - }) - .cloned() - .collect::>(); - let mut output = Vec::with_capacity(accounts.len()); - for account in accounts.into_iter() { - output.push( - account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ) - } - Ok(output) - } - - #[cfg(feature = "v1")] - async fn find_merchant_connector_account_by_profile_id_connector_name( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - connector_name: &str, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let maybe_mca = self - .merchant_connector_accounts - .lock() - .await - .iter() - .find(|account| { - account.profile_id.eq(&Some(profile_id.to_owned())) - && account.connector_name == connector_name - }) - .cloned(); - - match maybe_mca { - Some(mca) => mca - .to_owned() - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError), - None => Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account".to_string(), - ) - .into()), - } - } - - #[cfg(feature = "v1")] - async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - match self - .merchant_connector_accounts - .lock() - .await - .iter() - .find(|account| { - account.merchant_id == *merchant_id - && account.merchant_connector_id == *merchant_connector_id - }) - .cloned() - .async_map(|account| async { - account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - { - Some(result) => result, - None => { - return Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account".to_string(), - ) - .into()) - } - } - } - - #[cfg(feature = "v2")] - async fn find_merchant_connector_account_by_id( - &self, - state: &KeyManagerState, - id: &common_utils::id_type::MerchantConnectorAccountId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - match self - .merchant_connector_accounts - .lock() - .await - .iter() - .find(|account| account.get_id() == *id) - .cloned() - .async_map(|account| async { - account - .convert( - state, - key_store.key.get_inner(), - common_utils::types::keymanager::Identifier::Merchant( - key_store.merchant_id.clone(), - ), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await - { - Some(result) => result, - None => { - return Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account".to_string(), - ) - .into()) - } - } - } - - #[cfg(feature = "v1")] - async fn insert_merchant_connector_account( - &self, - state: &KeyManagerState, - t: domain::MerchantConnectorAccount, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let mut accounts = self.merchant_connector_accounts.lock().await; - let account = storage::MerchantConnectorAccount { - merchant_id: t.merchant_id, - connector_name: t.connector_name, - connector_account_details: t.connector_account_details.into(), - test_mode: t.test_mode, - disabled: t.disabled, - merchant_connector_id: t.merchant_connector_id.clone(), - id: Some(t.merchant_connector_id), - payment_methods_enabled: t.payment_methods_enabled, - metadata: t.metadata, - frm_configs: None, - frm_config: t.frm_configs, - connector_type: t.connector_type, - connector_label: t.connector_label, - business_country: t.business_country, - business_label: t.business_label, - business_sub_label: t.business_sub_label, - created_at: common_utils::date_time::now(), - modified_at: common_utils::date_time::now(), - connector_webhook_details: t.connector_webhook_details, - profile_id: Some(t.profile_id), - applepay_verified_domains: t.applepay_verified_domains, - pm_auth_config: t.pm_auth_config, - status: t.status, - connector_wallets_details: t.connector_wallets_details.map(Encryption::from), - additional_merchant_data: t.additional_merchant_data.map(|data| data.into()), - version: t.version, - }; - accounts.push(account.clone()); - account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[cfg(feature = "v2")] - async fn insert_merchant_connector_account( - &self, - state: &KeyManagerState, - t: domain::MerchantConnectorAccount, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let mut accounts = self.merchant_connector_accounts.lock().await; - let account = storage::MerchantConnectorAccount { - id: t.id, - merchant_id: t.merchant_id, - connector_name: t.connector_name, - connector_account_details: t.connector_account_details.into(), - disabled: t.disabled, - payment_methods_enabled: t.payment_methods_enabled, - metadata: t.metadata, - frm_config: t.frm_configs, - connector_type: t.connector_type, - connector_label: t.connector_label, - created_at: common_utils::date_time::now(), - modified_at: common_utils::date_time::now(), - connector_webhook_details: t.connector_webhook_details, - profile_id: t.profile_id, - applepay_verified_domains: t.applepay_verified_domains, - pm_auth_config: t.pm_auth_config, - status: t.status, - connector_wallets_details: t.connector_wallets_details.map(Encryption::from), - additional_merchant_data: t.additional_merchant_data.map(|data| data.into()), - version: t.version, - feature_metadata: t.feature_metadata.map(From::from), - }; - accounts.push(account.clone()); - account - .convert( - state, - key_store.key.get_inner(), - common_utils::types::keymanager::Identifier::Merchant( - key_store.merchant_id.clone(), - ), - ) - .await - .change_context(errors::StorageError::DecryptionError) - } - - async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - get_disabled: bool, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let accounts = self - .merchant_connector_accounts - .lock() - .await - .iter() - .filter(|account: &&storage::MerchantConnectorAccount| { - if get_disabled { - account.merchant_id == *merchant_id - } else { - account.merchant_id == *merchant_id && account.disabled == Some(false) - } - }) - .cloned() - .collect::>(); - - let mut output = Vec::with_capacity(accounts.len()); - for account in accounts.into_iter() { - output.push( - account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ) - } - Ok(domain::MerchantConnectorAccounts::new(output)) - } - - #[cfg(all(feature = "olap", feature = "v2"))] - async fn list_connector_account_by_profile_id( - &self, - state: &KeyManagerState, - profile_id: &common_utils::id_type::ProfileId, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult, errors::StorageError> { - let accounts = self - .merchant_connector_accounts - .lock() - .await - .iter() - .filter(|account: &&storage::MerchantConnectorAccount| { - account.profile_id == *profile_id - }) - .cloned() - .collect::>(); - - let mut output = Vec::with_capacity(accounts.len()); - for account in accounts.into_iter() { - output.push( - account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError)?, - ) - } - Ok(output) - } - - #[cfg(feature = "v1")] - async fn update_merchant_connector_account( - &self, - state: &KeyManagerState, - this: domain::MerchantConnectorAccount, - merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let mca_update_res = self - .merchant_connector_accounts - .lock() - .await - .iter_mut() - .find(|account| account.merchant_connector_id == this.merchant_connector_id) - .map(|a| { - let updated = - merchant_connector_account.create_merchant_connector_account(a.clone()); - *a = updated.clone(); - updated - }) - .async_map(|account| async { - account - .convert( - state, - key_store.key.get_inner(), - key_store.merchant_id.clone().into(), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await; - - match mca_update_res { - Some(result) => result, - None => { - return Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account to update".to_string(), - ) - .into()) - } - } - } - - #[cfg(feature = "v2")] - async fn update_merchant_connector_account( - &self, - state: &KeyManagerState, - this: domain::MerchantConnectorAccount, - merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - let mca_update_res = self - .merchant_connector_accounts - .lock() - .await - .iter_mut() - .find(|account| account.get_id() == this.get_id()) - .map(|a| { - let updated = - merchant_connector_account.create_merchant_connector_account(a.clone()); - *a = updated.clone(); - updated - }) - .async_map(|account| async { - account - .convert( - state, - key_store.key.get_inner(), - common_utils::types::keymanager::Identifier::Merchant( - key_store.merchant_id.clone(), - ), - ) - .await - .change_context(errors::StorageError::DecryptionError) - }) - .await; - - match mca_update_res { - Some(result) => result, - None => { - return Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account to update".to_string(), - ) - .into()) - } - } - } - - #[cfg(feature = "v1")] - async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, - ) -> CustomResult { - let mut accounts = self.merchant_connector_accounts.lock().await; - match accounts.iter().position(|account| { - account.merchant_id == *merchant_id - && account.merchant_connector_id == *merchant_connector_id - }) { - Some(index) => { - accounts.remove(index); - return Ok(true); - } - None => { - return Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account to delete".to_string(), - ) - .into()) - } - } - } - - #[cfg(feature = "v2")] - async fn delete_merchant_connector_account_by_id( - &self, - id: &common_utils::id_type::MerchantConnectorAccountId, - ) -> CustomResult { - let mut accounts = self.merchant_connector_accounts.lock().await; - match accounts.iter().position(|account| account.get_id() == *id) { - Some(index) => { - accounts.remove(index); - return Ok(true); - } - None => { - return Err(errors::StorageError::ValueNotFound( - "cannot find merchant connector account to delete".to_string(), - ) - .into()) - } - } - } -} - #[cfg(feature = "accounts_cache")] #[cfg(test)] mod merchant_connector_account_cache_tests { @@ -1542,7 +123,7 @@ mod merchant_connector_account_cache_tests { core::errors, db::{ merchant_connector_account::MerchantConnectorAccountInterface, - merchant_key_store::MerchantKeyStoreInterface, MasterKeyInterface, MockDb, + merchant_key_store::MerchantKeyStoreInterface, MockDb, }, routes::{ self, diff --git a/crates/router/src/db/merchant_key_store.rs b/crates/router/src/db/merchant_key_store.rs index c1ce0f5fea..204544ac5f 100644 --- a/crates/router/src/db/merchant_key_store.rs +++ b/crates/router/src/db/merchant_key_store.rs @@ -1,320 +1,4 @@ -use common_utils::types::keymanager::KeyManagerState; -use error_stack::{report, ResultExt}; -use masking::Secret; -use router_env::{instrument, tracing}; -#[cfg(feature = "accounts_cache")] -use storage_impl::redis::cache::{self, CacheKind, ACCOUNTS_CACHE}; - -use crate::{ - connection, - core::errors::{self, CustomResult}, - db::MockDb, - services::Store, - types::domain::{ - self, - behaviour::{Conversion, ReverseConversion}, - }, -}; - -#[async_trait::async_trait] -pub trait MerchantKeyStoreInterface { - async fn insert_merchant_key_store( - &self, - state: &KeyManagerState, - merchant_key_store: domain::MerchantKeyStore, - key: &Secret>, - ) -> CustomResult; - - async fn get_merchant_key_store_by_merchant_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - key: &Secret>, - ) -> CustomResult; - - async fn delete_merchant_key_store_by_merchant_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult; - - #[cfg(feature = "olap")] - async fn list_multiple_key_stores( - &self, - state: &KeyManagerState, - merchant_ids: Vec, - key: &Secret>, - ) -> CustomResult, errors::StorageError>; - - async fn get_all_key_stores( - &self, - state: &KeyManagerState, - key: &Secret>, - from: u32, - to: u32, - ) -> CustomResult, errors::StorageError>; -} - -#[async_trait::async_trait] -impl MerchantKeyStoreInterface for Store { - #[instrument(skip_all)] - async fn insert_merchant_key_store( - &self, - state: &KeyManagerState, - merchant_key_store: domain::MerchantKeyStore, - key: &Secret>, - ) -> CustomResult { - let conn = connection::pg_accounts_connection_write(self).await?; - let merchant_id = merchant_key_store.merchant_id.clone(); - merchant_key_store - .construct_new() - .await - .change_context(errors::StorageError::EncryptionError)? - .insert(&conn) - .await - .map_err(|error| report!(errors::StorageError::from(error)))? - .convert(state, key, merchant_id.into()) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[instrument(skip_all)] - async fn get_merchant_key_store_by_merchant_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - key: &Secret>, - ) -> CustomResult { - let fetch_func = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - - diesel_models::merchant_key_store::MerchantKeyStore::find_by_merchant_id( - &conn, - merchant_id, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(not(feature = "accounts_cache"))] - { - fetch_func() - .await? - .convert(state, key, merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DecryptionError) - } - - #[cfg(feature = "accounts_cache")] - { - let key_store_cache_key = - format!("merchant_key_store_{}", merchant_id.get_string_repr()); - cache::get_or_populate_in_memory( - self, - &key_store_cache_key, - fetch_func, - &ACCOUNTS_CACHE, - ) - .await? - .convert(state, key, merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DecryptionError) - } - } - - #[instrument(skip_all)] - async fn delete_merchant_key_store_by_merchant_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - let delete_func = || async { - let conn = connection::pg_accounts_connection_write(self).await?; - diesel_models::merchant_key_store::MerchantKeyStore::delete_by_merchant_id( - &conn, - merchant_id, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - #[cfg(not(feature = "accounts_cache"))] - { - delete_func().await - } - - #[cfg(feature = "accounts_cache")] - { - let key_store_cache_key = - format!("merchant_key_store_{}", merchant_id.get_string_repr()); - cache::publish_and_redact( - self, - CacheKind::Accounts(key_store_cache_key.into()), - delete_func, - ) - .await - } - } - - #[cfg(feature = "olap")] - #[instrument(skip_all)] - async fn list_multiple_key_stores( - &self, - state: &KeyManagerState, - merchant_ids: Vec, - key: &Secret>, - ) -> CustomResult, errors::StorageError> { - let fetch_func = || async { - let conn = connection::pg_accounts_connection_read(self).await?; - - diesel_models::merchant_key_store::MerchantKeyStore::list_multiple_key_stores( - &conn, - merchant_ids, - ) - .await - .map_err(|error| report!(errors::StorageError::from(error))) - }; - - futures::future::try_join_all(fetch_func().await?.into_iter().map(|key_store| async { - let merchant_id = key_store.merchant_id.clone(); - key_store - .convert(state, key, merchant_id.into()) - .await - .change_context(errors::StorageError::DecryptionError) - })) - .await - } - - async fn get_all_key_stores( - &self, - state: &KeyManagerState, - key: &Secret>, - from: u32, - to: u32, - ) -> CustomResult, errors::StorageError> { - let conn = connection::pg_accounts_connection_read(self).await?; - let stores = diesel_models::merchant_key_store::MerchantKeyStore::list_all_key_stores( - &conn, from, to, - ) - .await - .map_err(|err| report!(errors::StorageError::from(err)))?; - - futures::future::try_join_all(stores.into_iter().map(|key_store| async { - let merchant_id = key_store.merchant_id.clone(); - key_store - .convert(state, key, merchant_id.into()) - .await - .change_context(errors::StorageError::DecryptionError) - })) - .await - } -} - -#[async_trait::async_trait] -impl MerchantKeyStoreInterface for MockDb { - async fn insert_merchant_key_store( - &self, - state: &KeyManagerState, - merchant_key_store: domain::MerchantKeyStore, - key: &Secret>, - ) -> CustomResult { - let mut locked_merchant_key_store = self.merchant_key_store.lock().await; - - if locked_merchant_key_store - .iter() - .any(|merchant_key| merchant_key.merchant_id == merchant_key_store.merchant_id) - { - Err(errors::StorageError::DuplicateValue { - entity: "merchant_key_store", - key: Some(merchant_key_store.merchant_id.get_string_repr().to_owned()), - })?; - } - - let merchant_key = Conversion::convert(merchant_key_store) - .await - .change_context(errors::StorageError::MockDbError)?; - locked_merchant_key_store.push(merchant_key.clone()); - let merchant_id = merchant_key.merchant_id.clone(); - merchant_key - .convert(state, key, merchant_id.into()) - .await - .change_context(errors::StorageError::DecryptionError) - } - - async fn get_merchant_key_store_by_merchant_id( - &self, - state: &KeyManagerState, - merchant_id: &common_utils::id_type::MerchantId, - key: &Secret>, - ) -> CustomResult { - self.merchant_key_store - .lock() - .await - .iter() - .find(|merchant_key| merchant_key.merchant_id == *merchant_id) - .cloned() - .ok_or(errors::StorageError::ValueNotFound(String::from( - "merchant_key_store", - )))? - .convert(state, key, merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DecryptionError) - } - - async fn delete_merchant_key_store_by_merchant_id( - &self, - merchant_id: &common_utils::id_type::MerchantId, - ) -> CustomResult { - let mut merchant_key_stores = self.merchant_key_store.lock().await; - let index = merchant_key_stores - .iter() - .position(|mks| mks.merchant_id == *merchant_id) - .ok_or(errors::StorageError::ValueNotFound(format!( - "No merchant key store found for merchant_id = {merchant_id:?}", - )))?; - merchant_key_stores.remove(index); - Ok(true) - } - - #[cfg(feature = "olap")] - async fn list_multiple_key_stores( - &self, - state: &KeyManagerState, - merchant_ids: Vec, - key: &Secret>, - ) -> CustomResult, errors::StorageError> { - let merchant_key_stores = self.merchant_key_store.lock().await; - futures::future::try_join_all( - merchant_key_stores - .iter() - .filter(|merchant_key| merchant_ids.contains(&merchant_key.merchant_id)) - .map(|merchant_key| async { - merchant_key - .to_owned() - .convert(state, key, merchant_key.merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DecryptionError) - }), - ) - .await - } - async fn get_all_key_stores( - &self, - state: &KeyManagerState, - key: &Secret>, - _from: u32, - _to: u32, - ) -> CustomResult, errors::StorageError> { - let merchant_key_stores = self.merchant_key_store.lock().await; - - futures::future::try_join_all(merchant_key_stores.iter().map(|merchant_key| async { - merchant_key - .to_owned() - .convert(state, key, merchant_key.merchant_id.clone().into()) - .await - .change_context(errors::StorageError::DecryptionError) - })) - .await - } -} +pub use hyperswitch_domain_models::merchant_key_store::{self, MerchantKeyStoreInterface}; #[cfg(test)] mod tests { @@ -325,7 +9,7 @@ mod tests { use tokio::sync::oneshot; use crate::{ - db::{merchant_key_store::MerchantKeyStoreInterface, MasterKeyInterface, MockDb}, + db::{merchant_key_store::MerchantKeyStoreInterface, MockDb}, routes::{ self, app::{settings::Settings, StorageImpl}, diff --git a/crates/router/src/events.rs b/crates/router/src/events.rs index b7bf1b9f0f..27ba1fc764 100644 --- a/crates/router/src/events.rs +++ b/crates/router/src/events.rs @@ -1,14 +1,13 @@ use std::collections::HashMap; +use common_utils::types::TenantConfig; use error_stack::ResultExt; use events::{EventsError, Message, MessagingInterface}; +use hyperswitch_interfaces::events as events_interfaces; use masking::ErasedMaskSerialize; use router_env::logger; use serde::{Deserialize, Serialize}; -use storage_impl::{ - config::TenantConfig, - errors::{ApplicationError, StorageError, StorageResult}, -}; +use storage_impl::errors::{ApplicationError, StorageError, StorageResult}; use time::PrimitiveDateTime; use crate::{ @@ -66,6 +65,12 @@ impl Default for EventsHandler { } } +impl events_interfaces::EventHandlerInterface for EventsHandler { + fn log_connector_event(&self, event: &events_interfaces::connector_api_logs::ConnectorEvent) { + self.log_event(event); + } +} + impl EventsConfig { pub async fn get_event_handler(&self) -> StorageResult { Ok(match self { diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 3e6d6bb887..36a1b02b61 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -8,7 +8,7 @@ use api_models::routing::RuleMigrationQuery; use common_enums::{ExecutionMode, TransactionType}; #[cfg(feature = "partial-auth")] use common_utils::crypto::Blake3; -use common_utils::id_type; +use common_utils::{id_type, types::TenantConfig}; #[cfg(feature = "email")] use external_services::email::{ no_email::NoEmailClient, ses::AwsSes, smtp::SmtpServer, EmailClientConfigs, EmailService, @@ -27,7 +27,7 @@ use hyperswitch_interfaces::{ }; use router_env::tracing_actix_web::RequestId; use scheduler::SchedulerInterface; -use storage_impl::{config::TenantConfig, redis::RedisStore, MockDb}; +use storage_impl::{redis::RedisStore, MockDb}; use tokio::sync::oneshot; use self::settings::Tenant; @@ -203,7 +203,7 @@ impl SessionStateInfo for SessionState { self.event_handler.clone() } fn get_request_id(&self) -> Option { - self.api_client.get_request_id() + self.api_client.get_request_id_str() } fn add_request_id(&mut self, request_id: RequestId) { self.api_client.add_request_id(request_id); @@ -250,6 +250,31 @@ impl SessionStateInfo for SessionState { self.global_store.to_owned() } } + +impl hyperswitch_interfaces::api_client::ApiClientWrapper for SessionState { + fn get_api_client(&self) -> &dyn crate::services::ApiClient { + self.api_client.as_ref() + } + fn get_proxy(&self) -> hyperswitch_interfaces::types::Proxy { + self.conf.proxy.clone() + } + fn get_request_id(&self) -> Option { + self.request_id + } + fn get_request_id_str(&self) -> Option { + self.request_id + .map(|req_id| req_id.as_hyphenated().to_string()) + } + fn get_tenant(&self) -> Tenant { + self.tenant.clone() + } + fn get_connectors(&self) -> hyperswitch_domain_models::connector_endpoints::Connectors { + self.conf.connectors.clone() + } + fn event_handler(&self) -> &dyn hyperswitch_interfaces::events::EventHandlerInterface { + &self.event_handler + } +} #[derive(Clone)] pub struct AppState { pub flow_name: String, @@ -317,7 +342,7 @@ impl AppStateInfo for AppState { self.api_client.add_flow_name(flow_name); } fn get_request_id(&self) -> Option { - self.api_client.get_request_id() + self.api_client.get_request_id_str() } } diff --git a/crates/router/src/services.rs b/crates/router/src/services.rs index 1383768ed8..d94ab9b514 100644 --- a/crates/router/src/services.rs +++ b/crates/router/src/services.rs @@ -17,6 +17,7 @@ pub mod openidconnect; use std::sync::Arc; +use common_utils::types::TenantConfig; use error_stack::ResultExt; pub use hyperswitch_interfaces::connector_integration_v2::{ BoxedConnectorIntegrationV2, ConnectorIntegrationAnyV2, ConnectorIntegrationV2, @@ -24,7 +25,7 @@ pub use hyperswitch_interfaces::connector_integration_v2::{ use masking::{ExposeInterface, StrongSecret}; #[cfg(feature = "kv_store")] use storage_impl::kv_router_store::KVRouterStore; -use storage_impl::{config::TenantConfig, errors::StorageResult, redis::RedisStore, RouterStore}; +use storage_impl::{errors::StorageResult, redis::RedisStore, RouterStore}; use tokio::sync::oneshot; pub use self::{api::*, encryption::*}; diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index 230250400b..d92633ecae 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -20,11 +20,8 @@ pub use client::{ApiClient, MockApiClient, ProxyClient}; pub use common_enums::enums::PaymentAction; pub use common_utils::request::{ContentType, Method, Request, RequestBuilder}; use common_utils::{ - consts::{ - DEFAULT_TENANT, TENANT_HEADER, X_CONNECTOR_NAME, X_FLOW_NAME, X_HS_LATENCY, X_REQUEST_ID, - }, + consts::{DEFAULT_TENANT, TENANT_HEADER, X_HS_LATENCY}, errors::{ErrorSwitch, ReportSwitchExt}, - request::RequestContent, }; use error_stack::{report, Report, ResultExt}; use hyperswitch_domain_models::router_data_v2::flow_common_types as common_types; @@ -43,14 +40,17 @@ pub use hyperswitch_interfaces::{ ConnectorIntegrationAny, ConnectorRedirectResponse, ConnectorSpecifications, ConnectorValidation, }, + api_client::{ + call_connector_api, execute_connector_processing_step, handle_response, + handle_ucs_response, store_raw_connector_response_if_required, + }, connector_integration_v2::{ BoxedConnectorIntegrationV2, ConnectorIntegrationAnyV2, ConnectorIntegrationV2, }, }; -use masking::{Maskable, PeekInterface, Secret}; +use masking::{Maskable, PeekInterface}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use serde::Serialize; -use serde_json::json; use tera::{Context, Error as TeraError, Tera}; use super::{ @@ -59,26 +59,18 @@ use super::{ }; use crate::{ configs::Settings, - consts, core::{ api_locking, errors::{self, CustomResult}, - payments, unified_connector_service, utils as core_utils, - }, - events::{ - api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, - connector_api_logs::ConnectorEvent, }, + events::api_logs::{ApiEvent, ApiEventMetric, ApiEventsType}, headers, logger, routes::{ app::{AppStateInfo, ReqState, SessionStateInfo}, metrics, AppState, SessionState, }, - services::{ - connector_integration_interface::RouterDataConversion, - generic_link_response::build_generic_link_html, - }, - types::{self, api, ErrorResponse}, + services::generic_link_response::build_generic_link_html, + types::api, utils, }; @@ -138,516 +130,9 @@ pub type BoxedVaultConnectorIntegrationInterface = pub type BoxedGiftCardBalanceCheckIntegrationInterface = BoxedConnectorIntegrationInterface; -/// Handle UCS webhook response processing -fn handle_ucs_response( - router_data: types::RouterData, - transform_data_bytes: Vec, -) -> CustomResult, errors::ConnectorError> -where - T: Clone + Debug + 'static, - Req: Debug + Clone + 'static, - Resp: Debug + Clone + 'static, -{ - let webhook_transform_data: unified_connector_service::WebhookTransformData = - serde_json::from_slice(&transform_data_bytes) - .change_context(errors::ConnectorError::ResponseDeserializationFailed) - .attach_printable("Failed to deserialize UCS webhook transform data")?; - - let webhook_content = webhook_transform_data - .webhook_content - .ok_or(errors::ConnectorError::ResponseDeserializationFailed) - .attach_printable("UCS webhook transform data missing webhook_content")?; - - let payment_get_response = match webhook_content.content { - Some(unified_connector_service_client::payments::webhook_response_content::Content::PaymentsResponse(payments_response)) => { - Ok(payments_response) - }, - Some(unified_connector_service_client::payments::webhook_response_content::Content::RefundsResponse(_)) => { - Err(errors::ConnectorError::ProcessingStepFailed(Some("UCS webhook contains refund response but payment processing was expected".to_string().into())).into()) - }, - Some(unified_connector_service_client::payments::webhook_response_content::Content::DisputesResponse(_)) => { - Err(errors::ConnectorError::ProcessingStepFailed(Some("UCS webhook contains dispute response but payment processing was expected".to_string().into())).into()) - }, - Some(unified_connector_service_client::payments::webhook_response_content::Content::IncompleteTransformation(_)) => { - Err(errors::ConnectorError::ProcessingStepFailed(Some("UCS webhook contains incomplete transformation but payment processing was expected".to_string().into())).into()) - }, - None => { - Err(errors::ConnectorError::ResponseDeserializationFailed) - .attach_printable("UCS webhook content missing payments_response") - } - }?; - - let (router_data_response, status_code) = - unified_connector_service::handle_unified_connector_service_response_for_payment_get( - payment_get_response.clone(), - ) - .change_context(errors::ConnectorError::ProcessingStepFailed(None)) - .attach_printable("Failed to process UCS webhook response using PSync handler")?; - - let mut updated_router_data = router_data; - let router_data_response = router_data_response.map(|(response, status)| { - updated_router_data.status = status; - response - }); - - let _ = router_data_response.map_err(|error_response| { - updated_router_data.response = Err(error_response); - }); - updated_router_data.raw_connector_response = - payment_get_response.raw_connector_response.map(Secret::new); - updated_router_data.connector_http_status_code = Some(status_code); - - Ok(updated_router_data) -} - -fn store_raw_connector_response_if_required( - return_raw_connector_response: Option, - router_data: &mut types::RouterData, - body: &types::Response, -) -> CustomResult<(), errors::ConnectorError> -where - T: Clone + Debug + 'static, - Req: Debug + Clone + 'static, - Resp: Debug + Clone + 'static, -{ - if return_raw_connector_response == Some(true) { - let mut decoded = String::from_utf8(body.response.as_ref().to_vec()) - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - if decoded.starts_with('\u{feff}') { - decoded = decoded.trim_start_matches('\u{feff}').to_string(); - } - router_data.raw_connector_response = Some(Secret::new(decoded)); - } - Ok(()) -} - pub type BoxedSubscriptionConnectorIntegrationInterface = BoxedConnectorIntegrationInterface; -/// Handle the flow by interacting with connector module -/// `connector_request` is applicable only in case if the `CallConnectorAction` is `Trigger` -/// In other cases, It will be created if required, even if it is not passed -#[instrument(skip_all, fields(connector_name, payment_method))] -pub async fn execute_connector_processing_step< - 'b, - 'a, - T, - ResourceCommonData: Clone + RouterDataConversion + 'static, - Req: Debug + Clone + 'static, - Resp: Debug + Clone + 'static, ->( - state: &'b SessionState, - connector_integration: BoxedConnectorIntegrationInterface, - req: &'b types::RouterData, - call_connector_action: payments::CallConnectorAction, - connector_request: Option, - return_raw_connector_response: Option, -) -> CustomResult, errors::ConnectorError> -where - T: Clone + Debug + 'static, - // BoxedConnectorIntegration: 'b, -{ - // If needed add an error stack as follows - // connector_integration.build_request(req).attach_printable("Failed to build request"); - tracing::Span::current().record("connector_name", &req.connector); - tracing::Span::current().record("payment_method", req.payment_method.to_string()); - logger::debug!(connector_request=?connector_request); - let mut router_data = req.clone(); - match call_connector_action { - payments::CallConnectorAction::HandleResponse(res) => { - let response = types::Response { - headers: None, - response: res.into(), - status_code: 200, - }; - connector_integration.handle_response(req, None, response) - } - payments::CallConnectorAction::UCSHandleResponse(transform_data_bytes) => { - handle_ucs_response(router_data, transform_data_bytes) - } - payments::CallConnectorAction::Avoid => Ok(router_data), - payments::CallConnectorAction::StatusUpdate { - status, - error_code, - error_message, - } => { - router_data.status = status; - let error_response = if error_code.is_some() | error_message.is_some() { - Some(ErrorResponse { - code: error_code.unwrap_or(consts::NO_ERROR_CODE.to_string()), - message: error_message.unwrap_or(consts::NO_ERROR_MESSAGE.to_string()), - status_code: 200, // This status code is ignored in redirection response it will override with 302 status code. - reason: None, - attempt_status: None, - connector_transaction_id: None, - network_advice_code: None, - network_decline_code: None, - network_error_message: None, - connector_metadata: None, - }) - } else { - None - }; - router_data.response = error_response.map(Err).unwrap_or(router_data.response); - Ok(router_data) - } - payments::CallConnectorAction::Trigger => { - metrics::CONNECTOR_CALL_COUNT.add( - 1, - router_env::metric_attributes!( - ("connector", req.connector.to_string()), - ( - "flow", - core_utils::get_flow_name::() - .unwrap_or_else(|_| "UnknownFlow".to_string()) - ), - ), - ); - - let connector_request = match connector_request { - Some(connector_request) => Some(connector_request), - None => connector_integration - .build_request(req, &state.conf.connectors) - .inspect_err(|error| { - if matches!( - error.current_context(), - &errors::ConnectorError::RequestEncodingFailed - | &errors::ConnectorError::RequestEncodingFailedWithReason(_) - ) { - metrics::REQUEST_BUILD_FAILURE.add( - 1, - router_env::metric_attributes!(( - "connector", - req.connector.clone() - )), - ) - } - })?, - }; - - match connector_request { - Some(mut request) => { - let masked_request_body = match &request.body { - Some(request) => match request { - RequestContent::Json(i) - | RequestContent::FormUrlEncoded(i) - | RequestContent::Xml(i) => i - .masked_serialize() - .unwrap_or(json!({ "error": "failed to mask serialize"})), - RequestContent::FormData((_, i)) => i - .masked_serialize() - .unwrap_or(json!({ "error": "failed to mask serialize"})), - RequestContent::RawBytes(_) => json!({"request_type": "RAW_BYTES"}), - }, - None => serde_json::Value::Null, - }; - let flow_name = core_utils::get_flow_name::() - .unwrap_or_else(|_| "UnknownFlow".to_string()); - - request.headers.insert(( - X_FLOW_NAME.to_string(), - Maskable::Masked(Secret::new(flow_name.to_string())), - )); - - let connector_name = req.connector.clone(); - request.headers.insert(( - X_CONNECTOR_NAME.to_string(), - Maskable::Masked(Secret::new(connector_name.clone().to_string())), - )); - state.request_id.as_ref().map(|id| { - let request_id = id.to_string(); - request.headers.insert(( - X_REQUEST_ID.to_string(), - Maskable::Normal(request_id.clone()), - )); - request_id - }); - let request_url = request.url.clone(); - let request_method = request.method; - let current_time = Instant::now(); - let response = - call_connector_api(state, request, "execute_connector_processing_step") - .await; - let external_latency = current_time.elapsed().as_millis(); - logger::info!(raw_connector_request=?masked_request_body); - let status_code = response - .as_ref() - .map(|i| { - i.as_ref() - .map_or_else(|value| value.status_code, |value| value.status_code) - }) - .unwrap_or_default(); - let mut connector_event = ConnectorEvent::new( - state.tenant.tenant_id.clone(), - req.connector.clone(), - std::any::type_name::(), - masked_request_body, - request_url, - request_method, - req.payment_id.clone(), - req.merchant_id.clone(), - state.request_id.as_ref(), - external_latency, - req.refund_id.clone(), - req.dispute_id.clone(), - status_code, - ); - - match response { - Ok(body) => { - let response = match body { - Ok(body) => { - let connector_http_status_code = Some(body.status_code); - let handle_response_result = connector_integration - .handle_response(req, Some(&mut connector_event), body.clone()) - .inspect_err(|error| { - if error.current_context() - == &errors::ConnectorError::ResponseDeserializationFailed - { - metrics::RESPONSE_DESERIALIZATION_FAILURE.add( - - 1, - router_env::metric_attributes!(( - "connector", - req.connector.clone(), - )), - ) - } - }); - match handle_response_result { - Ok(mut data) => { - state.event_handler().log_event(&connector_event); - data.connector_http_status_code = - connector_http_status_code; - // Add up multiple external latencies in case of multiple external calls within the same request. - data.external_latency = Some( - data.external_latency - .map_or(external_latency, |val| { - val + external_latency - }), - ); - - store_raw_connector_response_if_required( - return_raw_connector_response, - &mut data, - &body, - )?; - - Ok(data) - } - Err(err) => { - connector_event - .set_error(json!({"error": err.to_string()})); - - state.event_handler().log_event(&connector_event); - Err(err) - } - }? - } - Err(body) => { - router_data.connector_http_status_code = Some(body.status_code); - router_data.external_latency = Some( - router_data - .external_latency - .map_or(external_latency, |val| val + external_latency), - ); - metrics::CONNECTOR_ERROR_RESPONSE_COUNT.add( - 1, - router_env::metric_attributes!(( - "connector", - req.connector.clone(), - )), - ); - - store_raw_connector_response_if_required( - return_raw_connector_response, - &mut router_data, - &body, - )?; - - let error = match body.status_code { - 500..=511 => { - let error_res = connector_integration - .get_5xx_error_response( - body, - Some(&mut connector_event), - )?; - state.event_handler().log_event(&connector_event); - error_res - } - _ => { - let error_res = connector_integration - .get_error_response( - body, - Some(&mut connector_event), - )?; - if let Some(status) = error_res.attempt_status { - router_data.status = status; - }; - state.event_handler().log_event(&connector_event); - error_res - } - }; - - router_data.response = Err(error); - - router_data - } - }; - Ok(response) - } - Err(error) => { - connector_event.set_error(json!({"error": error.to_string()})); - state.event_handler().log_event(&connector_event); - if error.current_context().is_upstream_timeout() { - let error_response = ErrorResponse { - code: consts::REQUEST_TIMEOUT_ERROR_CODE.to_string(), - message: consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string(), - reason: Some(consts::REQUEST_TIMEOUT_ERROR_MESSAGE.to_string()), - status_code: 504, - attempt_status: None, - connector_transaction_id: None, - network_advice_code: None, - network_decline_code: None, - network_error_message: None, - connector_metadata: None, - }; - router_data.response = Err(error_response); - router_data.connector_http_status_code = Some(504); - router_data.external_latency = Some( - router_data - .external_latency - .map_or(external_latency, |val| val + external_latency), - ); - Ok(router_data) - } else { - Err(error.change_context( - errors::ConnectorError::ProcessingStepFailed(None), - )) - } - } - } - } - None => Ok(router_data), - } - } - } -} - -#[instrument(skip_all)] -pub async fn call_connector_api( - state: &SessionState, - request: Request, - flow_name: &str, -) -> CustomResult, errors::ApiClientError> { - let current_time = Instant::now(); - let headers = request.headers.clone(); - let url = request.url.clone(); - let response = state - .api_client - .send_request(state, request, None, true) - .await; - - match response.as_ref() { - Ok(resp) => { - let status_code = resp.status().as_u16(); - let elapsed_time = current_time.elapsed(); - logger::info!( - ?headers, - url, - status_code, - flow=?flow_name, - ?elapsed_time - ); - } - Err(err) => { - logger::info!( - call_connector_api_error=?err - ); - } - } - - handle_response(response).await -} - -#[instrument(skip_all)] -async fn handle_response( - response: CustomResult, -) -> CustomResult, errors::ApiClientError> { - response - .map(|response| async { - logger::info!(?response); - let status_code = response.status().as_u16(); - let headers = Some(response.headers().to_owned()); - match status_code { - 200..=202 | 302 | 204 => { - // If needed add log line - // logger:: error!( error_parsing_response=?err); - let response = response - .bytes() - .await - .change_context(errors::ApiClientError::ResponseDecodingFailed) - .attach_printable("Error while waiting for response")?; - Ok(Ok(types::Response { - headers, - response, - status_code, - })) - } - - status_code @ 500..=599 => { - let bytes = response.bytes().await.map_err(|error| { - report!(error) - .change_context(errors::ApiClientError::ResponseDecodingFailed) - .attach_printable("Client error response received") - })?; - // let error = match status_code { - // 500 => errors::ApiClientError::InternalServerErrorReceived, - // 502 => errors::ApiClientError::BadGatewayReceived, - // 503 => errors::ApiClientError::ServiceUnavailableReceived, - // 504 => errors::ApiClientError::GatewayTimeoutReceived, - // _ => errors::ApiClientError::UnexpectedServerResponse, - // }; - Ok(Err(types::Response { - headers, - response: bytes, - status_code, - })) - } - - status_code @ 400..=499 => { - let bytes = response.bytes().await.map_err(|error| { - report!(error) - .change_context(errors::ApiClientError::ResponseDecodingFailed) - .attach_printable("Client error response received") - })?; - /* let error = match status_code { - 400 => errors::ApiClientError::BadRequestReceived(bytes), - 401 => errors::ApiClientError::UnauthorizedReceived(bytes), - 403 => errors::ApiClientError::ForbiddenReceived, - 404 => errors::ApiClientError::NotFoundReceived(bytes), - 405 => errors::ApiClientError::MethodNotAllowedReceived, - 408 => errors::ApiClientError::RequestTimeoutReceived, - 422 => errors::ApiClientError::UnprocessableEntityReceived(bytes), - 429 => errors::ApiClientError::TooManyRequestsReceived, - _ => errors::ApiClientError::UnexpectedServerResponse, - }; - Err(report!(error).attach_printable("Client error response received")) - */ - Ok(Err(types::Response { - headers, - response: bytes, - status_code, - })) - } - - _ => Err(report!(errors::ApiClientError::UnexpectedServerResponse) - .attach_printable("Unexpected response from server")), - } - })? - .await -} - #[derive(Debug, Eq, PartialEq, Serialize)] pub struct ApplicationRedirectResponse { pub url: String, diff --git a/crates/router/src/services/api/client.rs b/crates/router/src/services/api/client.rs index 9e54e673b9..fd736d9fe0 100644 --- a/crates/router/src/services/api/client.rs +++ b/crates/router/src/services/api/client.rs @@ -4,72 +4,22 @@ use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; pub use external_services::http_client::{self, client}; use http::{HeaderValue, Method}; -use hyperswitch_interfaces::types::Proxy; +pub use hyperswitch_interfaces::{ + api_client::{ApiClient, ApiClientWrapper, RequestBuilder}, + types::Proxy, +}; use masking::PeekInterface; use reqwest::multipart::Form; use router_env::tracing_actix_web::RequestId; use super::{request::Maskable, Request}; -use crate::{ - core::errors::{ApiClientError, CustomResult}, - routes::SessionState, -}; - -pub trait RequestBuilder: Send + Sync { - fn json(&mut self, body: serde_json::Value); - fn url_encoded_form(&mut self, body: serde_json::Value); - fn timeout(&mut self, timeout: Duration); - fn multipart(&mut self, form: Form); - fn header(&mut self, key: String, value: Maskable) -> CustomResult<(), ApiClientError>; - fn send( - self, - ) -> CustomResult< - Box> + 'static>, - ApiClientError, - >; -} - -#[async_trait::async_trait] -pub trait ApiClient: dyn_clone::DynClone -where - Self: Send + Sync, -{ - fn request( - &self, - method: Method, - url: String, - ) -> CustomResult, ApiClientError>; - - fn request_with_certificate( - &self, - method: Method, - url: String, - certificate: Option>, - certificate_key: Option>, - ) -> CustomResult, ApiClientError>; - - async fn send_request( - &self, - state: &SessionState, - request: Request, - option_timeout_secs: Option, - forward_to_kafka: bool, - ) -> CustomResult; - - fn add_request_id(&mut self, request_id: RequestId); - - fn get_request_id(&self) -> Option; - - fn add_flow_name(&mut self, flow_name: String); -} - -dyn_clone::clone_trait_object!(ApiClient); +use crate::core::errors::{ApiClientError, CustomResult}; #[derive(Clone)] pub struct ProxyClient { proxy_config: Proxy, client: reqwest::Client, - request_id: Option, + request_id: Option, } impl ProxyClient { @@ -186,23 +136,26 @@ impl ApiClient for ProxyClient { } async fn send_request( &self, - state: &SessionState, + api_client: &dyn ApiClientWrapper, request: Request, option_timeout_secs: Option, _forward_to_kafka: bool, ) -> CustomResult { - http_client::send_request(&state.conf.proxy, request, option_timeout_secs) + http_client::send_request(&api_client.get_proxy(), request, option_timeout_secs) .await .switch() } fn add_request_id(&mut self, request_id: RequestId) { - self.request_id - .replace(request_id.as_hyphenated().to_string()); + self.request_id = Some(request_id); } - fn get_request_id(&self) -> Option { - self.request_id.clone() + fn get_request_id(&self) -> Option { + self.request_id + } + + fn get_request_id_str(&self) -> Option { + self.request_id.map(|id| id.as_hyphenated().to_string()) } fn add_flow_name(&mut self, _flow_name: String) {} @@ -236,7 +189,7 @@ impl ApiClient for MockApiClient { async fn send_request( &self, - _state: &SessionState, + _state: &dyn ApiClientWrapper, _request: Request, _option_timeout_secs: Option, _forward_to_kafka: bool, @@ -249,7 +202,12 @@ impl ApiClient for MockApiClient { // [#2066]: Add Mock implementation for ApiClient } - fn get_request_id(&self) -> Option { + fn get_request_id(&self) -> Option { + // [#2066]: Add Mock implementation for ApiClient + None + } + + fn get_request_id_str(&self) -> Option { // [#2066]: Add Mock implementation for ApiClient None } diff --git a/crates/router/src/services/kafka.rs b/crates/router/src/services/kafka.rs index 8a7613f344..c724016abe 100644 --- a/crates/router/src/services/kafka.rs +++ b/crates/router/src/services/kafka.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, sync::Arc}; -use common_utils::errors::CustomResult; +use common_utils::{errors::CustomResult, types::TenantConfig}; use error_stack::{report, ResultExt}; use events::{EventsError, Message, MessagingInterface}; use num_traits::ToPrimitive; @@ -10,7 +10,6 @@ use rdkafka::{ producer::{BaseRecord, DefaultProducerContext, Producer, ThreadedProducer}, }; use serde_json::Value; -use storage_impl::config::TenantConfig; #[cfg(feature = "payouts")] pub mod payout; use diesel_models::fraud_check::FraudCheck; diff --git a/crates/storage_impl/Cargo.toml b/crates/storage_impl/Cargo.toml index 2b77423fa4..8bc23f4dbb 100644 --- a/crates/storage_impl/Cargo.toml +++ b/crates/storage_impl/Cargo.toml @@ -8,9 +8,10 @@ readme = "README.md" license.workspace = true [features] -default = ["olap", "oltp"] +default = ["olap", "oltp", "accounts_cache"] dynamic_routing = [] oltp = [] +accounts_cache = [] olap = ["hyperswitch_domain_models/olap"] payouts = ["hyperswitch_domain_models/payouts"] v1 = ["api_models/v1", "diesel_models/v1", "hyperswitch_domain_models/v1", "common_utils/v1"] diff --git a/crates/storage_impl/src/business_profile.rs b/crates/storage_impl/src/business_profile.rs new file mode 100644 index 0000000000..add7ece931 --- /dev/null +++ b/crates/storage_impl/src/business_profile.rs @@ -0,0 +1,482 @@ +use common_utils::ext_traits::AsyncExt; +use diesel_models::business_profile::{self, ProfileUpdateInternal}; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + behaviour::{Conversion, ReverseConversion}, + business_profile as domain, + business_profile::ProfileInterface, + merchant_key_store::MerchantKeyStore, +}; +use router_env::{instrument, tracing}; + +use crate::{ + kv_router_store, + utils::{pg_accounts_connection_read, pg_accounts_connection_write}, + CustomResult, DatabaseStore, KeyManagerState, MockDb, RouterStore, StorageError, +}; + +#[async_trait::async_trait] +impl ProfileInterface for kv_router_store::KVRouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_business_profile( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + business_profile: domain::Profile, + ) -> CustomResult { + self.router_store + .insert_business_profile(key_manager_state, merchant_key_store, business_profile) + .await + } + + #[instrument(skip_all)] + async fn find_business_profile_by_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult { + self.router_store + .find_business_profile_by_profile_id(key_manager_state, merchant_key_store, profile_id) + .await + } + + async fn find_business_profile_by_merchant_id_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult { + self.router_store + .find_business_profile_by_merchant_id_profile_id( + key_manager_state, + merchant_key_store, + merchant_id, + profile_id, + ) + .await + } + + #[instrument(skip_all)] + async fn find_business_profile_by_profile_name_merchant_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_name: &str, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + self.router_store + .find_business_profile_by_profile_name_merchant_id( + key_manager_state, + merchant_key_store, + profile_name, + merchant_id, + ) + .await + } + + #[instrument(skip_all)] + async fn update_profile_by_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + current_state: domain::Profile, + profile_update: domain::ProfileUpdate, + ) -> CustomResult { + self.router_store + .update_profile_by_profile_id( + key_manager_state, + merchant_key_store, + current_state, + profile_update, + ) + .await + } + + #[instrument(skip_all)] + async fn delete_profile_by_profile_id_merchant_id( + &self, + profile_id: &common_utils::id_type::ProfileId, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + self.router_store + .delete_profile_by_profile_id_merchant_id(profile_id, merchant_id) + .await + } + + #[instrument(skip_all)] + async fn list_profile_by_merchant_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult, StorageError> { + self.router_store + .list_profile_by_merchant_id(key_manager_state, merchant_key_store, merchant_id) + .await + } +} + +#[async_trait::async_trait] +impl ProfileInterface for RouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_business_profile( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + business_profile: domain::Profile, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + business_profile + .construct_new() + .await + .change_context(StorageError::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[instrument(skip_all)] + async fn find_business_profile_by_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult { + let conn = pg_accounts_connection_read(self).await?; + self.call_database( + key_manager_state, + merchant_key_store, + business_profile::Profile::find_by_profile_id(&conn, profile_id), + ) + .await + } + + async fn find_business_profile_by_merchant_id_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult { + let conn = pg_accounts_connection_read(self).await?; + self.call_database( + key_manager_state, + merchant_key_store, + business_profile::Profile::find_by_merchant_id_profile_id( + &conn, + merchant_id, + profile_id, + ), + ) + .await + } + + #[instrument(skip_all)] + async fn find_business_profile_by_profile_name_merchant_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_name: &str, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + let conn = pg_accounts_connection_read(self).await?; + self.call_database( + key_manager_state, + merchant_key_store, + business_profile::Profile::find_by_profile_name_merchant_id( + &conn, + profile_name, + merchant_id, + ), + ) + .await + } + + #[instrument(skip_all)] + async fn update_profile_by_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + current_state: domain::Profile, + profile_update: domain::ProfileUpdate, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + Conversion::convert(current_state) + .await + .change_context(StorageError::EncryptionError)? + .update_by_profile_id(&conn, ProfileUpdateInternal::from(profile_update)) + .await + .map_err(|error| report!(StorageError::from(error)))? + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[instrument(skip_all)] + async fn delete_profile_by_profile_id_merchant_id( + &self, + profile_id: &common_utils::id_type::ProfileId, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + business_profile::Profile::delete_by_profile_id_merchant_id(&conn, profile_id, merchant_id) + .await + .map_err(|error| report!(StorageError::from(error))) + } + + #[instrument(skip_all)] + async fn list_profile_by_merchant_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult, StorageError> { + let conn = pg_accounts_connection_read(self).await?; + self.find_resources( + key_manager_state, + merchant_key_store, + business_profile::Profile::list_profile_by_merchant_id(&conn, merchant_id), + ) + .await + } +} + +#[async_trait::async_trait] +impl ProfileInterface for MockDb { + type Error = StorageError; + async fn insert_business_profile( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + business_profile: domain::Profile, + ) -> CustomResult { + let stored_business_profile = Conversion::convert(business_profile) + .await + .change_context(StorageError::EncryptionError)?; + + self.business_profiles + .lock() + .await + .push(stored_business_profile.clone()); + + stored_business_profile + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + async fn find_business_profile_by_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult { + self.business_profiles + .lock() + .await + .iter() + .find(|business_profile| business_profile.get_id() == profile_id) + .cloned() + .async_map(|business_profile| async { + business_profile + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + StorageError::ValueNotFound(format!( + "No business profile found for profile_id = {profile_id:?}" + )) + .into(), + ) + } + + async fn find_business_profile_by_merchant_id_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + profile_id: &common_utils::id_type::ProfileId, + ) -> CustomResult { + self.business_profiles + .lock() + .await + .iter() + .find(|business_profile| { + business_profile.merchant_id == *merchant_id + && business_profile.get_id() == profile_id + }) + .cloned() + .async_map(|business_profile| async { + business_profile + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + StorageError::ValueNotFound(format!( + "No business profile found for merchant_id = {merchant_id:?} and profile_id = {profile_id:?}" + )) + .into(), + ) + } + + async fn update_profile_by_profile_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + current_state: domain::Profile, + profile_update: domain::ProfileUpdate, + ) -> CustomResult { + let profile_id = current_state.get_id().to_owned(); + self.business_profiles + .lock() + .await + .iter_mut() + .find(|business_profile| business_profile.get_id() == current_state.get_id()) + .async_map(|business_profile| async { + let profile_updated = ProfileUpdateInternal::from(profile_update).apply_changeset( + Conversion::convert(current_state) + .await + .change_context(StorageError::EncryptionError)?, + ); + *business_profile = profile_updated.clone(); + + profile_updated + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + StorageError::ValueNotFound(format!( + "No business profile found for profile_id = {profile_id:?}", + )) + .into(), + ) + } + + async fn delete_profile_by_profile_id_merchant_id( + &self, + profile_id: &common_utils::id_type::ProfileId, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + let mut business_profiles = self.business_profiles.lock().await; + let index = business_profiles + .iter() + .position(|business_profile| { + business_profile.get_id() == profile_id + && business_profile.merchant_id == *merchant_id + }) + .ok_or::(StorageError::ValueNotFound(format!( + "No business profile found for profile_id = {profile_id:?} and merchant_id = {merchant_id:?}" + )))?; + business_profiles.remove(index); + Ok(true) + } + + async fn list_profile_by_merchant_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult, StorageError> { + let business_profiles = self + .business_profiles + .lock() + .await + .iter() + .filter(|business_profile| business_profile.merchant_id == *merchant_id) + .cloned() + .collect::>(); + + let mut domain_business_profiles = Vec::with_capacity(business_profiles.len()); + + for business_profile in business_profiles { + let domain_profile = business_profile + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError)?; + domain_business_profiles.push(domain_profile); + } + + Ok(domain_business_profiles) + } + + async fn find_business_profile_by_profile_name_merchant_id( + &self, + key_manager_state: &KeyManagerState, + merchant_key_store: &MerchantKeyStore, + profile_name: &str, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + self.business_profiles + .lock() + .await + .iter() + .find(|business_profile| { + business_profile.profile_name == profile_name + && business_profile.merchant_id == *merchant_id + }) + .cloned() + .async_map(|business_profile| async { + business_profile + .convert( + key_manager_state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + StorageError::ValueNotFound(format!( + "No business profile found for profile_name = {profile_name} and merchant_id = {merchant_id:?}" + + )) + .into(), + ) + } +} diff --git a/crates/storage_impl/src/config.rs b/crates/storage_impl/src/config.rs index 857c23161e..64a92348ce 100644 --- a/crates/storage_impl/src/config.rs +++ b/crates/storage_impl/src/config.rs @@ -1,4 +1,4 @@ -use common_utils::{id_type, DbConnectionParams}; +use common_utils::DbConnectionParams; use masking::Secret; #[derive(Debug, Clone, serde::Deserialize)] @@ -33,14 +33,6 @@ impl DbConnectionParams for Database { } } -pub trait TenantConfig: Send + Sync { - fn get_tenant_id(&self) -> &id_type::TenantId; - fn get_schema(&self) -> &str; - fn get_accounts_schema(&self) -> &str; - fn get_redis_key_prefix(&self) -> &str; - fn get_clickhouse_database(&self) -> &str; -} - #[derive(Debug, serde::Deserialize, Clone, Copy, Default)] #[serde(rename_all = "PascalCase")] pub enum QueueStrategy { diff --git a/crates/storage_impl/src/configs.rs b/crates/storage_impl/src/configs.rs new file mode 100644 index 0000000000..e1073df87a --- /dev/null +++ b/crates/storage_impl/src/configs.rs @@ -0,0 +1,285 @@ +use diesel_models::configs as storage; +use error_stack::report; +use hyperswitch_domain_models::configs::ConfigInterface; +use router_env::{instrument, tracing}; + +use crate::{ + connection, + errors::StorageError, + kv_router_store, + redis::{ + cache, + cache::{CacheKind, CONFIG_CACHE}, + }, + store::ConfigUpdateInternal, + CustomResult, DatabaseStore, MockDb, RouterStore, +}; + +#[async_trait::async_trait] +impl ConfigInterface for kv_router_store::KVRouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_config( + &self, + config: storage::ConfigNew, + ) -> CustomResult { + self.router_store.insert_config(config).await + } + + #[instrument(skip_all)] + async fn update_config_in_database( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.router_store + .update_config_in_database(key, config_update) + .await + } + + //update in DB and remove in redis and cache + #[instrument(skip_all)] + async fn update_config_by_key( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.router_store + .update_config_by_key(key, config_update) + .await + } + + #[instrument(skip_all)] + async fn find_config_by_key_from_db( + &self, + key: &str, + ) -> CustomResult { + self.router_store.find_config_by_key_from_db(key).await + } + + //check in cache, then redis then finally DB, and on the way back populate redis and cache + #[instrument(skip_all)] + async fn find_config_by_key(&self, key: &str) -> CustomResult { + self.router_store.find_config_by_key(key).await + } + + #[instrument(skip_all)] + async fn find_config_by_key_unwrap_or( + &self, + key: &str, + // If the config is not found it will be cached with the default value. + default_config: Option, + ) -> CustomResult { + self.router_store + .find_config_by_key_unwrap_or(key, default_config) + .await + } + + #[instrument(skip_all)] + async fn delete_config_by_key(&self, key: &str) -> CustomResult { + self.router_store.delete_config_by_key(key).await + } +} + +#[async_trait::async_trait] +impl ConfigInterface for RouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_config( + &self, + config: storage::ConfigNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + let inserted = config + .insert(&conn) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + cache::redact_from_redis_and_publish(self, [CacheKind::Config((&inserted.key).into())]) + .await?; + + Ok(inserted) + } + + #[instrument(skip_all)] + async fn update_config_in_database( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Config::update_by_key(&conn, key, config_update) + .await + .map_err(|error| report!(StorageError::from(error))) + } + + //update in DB and remove in redis and cache + #[instrument(skip_all)] + async fn update_config_by_key( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + cache::publish_and_redact(self, CacheKind::Config(key.into()), || { + self.update_config_in_database(key, config_update) + }) + .await + } + + #[instrument(skip_all)] + async fn find_config_by_key_from_db( + &self, + key: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Config::find_by_key(&conn, key) + .await + .map_err(|error| report!(StorageError::from(error))) + } + + //check in cache, then redis then finally DB, and on the way back populate redis and cache + #[instrument(skip_all)] + async fn find_config_by_key(&self, key: &str) -> CustomResult { + let find_config_by_key_from_db = || async { + let conn = connection::pg_connection_write(self).await?; + storage::Config::find_by_key(&conn, key) + .await + .map_err(|error| report!(StorageError::from(error))) + }; + cache::get_or_populate_in_memory(self, key, find_config_by_key_from_db, &CONFIG_CACHE).await + } + + #[instrument(skip_all)] + async fn find_config_by_key_unwrap_or( + &self, + key: &str, + // If the config is not found it will be cached with the default value. + default_config: Option, + ) -> CustomResult { + let find_else_unwrap_or = || async { + let conn = connection::pg_connection_write(self).await?; + match storage::Config::find_by_key(&conn, key) + .await + .map_err(|error| report!(StorageError::from(error))) + { + Ok(a) => Ok(a), + Err(err) => { + if err.current_context().is_db_not_found() { + default_config + .map(|c| { + storage::ConfigNew { + key: key.to_string(), + config: c, + } + .into() + }) + .ok_or(err) + } else { + Err(err) + } + } + } + }; + + cache::get_or_populate_in_memory(self, key, find_else_unwrap_or, &CONFIG_CACHE).await + } + + #[instrument(skip_all)] + async fn delete_config_by_key(&self, key: &str) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + let deleted = storage::Config::delete_by_key(&conn, key) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + cache::redact_from_redis_and_publish(self, [CacheKind::Config((&deleted.key).into())]) + .await?; + + Ok(deleted) + } +} + +#[async_trait::async_trait] +impl ConfigInterface for MockDb { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_config( + &self, + config: storage::ConfigNew, + ) -> CustomResult { + let mut configs = self.configs.lock().await; + + let config_new = storage::Config { + key: config.key, + config: config.config, + }; + configs.push(config_new.clone()); + Ok(config_new) + } + + async fn update_config_in_database( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + self.update_config_by_key(key, config_update).await + } + + async fn update_config_by_key( + &self, + key: &str, + config_update: storage::ConfigUpdate, + ) -> CustomResult { + let result = self + .configs + .lock() + .await + .iter_mut() + .find(|c| c.key == key) + .ok_or_else(|| { + StorageError::ValueNotFound("cannot find config to update".to_string()).into() + }) + .map(|c| { + let config_updated = + ConfigUpdateInternal::from(config_update).create_config(c.clone()); + *c = config_updated.clone(); + config_updated + }); + + result + } + + async fn delete_config_by_key(&self, key: &str) -> CustomResult { + let mut configs = self.configs.lock().await; + let result = configs + .iter() + .position(|c| c.key == key) + .map(|index| configs.remove(index)) + .ok_or_else(|| { + StorageError::ValueNotFound("cannot find config to delete".to_string()).into() + }); + + result + } + + async fn find_config_by_key(&self, key: &str) -> CustomResult { + let configs = self.configs.lock().await; + let config = configs.iter().find(|c| c.key == key).cloned(); + + config.ok_or_else(|| StorageError::ValueNotFound("cannot find config".to_string()).into()) + } + + async fn find_config_by_key_unwrap_or( + &self, + key: &str, + _default_config: Option, + ) -> CustomResult { + self.find_config_by_key(key).await + } + + async fn find_config_by_key_from_db( + &self, + key: &str, + ) -> CustomResult { + self.find_config_by_key(key).await + } +} diff --git a/crates/storage_impl/src/database/store.rs b/crates/storage_impl/src/database/store.rs index 6761ec7678..9caa84be48 100644 --- a/crates/storage_impl/src/database/store.rs +++ b/crates/storage_impl/src/database/store.rs @@ -1,11 +1,11 @@ use async_bb8_diesel::{AsyncConnection, ConnectionError}; use bb8::CustomizeConnection; -use common_utils::DbConnectionParams; +use common_utils::{types::TenantConfig, DbConnectionParams}; use diesel::PgConnection; use error_stack::ResultExt; use crate::{ - config::{Database, TenantConfig}, + config::Database, errors::{StorageError, StorageResult}, }; diff --git a/crates/storage_impl/src/kv_router_store.rs b/crates/storage_impl/src/kv_router_store.rs index 7827f3a2a5..7ad2763c8f 100644 --- a/crates/storage_impl/src/kv_router_store.rs +++ b/crates/storage_impl/src/kv_router_store.rs @@ -17,8 +17,8 @@ use serde::de; #[cfg(not(feature = "payouts"))] pub use crate::database::store::Store; +pub use crate::{database::store::DatabaseStore, mock_db::MockDb}; use crate::{ - config::TenantConfig, database::store::PgPool, diesel_error_to_data_error, errors::{self, RedisErrorExt, StorageResult}, @@ -29,9 +29,8 @@ use crate::{ RedisConnInterface, }, utils::{find_all_combined_kv_database, try_redis_get_else_try_database_get}, - RouterStore, UniqueConstraints, + RouterStore, TenantConfig, UniqueConstraints, }; -pub use crate::{database::store::DatabaseStore, mock_db::MockDb}; #[derive(Debug, Clone)] pub struct KVRouterStore { diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 5e6483fd1d..691a010b1c 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -1,5 +1,6 @@ use std::{fmt::Debug, sync::Arc}; +use common_utils::types::TenantConfig; use diesel_models as store; use error_stack::ResultExt; use hyperswitch_domain_models::{ @@ -9,9 +10,11 @@ use hyperswitch_domain_models::{ use masking::StrongSecret; use redis::{kv_store::RedisConnInterface, pub_sub::PubSubInterface, RedisStore}; mod address; +pub mod business_profile; pub mod callback_mapper; pub mod cards_info; pub mod config; +pub mod configs; pub mod connection; pub mod customers; pub mod database; @@ -20,6 +23,9 @@ pub mod invoice; pub mod kv_router_store; pub mod lookup; pub mod mandate; +pub mod merchant_account; +pub mod merchant_connector_account; +pub mod merchant_key_store; pub mod metrics; pub mod mock_db; pub mod payment_method; @@ -66,7 +72,7 @@ where ); async fn new( config: Self::Config, - tenant_config: &dyn config::TenantConfig, + tenant_config: &dyn TenantConfig, test_transaction: bool, ) -> error_stack::Result { let (db_conf, cache_conf, encryption_key, cache_error_signal, inmemory_cache_stream) = @@ -112,7 +118,7 @@ impl RedisConnInterface for RouterStore { impl RouterStore { pub async fn from_config( db_conf: T::Config, - tenant_config: &dyn config::TenantConfig, + tenant_config: &dyn TenantConfig, encryption_key: StrongSecret>, cache_store: Arc, inmemory_cache_stream: &str, @@ -256,7 +262,7 @@ impl RouterStore { /// Will panic if `CONNECTOR_AUTH_FILE_PATH` is not set pub async fn test_store( db_conf: T::Config, - tenant_config: &dyn config::TenantConfig, + tenant_config: &dyn TenantConfig, cache_conf: &redis_interface::RedisSettings, encryption_key: StrongSecret>, ) -> error_stack::Result { diff --git a/crates/storage_impl/src/merchant_account.rs b/crates/storage_impl/src/merchant_account.rs new file mode 100644 index 0000000000..79f2687396 --- /dev/null +++ b/crates/storage_impl/src/merchant_account.rs @@ -0,0 +1,874 @@ +#[cfg(feature = "olap")] +use std::collections::HashMap; + +use common_utils::ext_traits::AsyncExt; +use diesel_models::merchant_account as storage; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + behaviour::{Conversion, ReverseConversion}, + merchant_account::{self as domain, MerchantAccountInterface}, + merchant_key_store::{MerchantKeyStore, MerchantKeyStoreInterface}, +}; +use masking::PeekInterface; +use router_env::{instrument, tracing}; + +#[cfg(feature = "accounts_cache")] +use crate::redis::{ + cache, + cache::{CacheKind, ACCOUNTS_CACHE}, +}; +#[cfg(feature = "accounts_cache")] +use crate::RedisConnInterface; +use crate::{ + kv_router_store, + store::MerchantAccountUpdateInternal, + utils::{pg_accounts_connection_read, pg_accounts_connection_write}, + CustomResult, DatabaseStore, KeyManagerState, MockDb, RouterStore, StorageError, +}; + +#[async_trait::async_trait] +impl MerchantAccountInterface for kv_router_store::KVRouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_merchant( + &self, + state: &KeyManagerState, + merchant_account: domain::MerchantAccount, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .insert_merchant(state, merchant_account, merchant_key_store) + .await + } + + #[instrument(skip_all)] + async fn find_merchant_account_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .find_merchant_account_by_merchant_id(state, merchant_id, merchant_key_store) + .await + } + + #[instrument(skip_all)] + async fn update_merchant( + &self, + state: &KeyManagerState, + this: domain::MerchantAccount, + merchant_account: domain::MerchantAccountUpdate, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .update_merchant(state, this, merchant_account, merchant_key_store) + .await + } + + #[instrument(skip_all)] + async fn update_specific_fields_in_merchant( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_account: domain::MerchantAccountUpdate, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .update_specific_fields_in_merchant( + state, + merchant_id, + merchant_account, + merchant_key_store, + ) + .await + } + + #[instrument(skip_all)] + async fn find_merchant_account_by_publishable_key( + &self, + state: &KeyManagerState, + publishable_key: &str, + ) -> CustomResult<(domain::MerchantAccount, MerchantKeyStore), StorageError> { + self.router_store + .find_merchant_account_by_publishable_key(state, publishable_key) + .await + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_merchant_accounts_by_organization_id( + &self, + state: &KeyManagerState, + organization_id: &common_utils::id_type::OrganizationId, + ) -> CustomResult, StorageError> { + self.router_store + .list_merchant_accounts_by_organization_id(state, organization_id) + .await + } + + #[instrument(skip_all)] + async fn delete_merchant_account_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + self.router_store + .delete_merchant_account_by_merchant_id(merchant_id) + .await + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_multiple_merchant_accounts( + &self, + state: &KeyManagerState, + merchant_ids: Vec, + ) -> CustomResult, StorageError> { + self.router_store + .list_multiple_merchant_accounts(state, merchant_ids) + .await + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_merchant_and_org_ids( + &self, + _state: &KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult< + Vec<( + common_utils::id_type::MerchantId, + common_utils::id_type::OrganizationId, + )>, + StorageError, + > { + self.router_store + .list_merchant_and_org_ids(_state, limit, offset) + .await + } + + async fn update_all_merchant_account( + &self, + merchant_account: domain::MerchantAccountUpdate, + ) -> CustomResult { + self.router_store + .update_all_merchant_account(merchant_account) + .await + } +} + +#[async_trait::async_trait] +impl MerchantAccountInterface for RouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_merchant( + &self, + state: &KeyManagerState, + merchant_account: domain::MerchantAccount, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + merchant_account + .construct_new() + .await + .change_context(StorageError::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(StorageError::from(error)))? + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[instrument(skip_all)] + async fn find_merchant_account_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let fetch_func = || async { + let conn = pg_accounts_connection_read(self).await?; + storage::MerchantAccount::find_by_merchant_id(&conn, merchant_id) + .await + .map_err(|error| report!(StorageError::from(error))) + }; + + #[cfg(not(feature = "accounts_cache"))] + { + fetch_func() + .await? + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_id.to_owned().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[cfg(feature = "accounts_cache")] + { + cache::get_or_populate_in_memory( + self, + merchant_id.get_string_repr(), + fetch_func, + &ACCOUNTS_CACHE, + ) + .await? + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + } + + #[instrument(skip_all)] + async fn update_merchant( + &self, + state: &KeyManagerState, + this: domain::MerchantAccount, + merchant_account: domain::MerchantAccountUpdate, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + + let updated_merchant_account = Conversion::convert(this) + .await + .change_context(StorageError::EncryptionError)? + .update(&conn, merchant_account.into()) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + #[cfg(feature = "accounts_cache")] + { + publish_and_redact_merchant_account_cache(self, &updated_merchant_account).await?; + } + updated_merchant_account + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[instrument(skip_all)] + async fn update_specific_fields_in_merchant( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_account: domain::MerchantAccountUpdate, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + let updated_merchant_account = storage::MerchantAccount::update_with_specific_fields( + &conn, + merchant_id, + merchant_account.into(), + ) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + #[cfg(feature = "accounts_cache")] + { + publish_and_redact_merchant_account_cache(self, &updated_merchant_account).await?; + } + updated_merchant_account + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[instrument(skip_all)] + async fn find_merchant_account_by_publishable_key( + &self, + state: &KeyManagerState, + publishable_key: &str, + ) -> CustomResult<(domain::MerchantAccount, MerchantKeyStore), StorageError> { + let fetch_by_pub_key_func = || async { + let conn = pg_accounts_connection_read(self).await?; + + storage::MerchantAccount::find_by_publishable_key(&conn, publishable_key) + .await + .map_err(|error| report!(StorageError::from(error))) + }; + + let merchant_account; + #[cfg(not(feature = "accounts_cache"))] + { + merchant_account = fetch_by_pub_key_func().await?; + } + + #[cfg(feature = "accounts_cache")] + { + merchant_account = cache::get_or_populate_in_memory( + self, + publishable_key, + fetch_by_pub_key_func, + &ACCOUNTS_CACHE, + ) + .await?; + } + let key_store = self + .get_merchant_key_store_by_merchant_id( + state, + merchant_account.get_id(), + &self.master_key().peek().to_vec().into(), + ) + .await?; + let domain_merchant_account = merchant_account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError)?; + Ok((domain_merchant_account, key_store)) + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_merchant_accounts_by_organization_id( + &self, + state: &KeyManagerState, + organization_id: &common_utils::id_type::OrganizationId, + ) -> CustomResult, StorageError> { + use futures::future::try_join_all; + let conn = pg_accounts_connection_read(self).await?; + + let encrypted_merchant_accounts = + storage::MerchantAccount::list_by_organization_id(&conn, organization_id) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + let db_master_key = self.master_key().peek().to_vec().into(); + + let merchant_key_stores = + try_join_all(encrypted_merchant_accounts.iter().map(|merchant_account| { + self.get_merchant_key_store_by_merchant_id( + state, + merchant_account.get_id(), + &db_master_key, + ) + })) + .await?; + + let merchant_accounts = try_join_all( + encrypted_merchant_accounts + .into_iter() + .zip(merchant_key_stores.iter()) + .map(|(merchant_account, key_store)| async { + merchant_account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }), + ) + .await?; + + Ok(merchant_accounts) + } + + #[instrument(skip_all)] + async fn delete_merchant_account_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + + let is_deleted_func = || async { + storage::MerchantAccount::delete_by_merchant_id(&conn, merchant_id) + .await + .map_err(|error| report!(StorageError::from(error))) + }; + + let is_deleted; + + #[cfg(not(feature = "accounts_cache"))] + { + is_deleted = is_deleted_func().await?; + } + + #[cfg(feature = "accounts_cache")] + { + let merchant_account = + storage::MerchantAccount::find_by_merchant_id(&conn, merchant_id) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + is_deleted = is_deleted_func().await?; + + publish_and_redact_merchant_account_cache(self, &merchant_account).await?; + } + + Ok(is_deleted) + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_multiple_merchant_accounts( + &self, + state: &KeyManagerState, + merchant_ids: Vec, + ) -> CustomResult, StorageError> { + let conn = pg_accounts_connection_read(self).await?; + + let encrypted_merchant_accounts = + storage::MerchantAccount::list_multiple_merchant_accounts(&conn, merchant_ids) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + let db_master_key = self.master_key().peek().to_vec().into(); + + let merchant_key_stores = self + .list_multiple_key_stores( + state, + encrypted_merchant_accounts + .iter() + .map(|merchant_account| merchant_account.get_id()) + .cloned() + .collect(), + &db_master_key, + ) + .await?; + + let key_stores_by_id: HashMap<_, _> = merchant_key_stores + .iter() + .map(|key_store| (key_store.merchant_id.to_owned(), key_store)) + .collect(); + + let merchant_accounts = + futures::future::try_join_all(encrypted_merchant_accounts.into_iter().map( + |merchant_account| async { + let key_store = key_stores_by_id.get(merchant_account.get_id()).ok_or( + StorageError::ValueNotFound(format!( + "merchant_key_store with merchant_id = {:?}", + merchant_account.get_id() + )), + )?; + merchant_account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }, + )) + .await?; + + Ok(merchant_accounts) + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_merchant_and_org_ids( + &self, + _state: &KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult< + Vec<( + common_utils::id_type::MerchantId, + common_utils::id_type::OrganizationId, + )>, + StorageError, + > { + let conn = pg_accounts_connection_read(self).await?; + let encrypted_merchant_accounts = + storage::MerchantAccount::list_all_merchant_accounts(&conn, limit, offset) + .await + .map_err(|error| report!(StorageError::from(error)))?; + + let merchant_and_org_ids = encrypted_merchant_accounts + .into_iter() + .map(|merchant_account| { + let merchant_id = merchant_account.get_id().clone(); + let org_id = merchant_account.organization_id; + (merchant_id, org_id) + }) + .collect(); + Ok(merchant_and_org_ids) + } + + async fn update_all_merchant_account( + &self, + merchant_account: domain::MerchantAccountUpdate, + ) -> CustomResult { + let conn = pg_accounts_connection_read(self).await?; + + let db_func = || async { + storage::MerchantAccount::update_all_merchant_accounts( + &conn, + MerchantAccountUpdateInternal::from(merchant_account), + ) + .await + .map_err(|error| report!(StorageError::from(error))) + }; + + let total; + #[cfg(not(feature = "accounts_cache"))] + { + let ma = db_func().await?; + total = ma.len(); + } + + #[cfg(feature = "accounts_cache")] + { + let ma = db_func().await?; + publish_and_redact_all_merchant_account_cache(self, &ma).await?; + total = ma.len(); + } + + Ok(total) + } +} + +#[async_trait::async_trait] +impl MerchantAccountInterface for MockDb { + type Error = StorageError; + #[allow(clippy::panic)] + async fn insert_merchant( + &self, + state: &KeyManagerState, + merchant_account: domain::MerchantAccount, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let mut accounts = self.merchant_accounts.lock().await; + let account = Conversion::convert(merchant_account) + .await + .change_context(StorageError::EncryptionError)?; + accounts.push(account.clone()); + + account + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[allow(clippy::panic)] + async fn find_merchant_account_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let accounts = self.merchant_accounts.lock().await; + accounts + .iter() + .find(|account| account.get_id() == merchant_id) + .cloned() + .ok_or(StorageError::ValueNotFound(format!( + "Merchant ID: {merchant_id:?} not found", + )))? + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + async fn update_merchant( + &self, + state: &KeyManagerState, + merchant_account: domain::MerchantAccount, + merchant_account_update: domain::MerchantAccountUpdate, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let merchant_id = merchant_account.get_id().to_owned(); + let mut accounts = self.merchant_accounts.lock().await; + accounts + .iter_mut() + .find(|account| account.get_id() == merchant_account.get_id()) + .async_map(|account| async { + let update = MerchantAccountUpdateInternal::from(merchant_account_update) + .apply_changeset( + Conversion::convert(merchant_account) + .await + .change_context(StorageError::EncryptionError)?, + ); + *account = update.clone(); + update + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + StorageError::ValueNotFound(format!("Merchant ID: {merchant_id:?} not found",)) + .into(), + ) + } + + async fn update_specific_fields_in_merchant( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_account_update: domain::MerchantAccountUpdate, + merchant_key_store: &MerchantKeyStore, + ) -> CustomResult { + let mut accounts = self.merchant_accounts.lock().await; + accounts + .iter_mut() + .find(|account| account.get_id() == merchant_id) + .async_map(|account| async { + let update = MerchantAccountUpdateInternal::from(merchant_account_update) + .apply_changeset(account.clone()); + *account = update.clone(); + update + .convert( + state, + merchant_key_store.key.get_inner(), + merchant_key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + .transpose()? + .ok_or( + StorageError::ValueNotFound(format!("Merchant ID: {merchant_id:?} not found",)) + .into(), + ) + } + + async fn find_merchant_account_by_publishable_key( + &self, + state: &KeyManagerState, + publishable_key: &str, + ) -> CustomResult<(domain::MerchantAccount, MerchantKeyStore), StorageError> { + let accounts = self.merchant_accounts.lock().await; + let account = accounts + .iter() + .find(|account| { + account + .publishable_key + .as_ref() + .is_some_and(|key| key == publishable_key) + }) + .ok_or(StorageError::ValueNotFound(format!( + "Publishable Key: {publishable_key} not found", + )))?; + let key_store = self + .get_merchant_key_store_by_merchant_id( + state, + account.get_id(), + &self.get_master_key().to_vec().into(), + ) + .await?; + let merchant_account = account + .clone() + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError)?; + Ok((merchant_account, key_store)) + } + + async fn update_all_merchant_account( + &self, + merchant_account_update: domain::MerchantAccountUpdate, + ) -> CustomResult { + let mut accounts = self.merchant_accounts.lock().await; + Ok(accounts.iter_mut().fold(0, |acc, account| { + let update = MerchantAccountUpdateInternal::from(merchant_account_update.clone()) + .apply_changeset(account.clone()); + *account = update; + acc + 1 + })) + } + + async fn delete_merchant_account_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + let mut accounts = self.merchant_accounts.lock().await; + accounts.retain(|x| x.get_id() != merchant_id); + Ok(true) + } + + #[cfg(feature = "olap")] + async fn list_merchant_accounts_by_organization_id( + &self, + state: &KeyManagerState, + organization_id: &common_utils::id_type::OrganizationId, + ) -> CustomResult, StorageError> { + let accounts = self.merchant_accounts.lock().await; + let futures = accounts + .iter() + .filter(|account| account.organization_id == *organization_id) + .map(|account| async { + let key_store = self + .get_merchant_key_store_by_merchant_id( + state, + account.get_id(), + &self.get_master_key().to_vec().into(), + ) + .await; + match key_store { + Ok(key) => account + .clone() + .convert(state, key.key.get_inner(), key.merchant_id.clone().into()) + .await + .change_context(StorageError::DecryptionError), + Err(err) => Err(err), + } + }); + futures::future::join_all(futures) + .await + .into_iter() + .collect() + } + + #[cfg(feature = "olap")] + async fn list_multiple_merchant_accounts( + &self, + state: &KeyManagerState, + merchant_ids: Vec, + ) -> CustomResult, StorageError> { + let accounts = self.merchant_accounts.lock().await; + let futures = accounts + .iter() + .filter(|account| merchant_ids.contains(account.get_id())) + .map(|account| async { + let key_store = self + .get_merchant_key_store_by_merchant_id( + state, + account.get_id(), + &self.get_master_key().to_vec().into(), + ) + .await; + match key_store { + Ok(key) => account + .clone() + .convert(state, key.key.get_inner(), key.merchant_id.clone().into()) + .await + .change_context(StorageError::DecryptionError), + Err(err) => Err(err), + } + }); + futures::future::join_all(futures) + .await + .into_iter() + .collect() + } + + #[cfg(feature = "olap")] + async fn list_merchant_and_org_ids( + &self, + _state: &KeyManagerState, + limit: u32, + offset: Option, + ) -> CustomResult< + Vec<( + common_utils::id_type::MerchantId, + common_utils::id_type::OrganizationId, + )>, + StorageError, + > { + let accounts = self.merchant_accounts.lock().await; + let limit = limit.try_into().unwrap_or(accounts.len()); + let offset = offset.unwrap_or(0).try_into().unwrap_or(0); + + let merchant_and_org_ids = accounts + .iter() + .skip(offset) + .take(limit) + .map(|account| (account.get_id().clone(), account.organization_id.clone())) + .collect::>(); + + Ok(merchant_and_org_ids) + } +} + +#[cfg(feature = "accounts_cache")] +async fn publish_and_redact_merchant_account_cache( + store: &(dyn RedisConnInterface + Send + Sync), + merchant_account: &storage::MerchantAccount, +) -> CustomResult<(), StorageError> { + let publishable_key = merchant_account + .publishable_key + .as_ref() + .map(|publishable_key| CacheKind::Accounts(publishable_key.into())); + + #[cfg(feature = "v1")] + let cgraph_key = merchant_account.default_profile.as_ref().map(|profile_id| { + CacheKind::CGraph( + format!( + "cgraph_{}_{}", + merchant_account.get_id().get_string_repr(), + profile_id.get_string_repr(), + ) + .into(), + ) + }); + + // TODO: we will not have default profile in v2 + #[cfg(feature = "v2")] + let cgraph_key = None; + + let mut cache_keys = vec![CacheKind::Accounts( + merchant_account.get_id().get_string_repr().into(), + )]; + + cache_keys.extend(publishable_key.into_iter()); + cache_keys.extend(cgraph_key.into_iter()); + + cache::redact_from_redis_and_publish(store, cache_keys).await?; + Ok(()) +} + +#[cfg(feature = "accounts_cache")] +async fn publish_and_redact_all_merchant_account_cache( + cache: &(dyn RedisConnInterface + Send + Sync), + merchant_accounts: &[storage::MerchantAccount], +) -> CustomResult<(), StorageError> { + let merchant_ids = merchant_accounts + .iter() + .map(|merchant_account| merchant_account.get_id().get_string_repr().to_string()); + let publishable_keys = merchant_accounts + .iter() + .filter_map(|m| m.publishable_key.clone()); + + let cache_keys: Vec> = merchant_ids + .chain(publishable_keys) + .map(|s| CacheKind::Accounts(s.into())) + .collect(); + + cache::redact_from_redis_and_publish(cache, cache_keys).await?; + Ok(()) +} diff --git a/crates/storage_impl/src/merchant_connector_account.rs b/crates/storage_impl/src/merchant_connector_account.rs new file mode 100644 index 0000000000..c8f09748da --- /dev/null +++ b/crates/storage_impl/src/merchant_connector_account.rs @@ -0,0 +1,1531 @@ +use async_bb8_diesel::AsyncConnection; +use common_utils::{encryption::Encryption, ext_traits::AsyncExt}; +use diesel_models::merchant_connector_account as storage; +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + behaviour::{Conversion, ReverseConversion}, + merchant_connector_account::{self as domain, MerchantConnectorAccountInterface}, + merchant_key_store::MerchantKeyStore, +}; +use router_env::{instrument, tracing}; + +#[cfg(feature = "accounts_cache")] +use crate::redis::cache; +use crate::{ + kv_router_store, + utils::{pg_accounts_connection_read, pg_accounts_connection_write}, + CustomResult, DatabaseStore, KeyManagerState, MockDb, RouterStore, StorageError, +}; + +#[async_trait::async_trait] +impl MerchantConnectorAccountInterface for kv_router_store::KVRouterStore { + type Error = StorageError; + #[cfg(feature = "v1")] + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_merchant_id_connector_label( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + connector_label: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .find_merchant_connector_account_by_merchant_id_connector_label( + state, + merchant_id, + connector_label, + key_store, + ) + .await + } + + #[cfg(feature = "v1")] + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_profile_id_connector_name( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .find_merchant_connector_account_by_profile_id_connector_name( + state, + profile_id, + connector_name, + key_store, + ) + .await + } + + #[cfg(feature = "v1")] + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_merchant_id_connector_name( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult, Self::Error> { + self.router_store + .find_merchant_connector_account_by_merchant_id_connector_name( + state, + merchant_id, + connector_name, + key_store, + ) + .await + } + + #[instrument(skip_all)] + #[cfg(feature = "v1")] + async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .find_by_merchant_connector_account_merchant_id_merchant_connector_id( + state, + merchant_id, + merchant_connector_id, + key_store, + ) + .await + } + + #[instrument(skip_all)] + #[cfg(feature = "v2")] + async fn find_merchant_connector_account_by_id( + &self, + state: &KeyManagerState, + id: &common_utils::id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .find_merchant_connector_account_by_id(state, id, key_store) + .await + } + + #[instrument(skip_all)] + async fn insert_merchant_connector_account( + &self, + state: &KeyManagerState, + t: domain::MerchantConnectorAccount, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .insert_merchant_connector_account(state, t, key_store) + .await + } + + async fn list_enabled_connector_accounts_by_profile_id( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + key_store: &MerchantKeyStore, + connector_type: common_enums::ConnectorType, + ) -> CustomResult, Self::Error> { + self.router_store + .list_enabled_connector_accounts_by_profile_id( + state, + profile_id, + key_store, + connector_type, + ) + .await + } + + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + get_disabled: bool, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .find_merchant_connector_account_by_merchant_id_and_disabled_list( + state, + merchant_id, + get_disabled, + key_store, + ) + .await + } + + #[instrument(skip_all)] + #[cfg(all(feature = "olap", feature = "v2"))] + async fn list_connector_account_by_profile_id( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + key_store: &MerchantKeyStore, + ) -> CustomResult, Self::Error> { + self.router_store + .list_connector_account_by_profile_id(state, profile_id, key_store) + .await + } + + #[instrument(skip_all)] + async fn update_multiple_merchant_connector_accounts( + &self, + merchant_connector_accounts: Vec<( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), Self::Error> { + self.router_store + .update_multiple_merchant_connector_accounts(merchant_connector_accounts) + .await + } + + #[instrument(skip_all)] + #[cfg(feature = "v1")] + async fn update_merchant_connector_account( + &self, + state: &KeyManagerState, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.router_store + .update_merchant_connector_account(state, this, merchant_connector_account, key_store) + .await + } + + #[instrument(skip_all)] + #[cfg(feature = "v2")] + async fn update_merchant_connector_account( + &self, + state: &KeyManagerState, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &MerchantKeyStore, + ) -> CustomResult { + self.update_merchant_connector_account(state, this, merchant_connector_account, key_store) + .await + } + + #[instrument(skip_all)] + #[cfg(feature = "v1")] + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, + ) -> CustomResult { + self.router_store + .delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + merchant_id, + merchant_connector_id, + ) + .await + } + + #[instrument(skip_all)] + #[cfg(feature = "v2")] + async fn delete_merchant_connector_account_by_id( + &self, + id: &common_utils::id_type::MerchantConnectorAccountId, + ) -> CustomResult { + self.router_store + .delete_merchant_connector_account_by_id(id) + .await + } +} + +#[async_trait::async_trait] +impl MerchantConnectorAccountInterface for RouterStore { + type Error = StorageError; + #[cfg(feature = "v1")] + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_merchant_id_connector_label( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + connector_label: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let find_call = || async { + let conn = pg_accounts_connection_read(self).await?; + storage::MerchantConnectorAccount::find_by_merchant_id_connector( + &conn, + merchant_id, + connector_label, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(not(feature = "accounts_cache"))] + { + find_call() + .await? + .convert(state, key_store.key.get_inner(), merchant_id.clone().into()) + .await + .change_context(Self::Error::DeserializationFailed) + } + + #[cfg(feature = "accounts_cache")] + { + cache::get_or_populate_in_memory( + self, + &format!("{}_{}", merchant_id.get_string_repr(), connector_label), + find_call, + &cache::ACCOUNTS_CACHE, + ) + .await + .async_and_then(|item| async { + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError) + }) + .await + } + } + + #[cfg(feature = "v1")] + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_profile_id_connector_name( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let find_call = || async { + let conn = pg_accounts_connection_read(self).await?; + storage::MerchantConnectorAccount::find_by_profile_id_connector_name( + &conn, + profile_id, + connector_name, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(not(feature = "accounts_cache"))] + { + find_call() + .await? + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DeserializationFailed) + } + + #[cfg(feature = "accounts_cache")] + { + cache::get_or_populate_in_memory( + self, + &format!("{}_{}", profile_id.get_string_repr(), connector_name), + find_call, + &cache::ACCOUNTS_CACHE, + ) + .await + .async_and_then(|item| async { + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError) + }) + .await + } + } + + #[cfg(feature = "v1")] + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_merchant_id_connector_name( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult, Self::Error> { + let conn = pg_accounts_connection_read(self).await?; + storage::MerchantConnectorAccount::find_by_merchant_id_connector_name( + &conn, + merchant_id, + connector_name, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + .async_and_then(|items| async { + let mut output = Vec::with_capacity(items.len()); + for item in items.into_iter() { + output.push( + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError)?, + ) + } + Ok(output) + }) + .await + } + + #[instrument(skip_all)] + #[cfg(feature = "v1")] + async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let find_call = || async { + let conn = pg_accounts_connection_read(self).await?; + storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( + &conn, + merchant_id, + merchant_connector_id, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(not(feature = "accounts_cache"))] + { + find_call() + .await? + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError) + } + + #[cfg(feature = "accounts_cache")] + { + cache::get_or_populate_in_memory( + self, + &format!( + "{}_{}", + merchant_id.get_string_repr(), + merchant_connector_id.get_string_repr() + ), + find_call, + &cache::ACCOUNTS_CACHE, + ) + .await? + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError) + } + } + + #[instrument(skip_all)] + #[cfg(feature = "v2")] + async fn find_merchant_connector_account_by_id( + &self, + state: &KeyManagerState, + id: &common_utils::id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let find_call = || async { + let conn = pg_accounts_connection_read(self).await?; + storage::MerchantConnectorAccount::find_by_id(&conn, id) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(not(feature = "accounts_cache"))] + { + find_call() + .await? + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone(), + ) + .await + .change_context(Self::Error::DecryptionError) + } + + #[cfg(feature = "accounts_cache")] + { + cache::get_or_populate_in_memory( + self, + id.get_string_repr(), + find_call, + &cache::ACCOUNTS_CACHE, + ) + .await? + .convert( + state, + key_store.key.get_inner(), + common_utils::types::keymanager::Identifier::Merchant( + key_store.merchant_id.clone(), + ), + ) + .await + .change_context(Self::Error::DecryptionError) + } + } + + #[instrument(skip_all)] + async fn insert_merchant_connector_account( + &self, + state: &KeyManagerState, + t: domain::MerchantConnectorAccount, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + t.construct_new() + .await + .change_context(Self::Error::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(Self::Error::from(error))) + .async_and_then(|item| async { + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError) + }) + .await + } + + async fn list_enabled_connector_accounts_by_profile_id( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + key_store: &MerchantKeyStore, + connector_type: common_enums::ConnectorType, + ) -> CustomResult, Self::Error> { + let conn = pg_accounts_connection_read(self).await?; + + storage::MerchantConnectorAccount::list_enabled_by_profile_id( + &conn, + profile_id, + connector_type, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + .async_and_then(|items| async { + let mut output = Vec::with_capacity(items.len()); + for item in items.into_iter() { + output.push( + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError)?, + ) + } + Ok(output) + }) + .await + } + + #[instrument(skip_all)] + async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + get_disabled: bool, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let conn = pg_accounts_connection_read(self).await?; + let merchant_connector_account_vec = + storage::MerchantConnectorAccount::find_by_merchant_id( + &conn, + merchant_id, + get_disabled, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + .async_and_then(|items| async { + let mut output = Vec::with_capacity(items.len()); + for item in items.into_iter() { + output.push( + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError)?, + ) + } + Ok(output) + }) + .await?; + Ok(domain::MerchantConnectorAccounts::new( + merchant_connector_account_vec, + )) + } + + #[instrument(skip_all)] + #[cfg(all(feature = "olap", feature = "v2"))] + async fn list_connector_account_by_profile_id( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + key_store: &MerchantKeyStore, + ) -> CustomResult, Self::Error> { + let conn = pg_accounts_connection_read(self).await?; + storage::MerchantConnectorAccount::list_by_profile_id(&conn, profile_id) + .await + .map_err(|error| report!(Self::Error::from(error))) + .async_and_then(|items| async { + let mut output = Vec::with_capacity(items.len()); + for item in items.into_iter() { + output.push( + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError)?, + ) + } + Ok(output) + }) + .await + } + + #[instrument(skip_all)] + async fn update_multiple_merchant_connector_accounts( + &self, + merchant_connector_accounts: Vec<( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), Self::Error> { + let conn = pg_accounts_connection_write(self).await?; + + async fn update_call( + connection: &diesel_models::PgPooledConn, + (merchant_connector_account, mca_update): ( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + ), + ) -> Result<(), error_stack::Report> { + Conversion::convert(merchant_connector_account) + .await + .change_context(StorageError::EncryptionError)? + .update(connection, mca_update) + .await + .map_err(|error| report!(StorageError::from(error)))?; + Ok(()) + } + + conn.transaction_async(|connection_pool| async move { + for (merchant_connector_account, update_merchant_connector_account) in + merchant_connector_accounts + { + #[cfg(feature = "v1")] + let _connector_name = merchant_connector_account.connector_name.clone(); + + #[cfg(feature = "v2")] + let _connector_name = merchant_connector_account.connector_name.to_string(); + + let _profile_id = merchant_connector_account.profile_id.clone(); + + let _merchant_id = merchant_connector_account.merchant_id.clone(); + let _merchant_connector_id = merchant_connector_account.get_id().clone(); + + let update = update_call( + &connection_pool, + ( + merchant_connector_account, + update_merchant_connector_account, + ), + ); + + #[cfg(feature = "accounts_cache")] + // Redact all caches as any of might be used because of backwards compatibility + Box::pin(cache::publish_and_redact_multiple( + self, + [ + cache::CacheKind::Accounts( + format!("{}_{}", _profile_id.get_string_repr(), _connector_name).into(), + ), + cache::CacheKind::Accounts( + format!( + "{}_{}", + _merchant_id.get_string_repr(), + _merchant_connector_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::CGraph( + format!( + "cgraph_{}_{}", + _merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + ], + || update, + )) + .await + .map_err(|error| { + // Returning `DatabaseConnectionError` after logging the actual error because + // -> it is not possible to get the underlying from `error_stack::Report` + // -> it is not possible to write a `From` impl to convert the `diesel::result::Error` to `error_stack::Report` + // because of Rust's orphan rules + router_env::logger::error!( + ?error, + "DB transaction for updating multiple merchant connector account failed" + ); + Self::Error::DatabaseConnectionError + })?; + + #[cfg(not(feature = "accounts_cache"))] + { + update.await.map_err(|error| { + // Returning `DatabaseConnectionError` after logging the actual error because + // -> it is not possible to get the underlying from `error_stack::Report` + // -> it is not possible to write a `From` impl to convert the `diesel::result::Error` to `error_stack::Report` + // because of Rust's orphan rules + router_env::logger::error!( + ?error, + "DB transaction for updating multiple merchant connector account failed" + ); + Self::Error::DatabaseConnectionError + })?; + } + } + Ok::<_, Self::Error>(()) + }) + .await?; + Ok(()) + } + + #[instrument(skip_all)] + #[cfg(feature = "v1")] + async fn update_merchant_connector_account( + &self, + state: &KeyManagerState, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let _connector_name = this.connector_name.clone(); + let _profile_id = this.profile_id.clone(); + + let _merchant_id = this.merchant_id.clone(); + let _merchant_connector_id = this.merchant_connector_id.clone(); + + let update_call = || async { + let conn = pg_accounts_connection_write(self).await?; + Conversion::convert(this) + .await + .change_context(Self::Error::EncryptionError)? + .update(&conn, merchant_connector_account) + .await + .map_err(|error| report!(Self::Error::from(error))) + .async_and_then(|item| async { + item.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(Self::Error::DecryptionError) + }) + .await + }; + + #[cfg(feature = "accounts_cache")] + { + // Redact all caches as any of might be used because of backwards compatibility + cache::publish_and_redact_multiple( + self, + [ + cache::CacheKind::Accounts( + format!("{}_{}", _profile_id.get_string_repr(), _connector_name).into(), + ), + cache::CacheKind::Accounts( + format!( + "{}_{}", + _merchant_id.get_string_repr(), + _merchant_connector_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::CGraph( + format!( + "cgraph_{}_{}", + _merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::PmFiltersCGraph( + format!( + "pm_filters_cgraph_{}_{}", + _merchant_id.get_string_repr(), + _profile_id.get_string_repr(), + ) + .into(), + ), + ], + update_call, + ) + .await + } + + #[cfg(not(feature = "accounts_cache"))] + { + update_call().await + } + } + + #[instrument(skip_all)] + #[cfg(feature = "v2")] + async fn update_merchant_connector_account( + &self, + state: &KeyManagerState, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let _connector_name = this.connector_name; + let _profile_id = this.profile_id.clone(); + + let _merchant_id = this.merchant_id.clone(); + let _merchant_connector_id = this.get_id().clone(); + + let update_call = || async { + let conn = pg_accounts_connection_write(self).await?; + Conversion::convert(this) + .await + .change_context(Self::Error::EncryptionError)? + .update(&conn, merchant_connector_account) + .await + .map_err(|error| report!(Self::Error::from(error))) + .async_and_then(|item| async { + item.convert( + state, + key_store.key.get_inner(), + common_utils::types::keymanager::Identifier::Merchant( + key_store.merchant_id.clone(), + ), + ) + .await + .change_context(Self::Error::DecryptionError) + }) + .await + }; + + #[cfg(feature = "accounts_cache")] + { + // Redact all caches as any of might be used because of backwards compatibility + cache::publish_and_redact_multiple( + self, + [ + cache::CacheKind::Accounts( + format!("{}_{}", _profile_id.get_string_repr(), _connector_name).into(), + ), + cache::CacheKind::Accounts( + _merchant_connector_id.get_string_repr().to_string().into(), + ), + cache::CacheKind::CGraph( + format!( + "cgraph_{}_{}", + _merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::PmFiltersCGraph( + format!( + "pm_filters_cgraph_{}_{}", + _merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + ], + update_call, + ) + .await + } + + #[cfg(not(feature = "accounts_cache"))] + { + update_call().await + } + } + + #[instrument(skip_all)] + #[cfg(feature = "v1")] + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + let delete_call = || async { + storage::MerchantConnectorAccount::delete_by_merchant_id_merchant_connector_id( + &conn, + merchant_id, + merchant_connector_id, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(feature = "accounts_cache")] + { + // We need to fetch mca here because the key that's saved in cache in + // {merchant_id}_{connector_label}. + // Used function from storage model to reuse the connection that made here instead of + // creating new. + + let mca = storage::MerchantConnectorAccount::find_by_merchant_id_merchant_connector_id( + &conn, + merchant_id, + merchant_connector_id, + ) + .await + .map_err(|error| report!(Self::Error::from(error)))?; + + let _profile_id = mca + .profile_id + .ok_or(Self::Error::ValueNotFound("profile_id".to_string()))?; + + cache::publish_and_redact_multiple( + self, + [ + cache::CacheKind::Accounts( + format!( + "{}_{}", + mca.merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::CGraph( + format!( + "cgraph_{}_{}", + mca.merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::PmFiltersCGraph( + format!( + "pm_filters_cgraph_{}_{}", + mca.merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + ], + delete_call, + ) + .await + } + + #[cfg(not(feature = "accounts_cache"))] + { + delete_call().await + } + } + + #[instrument(skip_all)] + #[cfg(feature = "v2")] + async fn delete_merchant_connector_account_by_id( + &self, + id: &common_utils::id_type::MerchantConnectorAccountId, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + let delete_call = || async { + storage::MerchantConnectorAccount::delete_by_id(&conn, id) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(feature = "accounts_cache")] + { + // We need to fetch mca here because the key that's saved in cache in + // {merchant_id}_{connector_label}. + // Used function from storage model to reuse the connection that made here instead of + // creating new. + + let mca = storage::MerchantConnectorAccount::find_by_id(&conn, id) + .await + .map_err(|error| report!(Self::Error::from(error)))?; + + let _profile_id = mca.profile_id; + + cache::publish_and_redact_multiple( + self, + [ + cache::CacheKind::Accounts( + format!( + "{}_{}", + mca.merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::CGraph( + format!( + "cgraph_{}_{}", + mca.merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + cache::CacheKind::PmFiltersCGraph( + format!( + "pm_filters_cgraph_{}_{}", + mca.merchant_id.get_string_repr(), + _profile_id.get_string_repr() + ) + .into(), + ), + ], + delete_call, + ) + .await + } + + #[cfg(not(feature = "accounts_cache"))] + { + delete_call().await + } + } +} + +#[async_trait::async_trait] +impl MerchantConnectorAccountInterface for MockDb { + type Error = StorageError; + async fn update_multiple_merchant_connector_accounts( + &self, + _merchant_connector_accounts: Vec<( + domain::MerchantConnectorAccount, + storage::MerchantConnectorAccountUpdateInternal, + )>, + ) -> CustomResult<(), StorageError> { + // No need to implement this function for `MockDb` as this function will be removed after the + // apple pay certificate migration + Err(StorageError::MockDbError)? + } + #[cfg(feature = "v1")] + async fn find_merchant_connector_account_by_merchant_id_connector_label( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + connector: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult { + match self + .merchant_connector_accounts + .lock() + .await + .iter() + .find(|account| { + account.merchant_id == *merchant_id + && account.connector_label == Some(connector.to_string()) + }) + .cloned() + .async_map(|account| async { + account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + { + Some(result) => result, + None => { + return Err(StorageError::ValueNotFound( + "cannot find merchant connector account".to_string(), + ) + .into()) + } + } + } + + async fn list_enabled_connector_accounts_by_profile_id( + &self, + _state: &KeyManagerState, + _profile_id: &common_utils::id_type::ProfileId, + _key_store: &MerchantKeyStore, + _connector_type: common_enums::ConnectorType, + ) -> CustomResult, StorageError> { + Err(StorageError::MockDbError)? + } + + #[cfg(feature = "v1")] + async fn find_merchant_connector_account_by_merchant_id_connector_name( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult, StorageError> { + let accounts = self + .merchant_connector_accounts + .lock() + .await + .iter() + .filter(|account| { + account.merchant_id == *merchant_id && account.connector_name == connector_name + }) + .cloned() + .collect::>(); + let mut output = Vec::with_capacity(accounts.len()); + for account in accounts.into_iter() { + output.push( + account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError)?, + ) + } + Ok(output) + } + + #[cfg(feature = "v1")] + async fn find_merchant_connector_account_by_profile_id_connector_name( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + connector_name: &str, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let maybe_mca = self + .merchant_connector_accounts + .lock() + .await + .iter() + .find(|account| { + account.profile_id.eq(&Some(profile_id.to_owned())) + && account.connector_name == connector_name + }) + .cloned(); + + match maybe_mca { + Some(mca) => mca + .to_owned() + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError), + None => Err(StorageError::ValueNotFound( + "cannot find merchant connector account".to_string(), + ) + .into()), + } + } + + #[cfg(feature = "v1")] + async fn find_by_merchant_connector_account_merchant_id_merchant_connector_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult { + match self + .merchant_connector_accounts + .lock() + .await + .iter() + .find(|account| { + account.merchant_id == *merchant_id + && account.merchant_connector_id == *merchant_connector_id + }) + .cloned() + .async_map(|account| async { + account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + { + Some(result) => result, + None => { + return Err(StorageError::ValueNotFound( + "cannot find merchant connector account".to_string(), + ) + .into()) + } + } + } + + #[cfg(feature = "v2")] + async fn find_merchant_connector_account_by_id( + &self, + state: &KeyManagerState, + id: &common_utils::id_type::MerchantConnectorAccountId, + key_store: &MerchantKeyStore, + ) -> CustomResult { + match self + .merchant_connector_accounts + .lock() + .await + .iter() + .find(|account| account.get_id() == *id) + .cloned() + .async_map(|account| async { + account + .convert( + state, + key_store.key.get_inner(), + common_utils::types::keymanager::Identifier::Merchant( + key_store.merchant_id.clone(), + ), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await + { + Some(result) => result, + None => { + return Err(StorageError::ValueNotFound( + "cannot find merchant connector account".to_string(), + ) + .into()) + } + } + } + + #[cfg(feature = "v1")] + async fn insert_merchant_connector_account( + &self, + state: &KeyManagerState, + t: domain::MerchantConnectorAccount, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let mut accounts = self.merchant_connector_accounts.lock().await; + let account = storage::MerchantConnectorAccount { + merchant_id: t.merchant_id, + connector_name: t.connector_name, + connector_account_details: t.connector_account_details.into(), + test_mode: t.test_mode, + disabled: t.disabled, + merchant_connector_id: t.merchant_connector_id.clone(), + id: Some(t.merchant_connector_id), + payment_methods_enabled: t.payment_methods_enabled, + metadata: t.metadata, + frm_configs: None, + frm_config: t.frm_configs, + connector_type: t.connector_type, + connector_label: t.connector_label, + business_country: t.business_country, + business_label: t.business_label, + business_sub_label: t.business_sub_label, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + connector_webhook_details: t.connector_webhook_details, + profile_id: Some(t.profile_id), + applepay_verified_domains: t.applepay_verified_domains, + pm_auth_config: t.pm_auth_config, + status: t.status, + connector_wallets_details: t.connector_wallets_details.map(Encryption::from), + additional_merchant_data: t.additional_merchant_data.map(|data| data.into()), + version: t.version, + }; + accounts.push(account.clone()); + account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + } + + #[cfg(feature = "v2")] + async fn insert_merchant_connector_account( + &self, + state: &KeyManagerState, + t: domain::MerchantConnectorAccount, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let mut accounts = self.merchant_connector_accounts.lock().await; + let account = storage::MerchantConnectorAccount { + id: t.id, + merchant_id: t.merchant_id, + connector_name: t.connector_name, + connector_account_details: t.connector_account_details.into(), + disabled: t.disabled, + payment_methods_enabled: t.payment_methods_enabled, + metadata: t.metadata, + frm_config: t.frm_configs, + connector_type: t.connector_type, + connector_label: t.connector_label, + created_at: common_utils::date_time::now(), + modified_at: common_utils::date_time::now(), + connector_webhook_details: t.connector_webhook_details, + profile_id: t.profile_id, + applepay_verified_domains: t.applepay_verified_domains, + pm_auth_config: t.pm_auth_config, + status: t.status, + connector_wallets_details: t.connector_wallets_details.map(Encryption::from), + additional_merchant_data: t.additional_merchant_data.map(|data| data.into()), + version: t.version, + feature_metadata: t.feature_metadata.map(From::from), + }; + accounts.push(account.clone()); + account + .convert( + state, + key_store.key.get_inner(), + common_utils::types::keymanager::Identifier::Merchant( + key_store.merchant_id.clone(), + ), + ) + .await + .change_context(StorageError::DecryptionError) + } + + async fn find_merchant_connector_account_by_merchant_id_and_disabled_list( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + get_disabled: bool, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let accounts = self + .merchant_connector_accounts + .lock() + .await + .iter() + .filter(|account: &&storage::MerchantConnectorAccount| { + if get_disabled { + account.merchant_id == *merchant_id + } else { + account.merchant_id == *merchant_id && account.disabled == Some(false) + } + }) + .cloned() + .collect::>(); + + let mut output = Vec::with_capacity(accounts.len()); + for account in accounts.into_iter() { + output.push( + account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError)?, + ) + } + Ok(domain::MerchantConnectorAccounts::new(output)) + } + + #[cfg(all(feature = "olap", feature = "v2"))] + async fn list_connector_account_by_profile_id( + &self, + state: &KeyManagerState, + profile_id: &common_utils::id_type::ProfileId, + key_store: &MerchantKeyStore, + ) -> CustomResult, StorageError> { + let accounts = self + .merchant_connector_accounts + .lock() + .await + .iter() + .filter(|account: &&storage::MerchantConnectorAccount| { + account.profile_id == *profile_id + }) + .cloned() + .collect::>(); + + let mut output = Vec::with_capacity(accounts.len()); + for account in accounts.into_iter() { + output.push( + account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError)?, + ) + } + Ok(output) + } + + #[cfg(feature = "v1")] + async fn update_merchant_connector_account( + &self, + state: &KeyManagerState, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let mca_update_res = self + .merchant_connector_accounts + .lock() + .await + .iter_mut() + .find(|account| account.merchant_connector_id == this.merchant_connector_id) + .map(|a| { + let updated = + merchant_connector_account.create_merchant_connector_account(a.clone()); + *a = updated.clone(); + updated + }) + .async_map(|account| async { + account + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await; + + match mca_update_res { + Some(result) => result, + None => { + return Err(StorageError::ValueNotFound( + "cannot find merchant connector account to update".to_string(), + ) + .into()) + } + } + } + + #[cfg(feature = "v2")] + async fn update_merchant_connector_account( + &self, + state: &KeyManagerState, + this: domain::MerchantConnectorAccount, + merchant_connector_account: storage::MerchantConnectorAccountUpdateInternal, + key_store: &MerchantKeyStore, + ) -> CustomResult { + let mca_update_res = self + .merchant_connector_accounts + .lock() + .await + .iter_mut() + .find(|account| account.get_id() == this.get_id()) + .map(|a| { + let updated = + merchant_connector_account.create_merchant_connector_account(a.clone()); + *a = updated.clone(); + updated + }) + .async_map(|account| async { + account + .convert( + state, + key_store.key.get_inner(), + common_utils::types::keymanager::Identifier::Merchant( + key_store.merchant_id.clone(), + ), + ) + .await + .change_context(StorageError::DecryptionError) + }) + .await; + + match mca_update_res { + Some(result) => result, + None => { + return Err(StorageError::ValueNotFound( + "cannot find merchant connector account to update".to_string(), + ) + .into()) + } + } + } + + #[cfg(feature = "v1")] + async fn delete_merchant_connector_account_by_merchant_id_merchant_connector_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + merchant_connector_id: &common_utils::id_type::MerchantConnectorAccountId, + ) -> CustomResult { + let mut accounts = self.merchant_connector_accounts.lock().await; + match accounts.iter().position(|account| { + account.merchant_id == *merchant_id + && account.merchant_connector_id == *merchant_connector_id + }) { + Some(index) => { + accounts.remove(index); + return Ok(true); + } + None => { + return Err(StorageError::ValueNotFound( + "cannot find merchant connector account to delete".to_string(), + ) + .into()) + } + } + } + + #[cfg(feature = "v2")] + async fn delete_merchant_connector_account_by_id( + &self, + id: &common_utils::id_type::MerchantConnectorAccountId, + ) -> CustomResult { + let mut accounts = self.merchant_connector_accounts.lock().await; + match accounts.iter().position(|account| account.get_id() == *id) { + Some(index) => { + accounts.remove(index); + return Ok(true); + } + None => { + return Err(StorageError::ValueNotFound( + "cannot find merchant connector account to delete".to_string(), + ) + .into()) + } + } + } +} diff --git a/crates/storage_impl/src/merchant_key_store.rs b/crates/storage_impl/src/merchant_key_store.rs new file mode 100644 index 0000000000..36126460af --- /dev/null +++ b/crates/storage_impl/src/merchant_key_store.rs @@ -0,0 +1,346 @@ +use error_stack::{report, ResultExt}; +use hyperswitch_domain_models::{ + behaviour::{Conversion, ReverseConversion}, + merchant_key_store as domain, + merchant_key_store::MerchantKeyStoreInterface, +}; +use masking::Secret; +use router_env::{instrument, tracing}; + +#[cfg(feature = "accounts_cache")] +use crate::redis::{ + cache, + cache::{CacheKind, ACCOUNTS_CACHE}, +}; +use crate::{ + kv_router_store, + utils::{pg_accounts_connection_read, pg_accounts_connection_write}, + CustomResult, DatabaseStore, KeyManagerState, MockDb, RouterStore, StorageError, +}; + +#[async_trait::async_trait] +impl MerchantKeyStoreInterface for kv_router_store::KVRouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_merchant_key_store( + &self, + state: &KeyManagerState, + merchant_key_store: domain::MerchantKeyStore, + key: &Secret>, + ) -> CustomResult { + self.router_store + .insert_merchant_key_store(state, merchant_key_store, key) + .await + } + + #[instrument(skip_all)] + async fn get_merchant_key_store_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + key: &Secret>, + ) -> CustomResult { + self.router_store + .get_merchant_key_store_by_merchant_id(state, merchant_id, key) + .await + } + + #[instrument(skip_all)] + async fn delete_merchant_key_store_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + self.router_store + .delete_merchant_key_store_by_merchant_id(merchant_id) + .await + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_multiple_key_stores( + &self, + state: &KeyManagerState, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, Self::Error> { + self.router_store + .list_multiple_key_stores(state, merchant_ids, key) + .await + } + + async fn get_all_key_stores( + &self, + state: &KeyManagerState, + key: &Secret>, + from: u32, + to: u32, + ) -> CustomResult, Self::Error> { + self.router_store + .get_all_key_stores(state, key, from, to) + .await + } +} + +#[async_trait::async_trait] +impl MerchantKeyStoreInterface for RouterStore { + type Error = StorageError; + #[instrument(skip_all)] + async fn insert_merchant_key_store( + &self, + state: &KeyManagerState, + merchant_key_store: domain::MerchantKeyStore, + key: &Secret>, + ) -> CustomResult { + let conn = pg_accounts_connection_write(self).await?; + let merchant_id = merchant_key_store.merchant_id.clone(); + merchant_key_store + .construct_new() + .await + .change_context(Self::Error::EncryptionError)? + .insert(&conn) + .await + .map_err(|error| report!(Self::Error::from(error)))? + .convert(state, key, merchant_id.into()) + .await + .change_context(Self::Error::DecryptionError) + } + + #[instrument(skip_all)] + async fn get_merchant_key_store_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + key: &Secret>, + ) -> CustomResult { + let fetch_func = || async { + let conn = pg_accounts_connection_read(self).await?; + + diesel_models::merchant_key_store::MerchantKeyStore::find_by_merchant_id( + &conn, + merchant_id, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(not(feature = "accounts_cache"))] + { + fetch_func() + .await? + .convert(state, key, merchant_id.clone().into()) + .await + .change_context(Self::Error::DecryptionError) + } + + #[cfg(feature = "accounts_cache")] + { + let key_store_cache_key = + format!("merchant_key_store_{}", merchant_id.get_string_repr()); + cache::get_or_populate_in_memory( + self, + &key_store_cache_key, + fetch_func, + &ACCOUNTS_CACHE, + ) + .await? + .convert(state, key, merchant_id.clone().into()) + .await + .change_context(Self::Error::DecryptionError) + } + } + + #[instrument(skip_all)] + async fn delete_merchant_key_store_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + let delete_func = || async { + let conn = pg_accounts_connection_write(self).await?; + diesel_models::merchant_key_store::MerchantKeyStore::delete_by_merchant_id( + &conn, + merchant_id, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + #[cfg(not(feature = "accounts_cache"))] + { + delete_func().await + } + + #[cfg(feature = "accounts_cache")] + { + let key_store_cache_key = + format!("merchant_key_store_{}", merchant_id.get_string_repr()); + cache::publish_and_redact( + self, + CacheKind::Accounts(key_store_cache_key.into()), + delete_func, + ) + .await + } + } + + #[cfg(feature = "olap")] + #[instrument(skip_all)] + async fn list_multiple_key_stores( + &self, + state: &KeyManagerState, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, Self::Error> { + let fetch_func = || async { + let conn = pg_accounts_connection_read(self).await?; + + diesel_models::merchant_key_store::MerchantKeyStore::list_multiple_key_stores( + &conn, + merchant_ids, + ) + .await + .map_err(|error| report!(Self::Error::from(error))) + }; + + futures::future::try_join_all(fetch_func().await?.into_iter().map(|key_store| async { + let merchant_id = key_store.merchant_id.clone(); + key_store + .convert(state, key, merchant_id.into()) + .await + .change_context(Self::Error::DecryptionError) + })) + .await + } + + async fn get_all_key_stores( + &self, + state: &KeyManagerState, + key: &Secret>, + from: u32, + to: u32, + ) -> CustomResult, Self::Error> { + let conn = pg_accounts_connection_read(self).await?; + let stores = diesel_models::merchant_key_store::MerchantKeyStore::list_all_key_stores( + &conn, from, to, + ) + .await + .map_err(|err| report!(Self::Error::from(err)))?; + + futures::future::try_join_all(stores.into_iter().map(|key_store| async { + let merchant_id = key_store.merchant_id.clone(); + key_store + .convert(state, key, merchant_id.into()) + .await + .change_context(Self::Error::DecryptionError) + })) + .await + } +} + +#[async_trait::async_trait] +impl MerchantKeyStoreInterface for MockDb { + type Error = StorageError; + async fn insert_merchant_key_store( + &self, + state: &KeyManagerState, + merchant_key_store: domain::MerchantKeyStore, + key: &Secret>, + ) -> CustomResult { + let mut locked_merchant_key_store = self.merchant_key_store.lock().await; + + if locked_merchant_key_store + .iter() + .any(|merchant_key| merchant_key.merchant_id == merchant_key_store.merchant_id) + { + Err(StorageError::DuplicateValue { + entity: "merchant_key_store", + key: Some(merchant_key_store.merchant_id.get_string_repr().to_owned()), + })?; + } + + let merchant_key = Conversion::convert(merchant_key_store) + .await + .change_context(StorageError::MockDbError)?; + locked_merchant_key_store.push(merchant_key.clone()); + let merchant_id = merchant_key.merchant_id.clone(); + merchant_key + .convert(state, key, merchant_id.into()) + .await + .change_context(StorageError::DecryptionError) + } + + async fn get_merchant_key_store_by_merchant_id( + &self, + state: &KeyManagerState, + merchant_id: &common_utils::id_type::MerchantId, + key: &Secret>, + ) -> CustomResult { + self.merchant_key_store + .lock() + .await + .iter() + .find(|merchant_key| merchant_key.merchant_id == *merchant_id) + .cloned() + .ok_or(StorageError::ValueNotFound(String::from( + "merchant_key_store", + )))? + .convert(state, key, merchant_id.clone().into()) + .await + .change_context(StorageError::DecryptionError) + } + + async fn delete_merchant_key_store_by_merchant_id( + &self, + merchant_id: &common_utils::id_type::MerchantId, + ) -> CustomResult { + let mut merchant_key_stores = self.merchant_key_store.lock().await; + let index = merchant_key_stores + .iter() + .position(|mks| mks.merchant_id == *merchant_id) + .ok_or(StorageError::ValueNotFound(format!( + "No merchant key store found for merchant_id = {merchant_id:?}", + )))?; + merchant_key_stores.remove(index); + Ok(true) + } + + #[cfg(feature = "olap")] + async fn list_multiple_key_stores( + &self, + state: &KeyManagerState, + merchant_ids: Vec, + key: &Secret>, + ) -> CustomResult, StorageError> { + let merchant_key_stores = self.merchant_key_store.lock().await; + futures::future::try_join_all( + merchant_key_stores + .iter() + .filter(|merchant_key| merchant_ids.contains(&merchant_key.merchant_id)) + .map(|merchant_key| async { + merchant_key + .to_owned() + .convert(state, key, merchant_key.merchant_id.clone().into()) + .await + .change_context(StorageError::DecryptionError) + }), + ) + .await + } + async fn get_all_key_stores( + &self, + state: &KeyManagerState, + key: &Secret>, + _from: u32, + _to: u32, + ) -> CustomResult, StorageError> { + let merchant_key_stores = self.merchant_key_store.lock().await; + + futures::future::try_join_all(merchant_key_stores.iter().map(|merchant_key| async { + merchant_key + .to_owned() + .convert(state, key, merchant_key.merchant_id.clone().into()) + .await + .change_context(StorageError::DecryptionError) + })) + .await + } +} diff --git a/crates/storage_impl/src/mock_db.rs b/crates/storage_impl/src/mock_db.rs index b3fc53fb8c..8b40907cb0 100644 --- a/crates/storage_impl/src/mock_db.rs +++ b/crates/storage_impl/src/mock_db.rs @@ -231,6 +231,13 @@ impl MockDb { Err(StorageError::ValueNotFound(error_message).into()) } } + + pub fn get_master_key(&self) -> &[u8] { + &[ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, + ] + } } #[cfg(not(feature = "payouts"))] diff --git a/crates/storage_impl/src/utils.rs b/crates/storage_impl/src/utils.rs index fd611911d6..acd708e617 100644 --- a/crates/storage_impl/src/utils.rs +++ b/crates/storage_impl/src/utils.rs @@ -47,6 +47,46 @@ pub async fn pg_connection_write( .change_context(StorageError::DatabaseConnectionError) } +pub async fn pg_accounts_connection_read( + store: &T, +) -> error_stack::Result< + PooledConnection<'_, async_bb8_diesel::ConnectionManager>, + StorageError, +> { + // If only OLAP is enabled get replica pool. + #[cfg(all(feature = "olap", not(feature = "oltp")))] + let pool = store.get_accounts_replica_pool(); + + // If either one of these are true we need to get master pool. + // 1. Only OLTP is enabled. + // 2. Both OLAP and OLTP is enabled. + // 3. Both OLAP and OLTP is disabled. + #[cfg(any( + all(not(feature = "olap"), feature = "oltp"), + all(feature = "olap", feature = "oltp"), + all(not(feature = "olap"), not(feature = "oltp")) + ))] + let pool = store.get_accounts_master_pool(); + + pool.get() + .await + .change_context(StorageError::DatabaseConnectionError) +} + +pub async fn pg_accounts_connection_write( + store: &T, +) -> error_stack::Result< + PooledConnection<'_, async_bb8_diesel::ConnectionManager>, + StorageError, +> { + // Since all writes should happen to master DB only choose master DB. + let pool = store.get_accounts_master_pool(); + + pool.get() + .await + .change_context(StorageError::DatabaseConnectionError) +} + pub async fn try_redis_get_else_try_database_get( redis_fut: RFut, database_call_closure: F, From 3296f6260623b024248b898a81ab56ccc767a3dd Mon Sep 17 00:00:00 2001 From: Shailesh <151172220+Shailesh-714@users.noreply.github.com> Date: Wed, 15 Oct 2025 12:23:38 +0530 Subject: [PATCH 03/16] chore: added explicit docker.io registry to all images in docker-compose.yml (#9771) --- docker-compose-development.yml | 30 +++++++++++------------ docker-compose.yml | 44 ++++++++++++++++------------------ 2 files changed, 36 insertions(+), 38 deletions(-) diff --git a/docker-compose-development.yml b/docker-compose-development.yml index ab5f616a4f..a26ba9ec21 100644 --- a/docker-compose-development.yml +++ b/docker-compose-development.yml @@ -14,7 +14,7 @@ networks: services: ### Dependencies pg: - image: postgres:latest + image: docker.io/postgres:latest ports: - "5432:5432" networks: @@ -33,7 +33,7 @@ services: timeout: 5s redis-standalone: - image: redis:7 + image: docker.io/redis:7 networks: - router_net ports: @@ -46,7 +46,7 @@ services: timeout: 5s migration_runner: - image: debian:trixie-slim + image: docker.io/debian:trixie-slim pull_policy: always command: > bash -c " @@ -111,7 +111,7 @@ services: timeout: 10s hyperswitch-producer: - image: rust:latest + image: docker.io/rust:latest command: cargo run --bin scheduler -- -f ./config/docker_compose.toml working_dir: /app networks: @@ -133,7 +133,7 @@ services: logs: "promtail" hyperswitch-consumer: - image: rust:latest + image: docker.io/rust:latest command: cargo run --bin scheduler -- -f ./config/docker_compose.toml working_dir: /app networks: @@ -161,7 +161,7 @@ services: timeout: 10s hyperswitch-drainer: - image: rust:latest + image: docker.io/rust:latest command: cargo run --bin drainer -- -f ./config/docker_compose.toml working_dir: /app deploy: @@ -186,7 +186,7 @@ services: ### Clustered Redis setup redis-cluster: - image: redis:7 + image: docker.io/redis:7 deploy: replicas: ${REDIS_CLUSTER_COUNT:-3} command: redis-server /usr/local/etc/redis/redis.conf @@ -201,7 +201,7 @@ services: - "16379" redis-init: - image: redis:7 + image: docker.io/redis:7 profiles: - clustered_redis depends_on: @@ -242,7 +242,7 @@ services: ### Monitoring grafana: - image: grafana/grafana:latest + image: docker.io/grafana/grafana:latest ports: - "3000:3000" networks: @@ -259,7 +259,7 @@ services: - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml promtail: - image: grafana/promtail:latest + image: docker.io/grafana/promtail:latest volumes: - ./logs:/var/log/router - ./config:/etc/promtail @@ -271,7 +271,7 @@ services: - router_net loki: - image: grafana/loki:latest + image: docker.io/grafana/loki:latest ports: - "3100" command: -config.file=/etc/loki/loki.yaml @@ -283,7 +283,7 @@ services: - ./config:/etc/loki otel-collector: - image: otel/opentelemetry-collector-contrib:latest + image: docker.io/otel/opentelemetry-collector-contrib:latest command: --config=/etc/otel-collector.yaml networks: - router_net @@ -297,7 +297,7 @@ services: - "8889" prometheus: - image: prom/prometheus:latest + image: docker.io/prom/prometheus:latest networks: - router_net profiles: @@ -309,7 +309,7 @@ services: restart: unless-stopped tempo: - image: grafana/tempo:latest + image: docker.io/grafana/tempo:latest command: -config.file=/etc/tempo.yaml volumes: - ./config/tempo.yaml:/etc/tempo.yaml @@ -323,7 +323,7 @@ services: restart: unless-stopped redis-insight: - image: redislabs/redisinsight:latest + image: docker.io/redislabs/redisinsight:latest networks: - router_net profiles: diff --git a/docker-compose.yml b/docker-compose.yml index 4e2600c834..c0bab9a82d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ networks: services: ### Dependencies prestart-hook: - image: curlimages/curl-base:latest + image: docker.io/curlimages/curl-base:latest container_name: prestart-hook environment: - ONE_CLICK_SETUP=${ONE_CLICK_SETUP:-false} @@ -25,7 +25,7 @@ services: - router_net pg: - image: postgres:latest + image: docker.io/postgres:latest ports: - "5432:5432" networks: @@ -44,7 +44,7 @@ services: timeout: 5s redis-standalone: - image: redis:7 + image: docker.io/redis:7 networks: - router_net ports: @@ -57,7 +57,7 @@ services: timeout: 5s migration_runner: - image: debian:trixie-slim + image: docker.io/debian:trixie-slim pull_policy: always command: > bash -c " @@ -78,7 +78,7 @@ services: - DATABASE_URL=postgresql://db_user:db_pass@pg:5432/hyperswitch_db mailhog: - image: mailhog/mailhog + image: docker.io/mailhog/mailhog networks: - router_net profiles: @@ -216,7 +216,7 @@ services: logs: "promtail" create-default-user: - image: curlimages/curl-base:latest + image: docker.io/curlimages/curl-base:latest container_name: create-default-user depends_on: hyperswitch-server: @@ -238,11 +238,9 @@ services: - router_net poststart-hook: - image: curlimages/curl-base:latest + image: docker.io/curlimages/curl-base:latest container_name: poststart-hook depends_on: - create-default-user: - condition: service_completed_successfully hyperswitch-server: condition: service_healthy # Ensures it only starts when `hyperswitch-server` is healthy environment: @@ -260,7 +258,7 @@ services: ### Clustered Redis setup redis-cluster: - image: redis:7 + image: docker.io/redis:7 deploy: replicas: ${REDIS_CLUSTER_COUNT:-3} command: redis-server /usr/local/etc/redis/redis.conf @@ -275,7 +273,7 @@ services: - "16379" redis-init: - image: redis:7 + image: docker.io/redis:7 profiles: - clustered_redis depends_on: @@ -301,7 +299,7 @@ services: ' ### Monitoring grafana: - image: grafana/grafana:latest + image: docker.io/grafana/grafana:latest ports: - "3000:3000" networks: @@ -318,7 +316,7 @@ services: - ./config/grafana-datasource.yaml:/etc/grafana/provisioning/datasources/datasource.yml loki: - image: grafana/loki:latest + image: docker.io/grafana/loki:latest ports: - "3100" command: -config.file=/etc/loki/loki.yaml @@ -330,7 +328,7 @@ services: - ./config:/etc/loki otel-collector: - image: otel/opentelemetry-collector-contrib:latest + image: docker.io/otel/opentelemetry-collector-contrib:latest command: --config=/etc/otel-collector.yaml networks: - router_net @@ -344,7 +342,7 @@ services: - "8889" prometheus: - image: prom/prometheus:latest + image: docker.io/prom/prometheus:latest networks: - router_net profiles: @@ -356,7 +354,7 @@ services: restart: unless-stopped tempo: - image: grafana/tempo:latest + image: docker.io/grafana/tempo:latest command: -config.file=/etc/tempo.yaml volumes: - ./config/tempo.yaml:/etc/tempo.yaml @@ -370,7 +368,7 @@ services: restart: unless-stopped redis-insight: - image: redislabs/redisinsight:latest + image: docker.io/redislabs/redisinsight:latest networks: - router_net profiles: @@ -381,7 +379,7 @@ services: - redisinsight_store:/db kafka0: - image: confluentinc/cp-kafka:7.0.5 + image: docker.io/confluentinc/cp-kafka:7.0.5 hostname: kafka0 networks: - router_net @@ -415,7 +413,7 @@ services: # Kafka UI for debugging kafka queues kafka-ui: - image: provectuslabs/kafka-ui:latest + image: docker.io/provectuslabs/kafka-ui:latest ports: - 8090:8080 networks: @@ -430,7 +428,7 @@ services: KAFKA_CLUSTERS_0_JMXPORT: 9997 clickhouse-server: - image: clickhouse/clickhouse-server:24.3 + image: docker.io/clickhouse/clickhouse-server:24.3 networks: - router_net ports: @@ -448,7 +446,7 @@ services: hard: 262144 opensearch: - image: opensearchproject/opensearch:2 + image: docker.io/opensearchproject/opensearch:2 container_name: opensearch hostname: opensearch environment: @@ -463,7 +461,7 @@ services: - router_net opensearch-dashboards: - image: opensearchproject/opensearch-dashboards:2 + image: docker.io/opensearchproject/opensearch-dashboards:2 ports: - 5601:5601 profiles: @@ -474,7 +472,7 @@ services: - router_net vector: - image: timberio/vector:latest-debian + image: docker.io/timberio/vector:latest-debian ports: - "8686" - "9598" From 6394c892cdfadf90e55bb524db719b547dc519ba Mon Sep 17 00:00:00 2001 From: Anurag Date: Wed, 15 Oct 2025 16:29:55 +0530 Subject: [PATCH 04/16] feat(connector): [Peachpayments] Add Webhook Flow and Support For merchant_order_reference_id (#9781) Co-authored-by: Anurag Singh Co-authored-by: Anurag Singh --- .../src/connectors/peachpayments.rs | 89 ++++++++- .../connectors/peachpayments/transformers.rs | 179 ++++++++++++++---- 2 files changed, 219 insertions(+), 49 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/peachpayments.rs b/crates/hyperswitch_connectors/src/connectors/peachpayments.rs index dd5c4869cf..2e95b9e8a6 100644 --- a/crates/hyperswitch_connectors/src/connectors/peachpayments.rs +++ b/crates/hyperswitch_connectors/src/connectors/peachpayments.rs @@ -5,11 +5,12 @@ use std::sync::LazyLock; use common_enums::{self, enums}; use common_utils::{ errors::CustomResult, - ext_traits::BytesExt, + ext_traits::{ByteSliceExt, BytesExt}, + id_type, request::{Method, Request, RequestBuilder, RequestContent}, types::{AmountConvertor, MinorUnit, MinorUnitForConnector}, }; -use error_stack::{report, ResultExt}; +use error_stack::ResultExt; use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_flow_types::{ @@ -42,7 +43,7 @@ use hyperswitch_interfaces::{ types::{self, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; +use masking::{ExposeInterface, Mask, Secret}; use transformers as peachpayments; use crate::{constants::headers, types::ResponseRouterData, utils}; @@ -542,23 +543,90 @@ impl ConnectorIntegration for Peachpaym impl webhooks::IncomingWebhook for Peachpayments { fn get_webhook_object_reference_id( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: peachpayments::PeachpaymentsIncomingWebhook = request + .body + .parse_struct("PeachpaymentsIncomingWebhook") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + let reference_id = webhook_body + .transaction + .as_ref() + .map(|txn| txn.reference_id.clone()) + .ok_or(errors::ConnectorError::WebhookReferenceIdNotFound)?; + + Ok(api_models::webhooks::ObjectReferenceId::PaymentId( + api_models::payments::PaymentIdType::PaymentAttemptId(reference_id), + )) } fn get_webhook_event_type( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: peachpayments::PeachpaymentsIncomingWebhook = request + .body + .parse_struct("PeachpaymentsIncomingWebhook") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + match webhook_body.webhook_type.as_str() { + "transaction" => { + if let Some(transaction) = webhook_body.transaction { + match transaction.transaction_result { + peachpayments::PeachpaymentsPaymentStatus::Successful + | peachpayments::PeachpaymentsPaymentStatus::ApprovedConfirmed => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentSuccess) + } + peachpayments::PeachpaymentsPaymentStatus::Authorized + | peachpayments::PeachpaymentsPaymentStatus::Approved => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentAuthorizationSuccess) + } + peachpayments::PeachpaymentsPaymentStatus::Pending => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentProcessing) + } + peachpayments::PeachpaymentsPaymentStatus::Declined + | peachpayments::PeachpaymentsPaymentStatus::Failed => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentFailure) + } + peachpayments::PeachpaymentsPaymentStatus::Voided + | peachpayments::PeachpaymentsPaymentStatus::Reversed => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentIntentCancelled) + } + peachpayments::PeachpaymentsPaymentStatus::ThreedsRequired => { + Ok(api_models::webhooks::IncomingWebhookEvent::PaymentActionRequired) + } + } + } else { + Err(errors::ConnectorError::WebhookEventTypeNotFound) + } + } + _ => Err(errors::ConnectorError::WebhookEventTypeNotFound), + } + .change_context(errors::ConnectorError::WebhookEventTypeNotFound) } fn get_webhook_resource_object( &self, - _request: &webhooks::IncomingWebhookRequestDetails<'_>, + request: &webhooks::IncomingWebhookRequestDetails<'_>, ) -> CustomResult, errors::ConnectorError> { - Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + let webhook_body: peachpayments::PeachpaymentsIncomingWebhook = request + .body + .parse_struct("PeachpaymentsIncomingWebhook") + .change_context(errors::ConnectorError::WebhookBodyDecodingFailed)?; + + Ok(Box::new(webhook_body)) + } + + async fn verify_webhook_source( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + _merchant_id: &id_type::MerchantId, + _connector_webhook_details: Option, + _connector_account_details: common_utils::crypto::Encryptable>, + _connector_name: &str, + ) -> CustomResult { + Ok(false) } } @@ -625,7 +693,8 @@ static PEACHPAYMENTS_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo { integration_status: enums::ConnectorIntegrationStatus::Beta, }; -static PEACHPAYMENTS_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 0] = []; +static PEACHPAYMENTS_SUPPORTED_WEBHOOK_FLOWS: [enums::EventClass; 1] = + [enums::EventClass::Payments]; impl ConnectorSpecifications for Peachpayments { fn get_connector_about(&self) -> Option<&'static ConnectorInfo> { diff --git a/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs b/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs index b66112f363..349af2ede6 100644 --- a/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/peachpayments/transformers.rs @@ -1,7 +1,8 @@ use std::str::FromStr; use cards::CardNumber; -use common_utils::{pii, types::MinorUnit}; +use common_enums::enums as storage_enums; +use common_utils::{errors::CustomResult, pii, types::MinorUnit}; use error_stack::ResultExt; use hyperswitch_domain_models::{ network_tokenization::NetworkTokenNumber, @@ -99,6 +100,7 @@ pub struct EcommerceCardPaymentOnlyTransactionData { pub routing: Routing, pub card: CardDetails, pub amount: AmountDetails, + pub rrn: Option, } #[derive(Debug, Serialize, PartialEq)] @@ -581,6 +583,7 @@ impl TryFrom<(&PeachpaymentsRouterData<&PaymentsAuthorizeRouterData>, Card)> routing, card, amount, + rrn: item.router_data.request.merchant_order_reference_id.clone(), }); // Generate current timestamp for sendDateTime (ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ) @@ -652,9 +655,16 @@ impl From for common_enums::AttemptStatus { } } +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum PeachpaymentsPaymentsResponse { + Response(Box), + WebhookResponse(Box), +} + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] #[serde[rename_all = "camelCase"]] -pub struct PeachpaymentsPaymentsResponse { +pub struct PeachpaymentsPaymentsData { pub transaction_id: String, pub response_code: Option, pub transaction_result: PeachpaymentsPaymentStatus, @@ -751,6 +761,105 @@ fn get_error_message(response_code: Option<&ResponseCode>) -> String { ) } +pub fn get_peachpayments_response( + response: PeachpaymentsPaymentsData, + status_code: u16, +) -> CustomResult< + ( + storage_enums::AttemptStatus, + Result, + ), + errors::ConnectorError, +> { + let status = common_enums::AttemptStatus::from(response.transaction_result); + let payments_response = if !is_payment_success( + response + .response_code + .as_ref() + .and_then(|code| code.value()), + ) { + Err(ErrorResponse { + code: get_error_code(response.response_code.as_ref()), + message: get_error_message(response.response_code.as_ref()), + reason: response + .ecommerce_card_payment_only_transaction_data + .and_then(|data| data.description), + status_code, + attempt_status: Some(status), + connector_transaction_id: Some(response.transaction_id.clone()), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + connector_metadata: None, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(response.transaction_id.clone()), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(response.transaction_id), + incremental_authorization_allowed: None, + charges: None, + }) + }; + Ok((status, payments_response)) +} + +pub fn get_webhook_response( + response: PeachpaymentsIncomingWebhook, + status_code: u16, +) -> CustomResult< + ( + storage_enums::AttemptStatus, + Result, + ), + errors::ConnectorError, +> { + let transaction = response + .transaction + .ok_or(errors::ConnectorError::WebhookResourceObjectNotFound)?; + let status = common_enums::AttemptStatus::from(transaction.transaction_result); + let webhook_response = if !is_payment_success( + transaction + .response_code + .as_ref() + .and_then(|code| code.value()), + ) { + Err(ErrorResponse { + code: get_error_code(transaction.response_code.as_ref()), + message: get_error_message(transaction.response_code.as_ref()), + reason: transaction + .ecommerce_card_payment_only_transaction_data + .and_then(|data| data.description), + status_code, + attempt_status: Some(status), + connector_transaction_id: Some(transaction.transaction_id.clone()), + network_advice_code: None, + network_decline_code: None, + network_error_message: None, + connector_metadata: None, + }) + } else { + Ok(PaymentsResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId( + transaction + .original_transaction_id + .unwrap_or(transaction.transaction_id.clone()), + ), + redirection_data: Box::new(None), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: None, + connector_response_reference_id: Some(transaction.transaction_id.clone()), + incremental_authorization_allowed: None, + charges: None, + }) + }; + Ok((status, webhook_response)) +} + impl TryFrom> for RouterData { @@ -758,43 +867,13 @@ impl TryFrom, ) -> Result { - let status = common_enums::AttemptStatus::from(item.response.transaction_result); - - // Check if it's an error response - let response = if !is_payment_success( - item.response - .response_code - .as_ref() - .and_then(|code| code.value()), - ) { - Err(ErrorResponse { - code: get_error_code(item.response.response_code.as_ref()), - message: get_error_message(item.response.response_code.as_ref()), - reason: item - .response - .ecommerce_card_payment_only_transaction_data - .and_then(|data| data.description), - status_code: item.http_code, - attempt_status: Some(status), - connector_transaction_id: Some(item.response.transaction_id.clone()), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, - connector_metadata: None, - }) - } else { - Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId( - item.response.transaction_id.clone(), - ), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: Some(item.response.transaction_id), - incremental_authorization_allowed: None, - charges: None, - }) + let (status, response) = match item.response { + PeachpaymentsPaymentsResponse::Response(response) => { + get_peachpayments_response(*response, item.http_code)? + } + PeachpaymentsPaymentsResponse::WebhookResponse(response) => { + get_webhook_response(*response, item.http_code)? + } }; Ok(Self { @@ -884,6 +963,28 @@ impl TryFrom<&PeachpaymentsRouterData<&PaymentsAuthorizeRouterData>> } } +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct PeachpaymentsIncomingWebhook { + pub webhook_id: String, + pub webhook_type: String, + pub reversal_failure_reason: Option, + pub transaction: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct WebhookTransaction { + pub transaction_id: String, + pub original_transaction_id: Option, + pub reference_id: String, + pub transaction_result: PeachpaymentsPaymentStatus, + pub error_message: Option, + pub response_code: Option, + pub ecommerce_card_payment_only_transaction_data: Option, + pub payment_method: Secret, +} + // Error Response #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] From bebffa97ffc66d9a4d3913ec884b3d3851735a4f Mon Sep 17 00:00:00 2001 From: peter007-cmd <55039991+peter007-cmd@users.noreply.github.com> Date: Wed, 15 Oct 2025 16:59:19 +0530 Subject: [PATCH 05/16] feat: include response body for create_user_authentication_method (#9653) --- crates/api_models/src/events/user.rs | 22 +++++++++++--------- crates/api_models/src/user.rs | 11 ++++++++++ crates/router/src/core/user.rs | 31 ++++++++++++++++++---------- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 11e6f47193..4b7663f05d 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -13,16 +13,17 @@ use crate::user::{ }, AcceptInviteFromEmailRequest, AuthSelectRequest, AuthorizeResponse, BeginTotpResponse, ChangePasswordRequest, CloneConnectorRequest, ConnectAccountRequest, CreateInternalUserRequest, - CreateTenantUserRequest, CreateUserAuthenticationMethodRequest, ForgotPasswordRequest, - GetSsoAuthUrlRequest, GetUserAuthenticationMethodsRequest, GetUserDetailsResponse, - GetUserRoleDetailsRequest, GetUserRoleDetailsResponseV2, InviteUserRequest, - PlatformAccountCreateRequest, PlatformAccountCreateResponse, ReInviteUserRequest, - RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest, - SignUpRequest, SignUpWithMerchantIdRequest, SsoSignInRequest, SwitchMerchantRequest, - SwitchOrganizationRequest, SwitchProfileRequest, TokenResponse, TwoFactorAuthStatusResponse, - TwoFactorStatus, UpdateUserAccountDetailsRequest, UpdateUserAuthenticationMethodRequest, - UserFromEmailRequest, UserMerchantAccountResponse, UserMerchantCreate, - UserOrgMerchantCreateRequest, VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest, + CreateTenantUserRequest, CreateUserAuthenticationMethodRequest, + CreateUserAuthenticationMethodResponse, ForgotPasswordRequest, GetSsoAuthUrlRequest, + GetUserAuthenticationMethodsRequest, GetUserDetailsResponse, GetUserRoleDetailsRequest, + GetUserRoleDetailsResponseV2, InviteUserRequest, PlatformAccountCreateRequest, + PlatformAccountCreateResponse, ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest, + RotatePasswordRequest, SendVerifyEmailRequest, SignUpRequest, SignUpWithMerchantIdRequest, + SsoSignInRequest, SwitchMerchantRequest, SwitchOrganizationRequest, SwitchProfileRequest, + TokenResponse, TwoFactorAuthStatusResponse, TwoFactorStatus, UpdateUserAccountDetailsRequest, + UpdateUserAuthenticationMethodRequest, UserFromEmailRequest, UserMerchantAccountResponse, + UserMerchantCreate, UserOrgMerchantCreateRequest, VerifyEmailRequest, + VerifyRecoveryCodeRequest, VerifyTotpRequest, }; common_utils::impl_api_event_type!( @@ -69,6 +70,7 @@ common_utils::impl_api_event_type!( RecoveryCodes, GetUserAuthenticationMethodsRequest, CreateUserAuthenticationMethodRequest, + CreateUserAuthenticationMethodResponse, UpdateUserAuthenticationMethodRequest, GetSsoAuthUrlRequest, SsoSignInRequest, diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 82d10a1621..6f19376ce2 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -353,6 +353,17 @@ pub struct CreateUserAuthenticationMethodRequest { pub email_domain: Option, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct CreateUserAuthenticationMethodResponse { + pub id: String, + pub auth_id: String, + pub owner_id: String, + pub owner_type: common_enums::Owner, + pub auth_type: common_enums::UserAuthType, + pub email_domain: Option, + pub allow_signup: bool, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "snake_case")] pub enum UpdateUserAuthenticationMethodRequest { diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 837b0bcf9f..c82bbec27a 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -2482,7 +2482,7 @@ pub async fn check_two_factor_auth_status_with_attempts( pub async fn create_user_authentication_method( state: SessionState, req: user_api::CreateUserAuthenticationMethodRequest, -) -> UserResponse<()> { +) -> UserResponse { let user_auth_encryption_key = hex::decode( state .conf @@ -2494,7 +2494,9 @@ pub async fn create_user_authentication_method( ) .change_context(UserErrors::InternalServerError) .attach_printable("Failed to decode DEK")?; + let id = uuid::Uuid::new_v4().to_string(); + let (private_config, public_config) = utils::user::construct_public_and_private_db_configs( &state, &req.auth_method, @@ -2511,15 +2513,14 @@ pub async fn create_user_authentication_method( .attach_printable("Failed to get list of auth methods for the owner id")?; let (auth_id, email_domain) = if let Some(auth_method) = auth_methods.first() { - let email_domain = match req.email_domain { + let email_domain = match &req.email_domain { Some(email_domain) => { - if email_domain != auth_method.email_domain { + if email_domain != &auth_method.email_domain { return Err(report!(UserErrors::InvalidAuthMethodOperationWithMessage( - "Email domain mismatch".to_string() + "Email domain mismatch".to_string(), ))); } - - email_domain + email_domain.clone() } None => auth_method.email_domain.clone(), }; @@ -2531,7 +2532,6 @@ pub async fn create_user_authentication_method( .ok_or(UserErrors::InvalidAuthMethodOperationWithMessage( "Email domain not found".to_string(), ))?; - (uuid::Uuid::new_v4().to_string(), email_domain) }; @@ -2549,8 +2549,7 @@ pub async fn create_user_authentication_method( }) .transpose()? .map(|config| config.name); - let req_auth_name = public_config.name; - db_auth_name.is_some_and(|name| name == req_auth_name) + db_auth_name.is_some_and(|name| name == public_config.name) } user_api::AuthConfig::Password | user_api::AuthConfig::MagicLink => true, }; @@ -2560,7 +2559,7 @@ pub async fn create_user_authentication_method( } let now = common_utils::date_time::now(); - state + let inserted_auth_method = state .store .insert_user_authentication_method(UserAuthenticationMethodNew { id, @@ -2578,7 +2577,17 @@ pub async fn create_user_authentication_method( .await .to_duplicate_response(UserErrors::UserAuthMethodAlreadyExists)?; - Ok(ApplicationResponse::StatusOk) + Ok(ApplicationResponse::Json( + user_api::CreateUserAuthenticationMethodResponse { + id: inserted_auth_method.id, + auth_id: inserted_auth_method.auth_id, + owner_id: inserted_auth_method.owner_id, + owner_type: inserted_auth_method.owner_type, + auth_type: inserted_auth_method.auth_type, + email_domain: Some(inserted_auth_method.email_domain), + allow_signup: inserted_auth_method.allow_signup, + }, + )) } pub async fn update_user_authentication_method( From f9bd87dd9b9ac3186bd08e779ae40327c9b2cf80 Mon Sep 17 00:00:00 2001 From: Prakhar Agrawal <72659712+Prakhar0013@users.noreply.github.com> Date: Wed, 15 Oct 2025 07:29:27 -0400 Subject: [PATCH 06/16] feat: Add attach_printable() for better error logging (#9667) --- crates/router/src/core/user.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index c82bbec27a..3a80fe18b6 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -458,7 +458,8 @@ pub async fn change_password( }, ) .await - .change_context(UserErrors::InternalServerError)?; + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to update user password in the database")?; let _ = auth::blacklist::insert_user_in_blacklist(&state, user.get_user_id()) .await From 06484931ea3c141ba1f1d9256424e2ef43443aec Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Wed, 15 Oct 2025 19:00:41 +0530 Subject: [PATCH 07/16] ci: remove cargo check (#9819) --- .github/workflows/CI-pr.yml | 81 ++----------------------------------- 1 file changed, 3 insertions(+), 78 deletions(-) diff --git a/.github/workflows/CI-pr.yml b/.github/workflows/CI-pr.yml index 8359bc7990..ec1bc0141e 100644 --- a/.github/workflows/CI-pr.yml +++ b/.github/workflows/CI-pr.yml @@ -121,59 +121,14 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - # - name: Install sccache - # uses: taiki-e/install-action@v2 - # with: - # tool: sccache - # checksum: true - - name: Install rust cache uses: Swatinem/rust-cache@v2.7.7 with: save-if: false - - name: Install cargo-hack - uses: taiki-e/install-action@v2 - with: - tool: cargo-hack - checksum: true - - - name: Install just - uses: taiki-e/install-action@v2 - with: - tool: just - checksum: true - - - name: Install jq + - name: Run cargo check shell: bash - run: .github/scripts/install-jq.sh - - - name: Cargo hack - shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: just ci_hack - - # cargo-deny: - # name: Run cargo-deny - # runs-on: ubuntu-latest - # strategy: - # matrix: - # checks: - # - advisories - # - bans licenses sources - - # # Prevent sudden announcement of a new advisory from failing CI - # continue-on-error: ${{ matrix.checks == 'advisories' }} - - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 - - # - name: Run cargo-deny - # uses: EmbarkStudios/cargo-deny-action@v1.3.2 - # with: - # command: check ${{ matrix.checks }} + run: cargo check --features "release" test: name: Run tests on stable toolchain @@ -229,39 +184,17 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - # - name: Install sccache - # uses: taiki-e/install-action@v2 - # with: - # tool: sccache - # checksum: true - - name: Install rust cache uses: Swatinem/rust-cache@v2.7.7 with: save-if: false - - name: Install cargo-hack - uses: taiki-e/install-action@v2 - with: - tool: cargo-hack - checksum: true - - name: Install just uses: taiki-e/install-action@v2 with: tool: just checksum: true - - name: Install jq - shell: bash - run: .github/scripts/install-jq.sh - - # - name: Install cargo-nextest - # uses: taiki-e/install-action@v2 - # with: - # tool: cargo-nextest - # checksum: true - - name: Run clippy shell: bash run: just clippy @@ -279,16 +212,8 @@ jobs: fi - name: Run cargo check - if: ${{ ! startsWith(github.event.pull_request.base.ref, 'hotfix-') }} shell: bash - env: - GH_TOKEN: ${{ github.token }} - run: just ci_hack - - - name: Cargo build release - if: ${{ startsWith(github.event.pull_request.base.ref, 'hotfix-') }} - shell: bash - run: cargo check --features release + run: cargo check --features "release" typos: name: Spell check From e641ea29977916bc501f54417ae9f8a76f0979b4 Mon Sep 17 00:00:00 2001 From: Anurag Date: Wed, 15 Oct 2025 19:36:47 +0530 Subject: [PATCH 08/16] fix(connector): Add WASM Changes for Finix Google Pay (#9845) Co-authored-by: Anurag Singh --- .../connector_configs/toml/development.toml | 78 +++++++++++++++++-- crates/connector_configs/toml/production.toml | 78 +++++++++++++++++-- crates/connector_configs/toml/sandbox.toml | 78 +++++++++++++++++-- .../src/connectors/finix/transformers.rs | 20 ++--- .../connectors/finix/transformers/request.rs | 6 +- .../router/src/core/connector_validation.rs | 1 - .../router/tests/connectors/sample_auth.toml | 8 +- 7 files changed, 226 insertions(+), 43 deletions(-) diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 91213bf03f..6c87d88f13 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -7308,16 +7308,11 @@ required = true type = "Text" [finix] -[finix.connector_auth.SignatureKey] +[finix.connector_auth.MultiAuthKey] api_key = "Username" api_secret = "Password" key1 = "Merchant Id" -[finix.metadata.merchant_id] -name = "merchant_id" -label = "Merchant Identity Id" -placeholder = "Enter Merchant Identity Id" -required = true -type = "Text" +key2 = "Merchant Identity Id" [[finix.credit]] payment_method_type = "Mastercard" [[finix.credit]] @@ -7338,6 +7333,75 @@ payment_method_type = "Interac" payment_method_type = "Maestro" [[finix.wallet]] payment_method_type = "google_pay" +[[finix.metadata.google_pay]] + name = "merchant_name" + label = "Google Pay Merchant Name" + placeholder = "Enter Google Pay Merchant Name" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "merchant_id" + label = "Google Pay Merchant Id" + placeholder = "Enter Google Pay Merchant Id" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "gateway_merchant_id" + label = "Google Pay Merchant Key" + placeholder = "Enter Google Pay Merchant Key" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "allowed_auth_methods" + label = "Allowed Auth Methods" + placeholder = "Enter Allowed Auth Methods" + required = true + type = "MultiSelect" + options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] + +[[finix.connector_wallets_details.google_pay]] + name = "merchant_name" + label = "Google Pay Merchant Name" + placeholder = "Enter Google Pay Merchant Name" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "merchant_id" + label = "Google Pay Merchant Id" + placeholder = "Enter Google Pay Merchant Id" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "gateway_merchant_id" + label = "Google Pay Merchant Key" + placeholder = "Enter Google Pay Merchant Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "public_key" + label = "Google Pay Public Key" + placeholder = "Enter Google Pay Public Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "private_key" + label = "Google Pay Private Key" + placeholder = "Enter Google Pay Private Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "recipient_id" + label = "Recipient Id" + placeholder = "Enter Recipient Id" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "allowed_auth_methods" + label = "Allowed Auth Methods" + placeholder = "Enter Allowed Auth Methods" + required = true + type = "MultiSelect" + options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] [loonio] [loonio.connector_auth.BodyKey] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 6b151cbefc..5962c55929 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -6045,16 +6045,11 @@ required = true type = "Text" [finix] -[finix.connector_auth.SignatureKey] +[finix.connector_auth.MultiAuthKey] api_key = "Username" api_secret = "Password" key1 = "Merchant Id" -[finix.metadata.merchant_id] -name = "merchant_id" -label = "Merchant Identity Id" -placeholder = "Enter Merchant Identity Id" -required = true -type = "Text" +key2 = "Merchant Identity Id" [[finix.credit]] payment_method_type = "Mastercard" [[finix.credit]] @@ -6075,6 +6070,75 @@ payment_method_type = "Interac" payment_method_type = "Maestro" [[finix.wallet]] payment_method_type = "google_pay" +[[finix.metadata.google_pay]] + name = "merchant_name" + label = "Google Pay Merchant Name" + placeholder = "Enter Google Pay Merchant Name" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "merchant_id" + label = "Google Pay Merchant Id" + placeholder = "Enter Google Pay Merchant Id" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "gateway_merchant_id" + label = "Google Pay Merchant Key" + placeholder = "Enter Google Pay Merchant Key" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "allowed_auth_methods" + label = "Allowed Auth Methods" + placeholder = "Enter Allowed Auth Methods" + required = true + type = "MultiSelect" + options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] + +[[finix.connector_wallets_details.google_pay]] + name = "merchant_name" + label = "Google Pay Merchant Name" + placeholder = "Enter Google Pay Merchant Name" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "merchant_id" + label = "Google Pay Merchant Id" + placeholder = "Enter Google Pay Merchant Id" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "gateway_merchant_id" + label = "Google Pay Merchant Key" + placeholder = "Enter Google Pay Merchant Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "public_key" + label = "Google Pay Public Key" + placeholder = "Enter Google Pay Public Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "private_key" + label = "Google Pay Private Key" + placeholder = "Enter Google Pay Private Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "recipient_id" + label = "Recipient Id" + placeholder = "Enter Recipient Id" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "allowed_auth_methods" + label = "Allowed Auth Methods" + placeholder = "Enter Allowed Auth Methods" + required = true + type = "MultiSelect" + options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] [loonio] [loonio.connector_auth.BodyKey] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 30edc0d8c5..7b79421cc0 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -7284,16 +7284,11 @@ required = true type = "Text" [finix] -[finix.connector_auth.SignatureKey] +[finix.connector_auth.MultiAuthKey] api_key = "Username" api_secret = "Password" key1 = "Merchant Id" -[finix.metadata.merchant_id] -name = "merchant_id" -label = "Merchant Identity Id" -placeholder = "Enter Merchant Identity Id" -required = true -type = "Text" +key2 = "Merchant Identity Id" [[finix.credit]] payment_method_type = "Mastercard" [[finix.credit]] @@ -7314,6 +7309,75 @@ payment_method_type = "Interac" payment_method_type = "Maestro" [[finix.wallet]] payment_method_type = "google_pay" +[[finix.metadata.google_pay]] + name = "merchant_name" + label = "Google Pay Merchant Name" + placeholder = "Enter Google Pay Merchant Name" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "merchant_id" + label = "Google Pay Merchant Id" + placeholder = "Enter Google Pay Merchant Id" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "gateway_merchant_id" + label = "Google Pay Merchant Key" + placeholder = "Enter Google Pay Merchant Key" + required = true + type = "Text" +[[finix.metadata.google_pay]] + name = "allowed_auth_methods" + label = "Allowed Auth Methods" + placeholder = "Enter Allowed Auth Methods" + required = true + type = "MultiSelect" + options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] + +[[finix.connector_wallets_details.google_pay]] + name = "merchant_name" + label = "Google Pay Merchant Name" + placeholder = "Enter Google Pay Merchant Name" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "merchant_id" + label = "Google Pay Merchant Id" + placeholder = "Enter Google Pay Merchant Id" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "gateway_merchant_id" + label = "Google Pay Merchant Key" + placeholder = "Enter Google Pay Merchant Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "public_key" + label = "Google Pay Public Key" + placeholder = "Enter Google Pay Public Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "private_key" + label = "Google Pay Private Key" + placeholder = "Enter Google Pay Private Key" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "recipient_id" + label = "Recipient Id" + placeholder = "Enter Recipient Id" + required = true + type = "Text" +[[finix.connector_wallets_details.google_pay]] + name = "allowed_auth_methods" + label = "Allowed Auth Methods" + placeholder = "Enter Allowed Auth Methods" + required = true + type = "MultiSelect" + options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] [loonio] [loonio.connector_auth.BodyKey] diff --git a/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs b/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs index ea60f6d081..d65faedc0e 100644 --- a/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs @@ -29,7 +29,7 @@ use crate::{ types::{RefundsResponseRouterData, ResponseRouterData}, unimplemented_payment_method, utils::{ - self, get_unimplemented_payment_method_error_message, AddressDetailsData, CardData, + get_unimplemented_payment_method_error_message, AddressDetailsData, CardData, RouterData as _, }, }; @@ -41,17 +41,6 @@ pub struct FinixRouterData<'a, Flow, Req, Res> { pub merchant_identity_id: Secret, } -impl TryFrom<&Option> for FinixMeta { - type Error = error_stack::Report; - fn try_from( - meta_data: &Option, - ) -> Result { - let metadata = utils::to_connector_meta_from_secret::(meta_data.clone()) - .change_context(ConnectorError::InvalidConnectorConfig { config: "metadata" })?; - Ok(metadata) - } -} - impl<'a, Flow, Req, Res> TryFrom<(MinorUnit, &'a RouterData)> for FinixRouterData<'a, Flow, Req, Res> { @@ -60,13 +49,12 @@ impl<'a, Flow, Req, Res> TryFrom<(MinorUnit, &'a RouterData)> fn try_from(value: (MinorUnit, &'a RouterData)) -> Result { let (amount, router_data) = value; let auth = FinixAuthType::try_from(&router_data.connector_auth_type)?; - let connector_meta = FinixMeta::try_from(&router_data.connector_meta_data)?; Ok(Self { amount, router_data, merchant_id: auth.merchant_id, - merchant_identity_id: connector_meta.merchant_id, + merchant_identity_id: auth.merchant_identity_id, }) } } @@ -313,14 +301,16 @@ impl TryFrom<&ConnectorAuthType> for FinixAuthType { type Error = error_stack::Report; fn try_from(auth_type: &ConnectorAuthType) -> Result { match auth_type { - ConnectorAuthType::SignatureKey { + ConnectorAuthType::MultiAuthKey { api_key, key1, api_secret, + key2, } => Ok(Self { finix_user_name: api_key.clone(), finix_password: api_secret.clone(), merchant_id: key1.clone(), + merchant_identity_id: key2.clone(), }), _ => Err(ConnectorError::FailedToObtainAuthType.into()), } diff --git a/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs b/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs index a2392566d9..399cf95350 100644 --- a/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs +++ b/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs @@ -7,11 +7,6 @@ use serde::{Deserialize, Serialize}; use super::*; -#[derive(Deserialize)] -pub struct FinixMeta { - pub merchant_id: Secret, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FinixPaymentsRequest { pub amount: MinorUnit, @@ -224,4 +219,5 @@ pub struct FinixAuthType { pub finix_user_name: Secret, pub finix_password: Secret, pub merchant_id: Secret, + pub merchant_identity_id: Secret, } diff --git a/crates/router/src/core/connector_validation.rs b/crates/router/src/core/connector_validation.rs index af67a69c83..a38e55e420 100644 --- a/crates/router/src/core/connector_validation.rs +++ b/crates/router/src/core/connector_validation.rs @@ -588,7 +588,6 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { } api_enums::Connector::Finix => { finix::transformers::FinixAuthType::try_from(self.auth_type)?; - finix::transformers::FinixMeta::try_from(self.connector_meta_data)?; Ok(()) } } diff --git a/crates/router/tests/connectors/sample_auth.toml b/crates/router/tests/connectors/sample_auth.toml index 45ce89a56c..abcc3e3b8b 100644 --- a/crates/router/tests/connectors/sample_auth.toml +++ b/crates/router/tests/connectors/sample_auth.toml @@ -373,4 +373,10 @@ key1="Tenant ID" [tesouro] api_key="Client ID" -key1="Client Secret" \ No newline at end of file +key1="Client Secret" + +[finix] +api_key = "Username" +key1 = "Merchant Id" +key2 = "Merchant Identity Id" +api_secret = "Password" \ No newline at end of file From c563fbe1ce1058aa6616af67f1d9edde85b1534e Mon Sep 17 00:00:00 2001 From: Sayak Bhattacharya Date: Wed, 15 Oct 2025 20:50:47 +0530 Subject: [PATCH 09/16] fix(connector): [CALIDA] Treat Bluecode as an alias for Calida (#9817) Co-authored-by: Sayak Bhattacharya --- crates/common_enums/src/connector_enums.rs | 2 ++ crates/hyperswitch_connectors/src/connectors/calida.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 6b42837bc2..6604aa7a06 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -74,6 +74,7 @@ pub enum RoutableConnectors { Blackhawknetwork, Bamboraapac, Bluesnap, + #[serde(alias = "bluecode")] Calida, Boku, Braintree, @@ -250,6 +251,7 @@ pub enum Connector { Bitpay, Bluesnap, Blackhawknetwork, + #[serde(alias = "bluecode")] Calida, Boku, Braintree, diff --git a/crates/hyperswitch_connectors/src/connectors/calida.rs b/crates/hyperswitch_connectors/src/connectors/calida.rs index e374b7fddd..73dc952aca 100644 --- a/crates/hyperswitch_connectors/src/connectors/calida.rs +++ b/crates/hyperswitch_connectors/src/connectors/calida.rs @@ -742,7 +742,7 @@ static CALIDA_SUPPORTED_PAYMENT_METHODS: LazyLock = Laz static CALIDA_CONNECTOR_INFO: ConnectorInfo = ConnectorInfo { display_name: "Calida", - description: "Calida is building a global payment network that combines Alipay+, Discover and EMPSA and enables seamless payments in 75 countries. With over 160 million acceptance points, payments are processed according to the highest European security and data protection standards to make Europe less dependent on international players.", + description: "Calida Financial is a licensed e-money institution based in Malta and they provide customized financial infrastructure and payment solutions across the EU and EEA. As part of The Payments Group, it focuses on embedded finance, prepaid services, and next-generation digital payment products.", connector_type: enums::HyperswitchConnectorCategory::AlternativePaymentMethod, integration_status: enums::ConnectorIntegrationStatus::Alpha, }; From 5094ee50c2c17d59b70e64a802f07752a77112bb Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 16 Oct 2025 00:30:51 +0000 Subject: [PATCH 10/16] chore(version): 2025.10.16.0 --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b266cf305e..1b681a91ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,32 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2025.10.16.0 + +### Features + +- **connector:** [Peachpayments] Add Webhook Flow and Support For merchant_order_reference_id ([#9781](https://github.com/juspay/hyperswitch/pull/9781)) ([`6394c89`](https://github.com/juspay/hyperswitch/commit/6394c892cdfadf90e55bb524db719b547dc519ba)) +- Include response body for create_user_authentication_method ([#9653](https://github.com/juspay/hyperswitch/pull/9653)) ([`bebffa9`](https://github.com/juspay/hyperswitch/commit/bebffa97ffc66d9a4d3913ec884b3d3851735a4f)) +- Add attach_printable() for better error logging ([#9667](https://github.com/juspay/hyperswitch/pull/9667)) ([`f9bd87d`](https://github.com/juspay/hyperswitch/commit/f9bd87dd9b9ac3186bd08e779ae40327c9b2cf80)) + +### Bug Fixes + +- **connector:** + - Add WASM Changes for Finix Google Pay ([#9845](https://github.com/juspay/hyperswitch/pull/9845)) ([`e641ea2`](https://github.com/juspay/hyperswitch/commit/e641ea29977916bc501f54417ae9f8a76f0979b4)) + - [CALIDA] Treat Bluecode as an alias for Calida ([#9817](https://github.com/juspay/hyperswitch/pull/9817)) ([`c563fbe`](https://github.com/juspay/hyperswitch/commit/c563fbe1ce1058aa6616af67f1d9edde85b1534e)) + +### Refactors + +- **db_interfaces:** Move db interfaces in router to domain_models ([#9830](https://github.com/juspay/hyperswitch/pull/9830)) ([`5962833`](https://github.com/juspay/hyperswitch/commit/59628332de7053c8a4cc3901b02571ed7f0d698b)) + +### Miscellaneous Tasks + +- Added explicit docker.io registry to all images in docker-compose.yml ([#9771](https://github.com/juspay/hyperswitch/pull/9771)) ([`3296f62`](https://github.com/juspay/hyperswitch/commit/3296f6260623b024248b898a81ab56ccc767a3dd)) + +**Full Changelog:** [`2025.10.15.0...2025.10.16.0`](https://github.com/juspay/hyperswitch/compare/2025.10.15.0...2025.10.16.0) + +- - - + ## 2025.10.15.0 ### Features From ecf702aba92bec721ff7e08095739f3809c6c525 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:05:31 +0530 Subject: [PATCH 11/16] feat(router): add pre-confirm payments eligibility api (#9774) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference/v1/openapi_spec_v1.json | 78 +++++++-- api-reference/v2/openapi_spec_v2.json | 63 ++++++- crates/api_models/src/events/payment.rs | 18 ++ crates/api_models/src/payments.rs | 14 +- crates/router/src/core/blocklist/utils.rs | 73 ++++---- crates/router/src/core/payments.rs | 175 +++++++++++++++++++ crates/router/src/routes/app.rs | 4 + crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/routes/payments.rs | 46 +++++ crates/router/src/services/authentication.rs | 9 + crates/router/src/types/api/payments.rs | 17 +- crates/router_env/src/logger/types.rs | 2 + 12 files changed, 439 insertions(+), 63 deletions(-) diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index 58b315e411..c48b122e0d 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -19542,13 +19542,62 @@ } }, "NextActionCall": { - "type": "string", - "enum": [ - "post_session_tokens", - "confirm", - "sync", - "complete_authorize", - "await_merchant_callback" + "oneOf": [ + { + "type": "string", + "description": "The next action call is Post Session Tokens", + "enum": [ + "post_session_tokens" + ] + }, + { + "type": "string", + "description": "The next action call is confirm", + "enum": [ + "confirm" + ] + }, + { + "type": "string", + "description": "The next action call is sync", + "enum": [ + "sync" + ] + }, + { + "type": "string", + "description": "The next action call is Complete Authorize", + "enum": [ + "complete_authorize" + ] + }, + { + "type": "string", + "description": "The next action is to await for a merchant callback", + "enum": [ + "await_merchant_callback" + ] + }, + { + "type": "object", + "required": [ + "deny" + ], + "properties": { + "deny": { + "type": "object", + "description": "The next action is to deny the payment with an error message", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } ] }, "NextActionData": { @@ -24552,14 +24601,17 @@ }, "PaymentsEligibilityResponse": { "type": "object", + "required": [ + "payment_id", + "sdk_next_action" + ], "properties": { + "payment_id": { + "type": "string", + "description": "The identifier for the payment" + }, "sdk_next_action": { - "allOf": [ - { - "$ref": "#/components/schemas/SdkNextAction" - } - ], - "nullable": true + "$ref": "#/components/schemas/SdkNextAction" } } }, diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index d4094e383a..7a6803d508 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -15547,13 +15547,62 @@ } }, "NextActionCall": { - "type": "string", - "enum": [ - "post_session_tokens", - "confirm", - "sync", - "complete_authorize", - "await_merchant_callback" + "oneOf": [ + { + "type": "string", + "description": "The next action call is Post Session Tokens", + "enum": [ + "post_session_tokens" + ] + }, + { + "type": "string", + "description": "The next action call is confirm", + "enum": [ + "confirm" + ] + }, + { + "type": "string", + "description": "The next action call is sync", + "enum": [ + "sync" + ] + }, + { + "type": "string", + "description": "The next action call is Complete Authorize", + "enum": [ + "complete_authorize" + ] + }, + { + "type": "string", + "description": "The next action is to await for a merchant callback", + "enum": [ + "await_merchant_callback" + ] + }, + { + "type": "object", + "required": [ + "deny" + ], + "properties": { + "deny": { + "type": "object", + "description": "The next action is to deny the payment with an error message", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } ] }, "NextActionData": { diff --git a/crates/api_models/src/events/payment.rs b/crates/api_models/src/events/payment.rs index ad10722517..104fe4e089 100644 --- a/crates/api_models/src/events/payment.rs +++ b/crates/api_models/src/events/payment.rs @@ -174,6 +174,24 @@ impl ApiEventMetric for payments::PaymentsRequest { } } +#[cfg(feature = "v1")] +impl ApiEventMetric for payments::PaymentsEligibilityRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + +#[cfg(feature = "v1")] +impl ApiEventMetric for payments::PaymentsEligibilityResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Payment { + payment_id: self.payment_id.clone(), + }) + } +} + #[cfg(feature = "v2")] impl ApiEventMetric for PaymentsCreateIntentRequest { fn get_api_event_type(&self) -> Option { diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 6a51e5c5d7..937cdaf98d 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -8064,6 +8064,8 @@ pub enum NextActionCall { CompleteAuthorize, /// The next action is to await for a merchant callback AwaitMerchantCallback, + /// The next action is to deny the payment with an error message + Deny { message: String }, } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, serde::Deserialize, ToSchema)] @@ -9334,9 +9336,13 @@ pub struct ClickToPaySessionResponse { #[derive(Debug, serde::Deserialize, Clone, ToSchema)] pub struct PaymentsEligibilityRequest { + /// The identifier for the payment + /// Added in the payload for ApiEventMetrics, populated from the path param + #[serde(skip)] + pub payment_id: id_type::PaymentId, /// Token used for client side verification #[schema(value_type = String, example = "pay_U42c409qyHwOkWo3vK60_secret_el9ksDkiB8hi6j9N78yo")] - pub client_secret: Secret, + pub client_secret: Option>, /// The payment method to be used for the payment #[schema(value_type = PaymentMethod, example = "wallet")] pub payment_method_type: api_enums::PaymentMethod, @@ -9349,7 +9355,11 @@ pub struct PaymentsEligibilityRequest { #[derive(Debug, serde::Serialize, Clone, ToSchema)] pub struct PaymentsEligibilityResponse { - pub sdk_next_action: Option, + /// The identifier for the payment + #[schema(value_type = String)] + pub payment_id: id_type::PaymentId, + /// Next action to be performed by the SDK + pub sdk_next_action: SdkNextAction, } #[cfg(feature = "v1")] diff --git a/crates/router/src/core/blocklist/utils.rs b/crates/router/src/core/blocklist/utils.rs index a00c4b5beb..c849133b72 100644 --- a/crates/router/src/core/blocklist/utils.rs +++ b/crates/router/src/core/blocklist/utils.rs @@ -290,45 +290,40 @@ async fn delete_card_bin_blocklist_entry( }) } -pub async fn validate_data_for_blocklist( +pub async fn should_payment_be_blocked( state: &SessionState, merchant_context: &domain::MerchantContext, - payment_data: &mut PaymentData, -) -> CustomResult -where - F: Send + Clone, -{ + payment_method_data: &Option, +) -> CustomResult { let db = &state.store; let merchant_id = merchant_context.get_merchant_account().get_id(); let merchant_fingerprint_secret = get_merchant_fingerprint_secret(state, merchant_id).await?; // Hashed Fingerprint to check whether or not this payment should be blocked. - let card_number_fingerprint = if let Some(domain::PaymentMethodData::Card(card)) = - payment_data.payment_method_data.as_ref() - { - generate_fingerprint( - state, - StrongSecret::new(card.card_number.get_card_no()), - StrongSecret::new(merchant_fingerprint_secret.clone()), - api_models::enums::LockerChoice::HyperswitchCardVault, - ) - .await - .attach_printable("error in pm fingerprint creation") - .map_or_else( - |error| { - logger::error!(?error); - None - }, - Some, - ) - .map(|payload| payload.card_fingerprint) - } else { - None - }; + let card_number_fingerprint = + if let Some(domain::PaymentMethodData::Card(card)) = payment_method_data { + generate_fingerprint( + state, + StrongSecret::new(card.card_number.get_card_no()), + StrongSecret::new(merchant_fingerprint_secret.clone()), + api_models::enums::LockerChoice::HyperswitchCardVault, + ) + .await + .attach_printable("error in pm fingerprint creation") + .map_or_else( + |error| { + logger::error!(?error); + None + }, + Some, + ) + .map(|payload| payload.card_fingerprint) + } else { + None + }; // Hashed Cardbin to check whether or not this payment should be blocked. - let card_bin_fingerprint = payment_data - .payment_method_data + let card_bin_fingerprint = payment_method_data .as_ref() .and_then(|pm_data| match pm_data { domain::PaymentMethodData::Card(card) => Some(card.card_number.get_card_isin()), @@ -337,8 +332,7 @@ where // Hashed Extended Cardbin to check whether or not this payment should be blocked. let extended_card_bin_fingerprint = - payment_data - .payment_method_data + payment_method_data .as_ref() .and_then(|pm_data| match pm_data { domain::PaymentMethodData::Card(card) => { @@ -385,6 +379,21 @@ where } } } + Ok(should_payment_be_blocked) +} + +pub async fn validate_data_for_blocklist( + state: &SessionState, + merchant_context: &domain::MerchantContext, + payment_data: &mut PaymentData, +) -> CustomResult +where + F: Send + Clone, +{ + let db = &state.store; + let should_payment_be_blocked = + should_payment_be_blocked(state, merchant_context, &payment_data.payment_method_data) + .await?; if should_payment_be_blocked { // Update db for attempt and intent status. db.update_payment_intent( diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 7c953a5325..903dfa58d6 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -104,6 +104,8 @@ use super::{ }, }; #[cfg(feature = "v1")] +use crate::core::blocklist::utils as blocklist_utils; +#[cfg(feature = "v1")] use crate::core::debit_routing; #[cfg(feature = "frm")] use crate::core::fraud_check as frm_core; @@ -10986,6 +10988,179 @@ pub async fn payments_manual_update( )) } +// Trait for Eligibility Checks +#[cfg(feature = "v1")] +#[async_trait::async_trait] +trait EligibilityCheck { + type Output; + + // Determine if the check should be run based on the runtime checks + async fn should_run( + &self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + ) -> CustomResult; + + // Run the actual check and return the SDK Next Action if applicable + async fn execute_check( + &self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + payment_method_data: &Option, + ) -> CustomResult; + + fn transform(output: Self::Output) -> Option; +} + +// Result of an Eligibility Check +#[cfg(feature = "v1")] +#[derive(Debug, Clone)] +pub enum CheckResult { + Allow, + Deny { message: String }, +} + +#[cfg(feature = "v1")] +impl From for Option { + fn from(result: CheckResult) -> Self { + match result { + CheckResult::Allow => None, + CheckResult::Deny { message } => Some(api_models::payments::SdkNextAction { + next_action: api_models::payments::NextActionCall::Deny { message }, + }), + } + } +} + +// Perform Blocklist Check for the Card Number provided in Payment Method Data +#[cfg(feature = "v1")] +struct BlockListCheck; + +#[cfg(feature = "v1")] +#[async_trait::async_trait] +impl EligibilityCheck for BlockListCheck { + type Output = CheckResult; + + async fn should_run( + &self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + ) -> CustomResult { + let merchant_id = merchant_context.get_merchant_account().get_id(); + let blocklist_enabled_key = merchant_id.get_blocklist_guard_key(); + let blocklist_guard_enabled = state + .store + .find_config_by_key_unwrap_or(&blocklist_enabled_key, Some("false".to_string())) + .await; + + Ok(match blocklist_guard_enabled { + Ok(config) => serde_json::from_str(&config.config).unwrap_or(false), + + // If it is not present in db we are defaulting it to false + Err(inner) => { + if !inner.current_context().is_db_not_found() { + logger::error!("Error fetching guard blocklist enabled config {:?}", inner); + } + false + } + }) + } + + async fn execute_check( + &self, + state: &SessionState, + merchant_context: &domain::MerchantContext, + payment_method_data: &Option, + ) -> CustomResult { + let should_payment_be_blocked = blocklist_utils::should_payment_be_blocked( + state, + merchant_context, + payment_method_data, + ) + .await?; + if should_payment_be_blocked { + Ok(CheckResult::Deny { + message: "Card number is blocklisted".to_string(), + }) + } else { + Ok(CheckResult::Allow) + } + } + + fn transform(output: CheckResult) -> Option { + output.into() + } +} + +// Eligibility Pipeline to run all the eligibility checks in sequence +#[cfg(feature = "v1")] +pub struct EligibilityHandler { + state: SessionState, + merchant_context: domain::MerchantContext, + payment_method_data: Option, +} + +#[cfg(feature = "v1")] +impl EligibilityHandler { + fn new( + state: SessionState, + merchant_context: domain::MerchantContext, + payment_method_data: Option, + ) -> Self { + Self { + state, + merchant_context, + payment_method_data, + } + } + + async fn run_check( + &self, + check: C, + ) -> CustomResult, errors::ApiErrorResponse> { + let should_run = check + .should_run(&self.state, &self.merchant_context) + .await?; + Ok(match should_run { + true => check + .execute_check( + &self.state, + &self.merchant_context, + &self.payment_method_data, + ) + .await + .map(C::transform)?, + false => None, + }) + } +} + +#[cfg(all(feature = "oltp", feature = "v1"))] +pub async fn payments_submit_eligibility( + state: SessionState, + merchant_context: domain::MerchantContext, + req: api_models::payments::PaymentsEligibilityRequest, + payment_id: id_type::PaymentId, +) -> RouterResponse { + let payment_method_data = req + .payment_method_data + .payment_method_data + .map(domain::PaymentMethodData::from); + let eligibility_handler = EligibilityHandler::new(state, merchant_context, payment_method_data); + let sdk_next_action = eligibility_handler + .run_check(BlockListCheck) + .await? + .unwrap_or(api_models::payments::SdkNextAction { + next_action: api_models::payments::NextActionCall::Confirm, + }); + Ok(services::ApplicationResponse::Json( + api_models::payments::PaymentsEligibilityResponse { + payment_id, + sdk_next_action, + }, + )) +} + pub trait PaymentMethodChecker { fn should_update_in_post_update_tracker(&self) -> bool; fn should_update_in_update_tracker(&self) -> bool; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 36a1b02b61..f575f80af1 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -915,6 +915,10 @@ impl Payments { web::resource("/{payment_id}/reject") .route(web::post().to(payments::payments_reject)), ) + .service( + web::resource("/{payment_id}/eligibility") + .route(web::post().to(payments::payments_submit_eligibility)), + ) .service( web::resource("/redirect/{payment_id}/{merchant_id}/{attempt_id}") .route(web::get().to(payments::payments_start)), diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index e7c9f97c00..a8b7294c1c 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -170,7 +170,8 @@ impl From for ApiIdentifier { | Flow::ProxyConfirmIntent | Flow::PaymentsRetrieveUsingMerchantReferenceId | Flow::PaymentAttemptsList - | Flow::RecoveryPaymentsCreate => Self::Payments, + | Flow::RecoveryPaymentsCreate + | Flow::PaymentsSubmitEligibility => Self::Payments, Flow::PayoutsCreate | Flow::PayoutsRetrieve | Flow::PayoutsUpdate diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 0cbfeff4c9..c790533ec5 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -2453,6 +2453,52 @@ pub async fn retrieve_extended_card_info( .await } +#[cfg(all(feature = "oltp", feature = "v1"))] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsSubmitEligibility, payment_id))] +pub async fn payments_submit_eligibility( + state: web::Data, + http_req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let flow = Flow::PaymentsSubmitEligibility; + let payment_id = path.into_inner(); + let mut payload = json_payload.into_inner(); + payload.payment_id = payment_id.clone(); + + let api_auth = auth::ApiKeyAuth { + is_connected_allowed: false, + is_platform_allowed: true, + }; + + let (auth_type, _auth_flow) = + match auth::check_client_secret_and_get_auth(http_req.headers(), &payload, api_auth) { + Ok(auth) => auth, + Err(err) => return api::log_and_return_error_response(report!(err)), + }; + + Box::pin(api::server_wrap( + flow, + state, + &http_req, + payment_id, + |state, auth: auth::AuthenticationData, payment_id, _| { + let merchant_context = domain::MerchantContext::NormalMerchant(Box::new( + domain::Context(auth.merchant_account, auth.key_store), + )); + payments::payments_submit_eligibility( + state, + merchant_context, + payload.clone(), + payment_id, + ) + }, + &*auth_type, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "v1")] pub fn get_or_generate_payment_id( payload: &mut payment_types::PaymentsRequest, diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 042dac243b..dcdb1ffe6f 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -4261,6 +4261,15 @@ impl ClientSecretFetch for payments::PaymentsRequest { } } +#[cfg(feature = "v1")] +impl ClientSecretFetch for payments::PaymentsEligibilityRequest { + fn get_client_secret(&self) -> Option<&String> { + self.client_secret + .as_ref() + .map(|client_secret| client_secret.peek()) + } +} + #[cfg(feature = "v1")] impl ClientSecretFetch for api_models::blocklist::ListBlocklistQuery { fn get_client_secret(&self) -> Option<&String> { diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 4b3f448de1..bbafc1af3d 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -23,14 +23,15 @@ pub use api_models::{ PaymentsAggregateResponse, PaymentsApproveRequest, PaymentsCancelPostCaptureRequest, PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsCompleteAuthorizeRequest, PaymentsDynamicTaxCalculationRequest, PaymentsDynamicTaxCalculationResponse, - PaymentsExternalAuthenticationRequest, PaymentsIncrementalAuthorizationRequest, - PaymentsManualUpdateRequest, PaymentsPostSessionTokensRequest, - PaymentsPostSessionTokensResponse, PaymentsRedirectRequest, PaymentsRedirectionResponse, - PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, PaymentsResponseForm, - PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, - PaymentsStartRequest, PaymentsUpdateMetadataRequest, PaymentsUpdateMetadataResponse, - PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, UrlDetails, - VaultSessionDetails, VerifyRequest, VerifyResponse, VgsSessionDetails, WalletData, + PaymentsEligibilityRequest, PaymentsExternalAuthenticationRequest, + PaymentsIncrementalAuthorizationRequest, PaymentsManualUpdateRequest, + PaymentsPostSessionTokensRequest, PaymentsPostSessionTokensResponse, + PaymentsRedirectRequest, PaymentsRedirectionResponse, PaymentsRejectRequest, + PaymentsRequest, PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, + PaymentsSessionRequest, PaymentsSessionResponse, PaymentsStartRequest, + PaymentsUpdateMetadataRequest, PaymentsUpdateMetadataResponse, PgRedirectResponse, + PhoneDetails, RedirectionResponse, SessionToken, UrlDetails, VaultSessionDetails, + VerifyRequest, VerifyResponse, VgsSessionDetails, WalletData, }, }; pub use common_types::payments::{AcceptanceType, CustomerAcceptance, OnlineMandate}; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 81975b2df0..1968447a16 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -678,6 +678,8 @@ pub enum Flow { RevenueRecoveryRedis, /// Gift card balance check flow GiftCardBalanceCheck, + /// Payments Submit Eligibility flow + PaymentsSubmitEligibility, } /// Trait for providing generic behaviour to flow metric From 1f34f89063f310940f44a589730d4333325dc898 Mon Sep 17 00:00:00 2001 From: Kanika Bansal Date: Thu, 16 Oct 2025 12:08:26 +0530 Subject: [PATCH 12/16] refactor(users): remove deprecated permission groups (#9604) --- crates/common_enums/src/enums.rs | 6 ----- .../router/src/services/authorization/info.rs | 5 ++-- .../authorization/permission_groups.rs | 16 +---------- .../authorization/roles/predefined_roles.rs | 27 ------------------- crates/router/src/utils/user_role.rs | 4 +-- .../down.sql | 2 ++ .../up.sql | 12 +++++++++ 7 files changed, 18 insertions(+), 54 deletions(-) create mode 100644 migrations/2025-10-06-111411_deprecated_roles_backfill/down.sql create mode 100644 migrations/2025-10-06-111411_deprecated_roles_backfill/up.sql diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index a42a266114..2cbd3a9e54 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -8420,12 +8420,6 @@ pub enum PermissionGroup { AnalyticsView, UsersView, UsersManage, - // TODO: To be deprecated, make sure DB is migrated before removing - MerchantDetailsView, - // TODO: To be deprecated, make sure DB is migrated before removing - MerchantDetailsManage, - // TODO: To be deprecated, make sure DB is migrated before removing - OrganizationManage, AccountView, AccountManage, ReconReportsView, diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index 49c315221c..a76f24bee1 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -41,9 +41,8 @@ fn get_group_description(group: PermissionGroup) -> Option<&'static str> { PermissionGroup::AnalyticsView => Some("View Analytics"), PermissionGroup::UsersView => Some("View Users"), PermissionGroup::UsersManage => Some("Manage and invite Users to the Team"), - PermissionGroup::MerchantDetailsView | PermissionGroup::AccountView => Some("View Merchant Details"), - PermissionGroup::MerchantDetailsManage | PermissionGroup::AccountManage => Some("Create, modify and delete Merchant Details like api keys, webhooks, etc"), - PermissionGroup::OrganizationManage => Some("Manage organization level tasks like create new Merchant accounts, Organization level roles, etc"), + PermissionGroup::AccountView => Some("View Merchant Details"), + PermissionGroup::AccountManage => Some("Create, modify and delete Merchant Details like api keys, webhooks, etc"), PermissionGroup::ReconReportsView => Some("View reconciliation reports and analytics"), PermissionGroup::ReconReportsManage => Some("Manage reconciliation reports"), PermissionGroup::ReconOpsView => Some("View and access all reconciliation operations including reports and analytics"), diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs index 6a2ce1c839..7767373dce 100644 --- a/crates/router/src/services/authorization/permission_groups.rs +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -20,7 +20,6 @@ impl PermissionGroupExt for PermissionGroup { | Self::WorkflowsView | Self::AnalyticsView | Self::UsersView - | Self::MerchantDetailsView | Self::AccountView | Self::ReconOpsView | Self::ReconReportsView @@ -30,8 +29,6 @@ impl PermissionGroupExt for PermissionGroup { | Self::ConnectorsManage | Self::WorkflowsManage | Self::UsersManage - | Self::MerchantDetailsManage - | Self::OrganizationManage | Self::AccountManage | Self::ReconOpsManage | Self::ReconReportsManage @@ -47,11 +44,7 @@ impl PermissionGroupExt for PermissionGroup { Self::WorkflowsView | Self::WorkflowsManage => ParentGroup::Workflows, Self::AnalyticsView => ParentGroup::Analytics, Self::UsersView | Self::UsersManage => ParentGroup::Users, - Self::MerchantDetailsView - | Self::OrganizationManage - | Self::MerchantDetailsManage - | Self::AccountView - | Self::AccountManage => ParentGroup::Account, + Self::AccountView | Self::AccountManage => ParentGroup::Account, Self::ThemeView | Self::ThemeManage => ParentGroup::Theme, Self::ReconOpsView | Self::ReconOpsManage => ParentGroup::ReconOps, @@ -96,13 +89,6 @@ impl PermissionGroupExt for PermissionGroup { Self::ReconReportsView => vec![Self::ReconReportsView], Self::ReconReportsManage => vec![Self::ReconReportsView, Self::ReconReportsManage], - Self::MerchantDetailsView => vec![Self::MerchantDetailsView], - Self::MerchantDetailsManage => { - vec![Self::MerchantDetailsView, Self::MerchantDetailsManage] - } - - Self::OrganizationManage => vec![Self::OrganizationManage], - Self::AccountView => vec![Self::AccountView], Self::AccountManage => vec![Self::AccountView, Self::AccountManage], diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs index 853b4d3f8a..83e1b94a22 100644 --- a/crates/router/src/services/authorization/roles/predefined_roles.rs +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -22,11 +22,8 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::AnalyticsView, PermissionGroup::UsersView, PermissionGroup::UsersManage, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, - PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, - PermissionGroup::OrganizationManage, PermissionGroup::ReconOpsView, PermissionGroup::ReconOpsManage, PermissionGroup::ReconReportsView, @@ -51,7 +48,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::WorkflowsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, PermissionGroup::ReconOpsView, PermissionGroup::ReconReportsView, @@ -75,7 +71,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::WorkflowsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, PermissionGroup::ReconOpsView, PermissionGroup::ReconReportsView, @@ -106,11 +101,8 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::AnalyticsView, PermissionGroup::UsersView, PermissionGroup::UsersManage, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, - PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, - PermissionGroup::OrganizationManage, PermissionGroup::ReconOpsView, PermissionGroup::ReconOpsManage, PermissionGroup::ReconReportsView, @@ -141,11 +133,8 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::AnalyticsView, PermissionGroup::UsersView, PermissionGroup::UsersManage, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, - PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, - PermissionGroup::OrganizationManage, PermissionGroup::ReconOpsView, PermissionGroup::ReconOpsManage, PermissionGroup::ReconReportsView, @@ -178,9 +167,7 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::AnalyticsView, PermissionGroup::UsersView, PermissionGroup::UsersManage, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, - PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, PermissionGroup::ReconOpsView, PermissionGroup::ReconOpsManage, @@ -206,7 +193,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::WorkflowsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, PermissionGroup::ReconOpsView, PermissionGroup::ReconReportsView, @@ -229,7 +215,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::AnalyticsView, PermissionGroup::UsersView, PermissionGroup::UsersManage, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, ], role_id: consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN.to_string(), @@ -250,9 +235,7 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::ConnectorsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, - PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, PermissionGroup::ReconOpsView, PermissionGroup::ReconReportsView, @@ -277,7 +260,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::WorkflowsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, PermissionGroup::ReconOpsView, PermissionGroup::ReconOpsManage, @@ -300,7 +282,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::OperationsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, PermissionGroup::ReconOpsView, PermissionGroup::ReconReportsView, @@ -330,9 +311,7 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::AnalyticsView, PermissionGroup::UsersView, PermissionGroup::UsersManage, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, - PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, ], role_id: consts::user_role::ROLE_ID_PROFILE_ADMIN.to_string(), @@ -354,7 +333,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::WorkflowsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, ], role_id: consts::user_role::ROLE_ID_PROFILE_VIEW_ONLY.to_string(), @@ -375,7 +353,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::AnalyticsView, PermissionGroup::UsersView, PermissionGroup::UsersManage, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, ], role_id: consts::user_role::ROLE_ID_PROFILE_IAM_ADMIN.to_string(), @@ -396,9 +373,7 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::ConnectorsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, - PermissionGroup::MerchantDetailsManage, PermissionGroup::AccountManage, ], role_id: consts::user_role::ROLE_ID_PROFILE_DEVELOPER.to_string(), @@ -421,7 +396,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::WorkflowsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, ], role_id: consts::user_role::ROLE_ID_PROFILE_OPERATOR.to_string(), @@ -441,7 +415,6 @@ pub static PREDEFINED_ROLES: LazyLock> = LazyLoc PermissionGroup::OperationsView, PermissionGroup::AnalyticsView, PermissionGroup::UsersView, - PermissionGroup::MerchantDetailsView, PermissionGroup::AccountView, ], role_id: consts::user_role::ROLE_ID_PROFILE_CUSTOMER_SUPPORT.to_string(), diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index f9a5f77807..659460e53b 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -40,9 +40,7 @@ pub fn validate_role_groups(groups: &[PermissionGroup]) -> UserResult<()> { let unique_groups: HashSet<_> = groups.iter().copied().collect(); - if unique_groups.contains(&PermissionGroup::OrganizationManage) - || unique_groups.contains(&PermissionGroup::InternalManage) - { + if unique_groups.contains(&PermissionGroup::InternalManage) { return Err(report!(UserErrors::InvalidRoleOperation)) .attach_printable("Invalid groups present in the custom role"); } diff --git a/migrations/2025-10-06-111411_deprecated_roles_backfill/down.sql b/migrations/2025-10-06-111411_deprecated_roles_backfill/down.sql new file mode 100644 index 0000000000..c7c9cbeb40 --- /dev/null +++ b/migrations/2025-10-06-111411_deprecated_roles_backfill/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +SELECT 1; \ No newline at end of file diff --git a/migrations/2025-10-06-111411_deprecated_roles_backfill/up.sql b/migrations/2025-10-06-111411_deprecated_roles_backfill/up.sql new file mode 100644 index 0000000000..1fb2b9fb0d --- /dev/null +++ b/migrations/2025-10-06-111411_deprecated_roles_backfill/up.sql @@ -0,0 +1,12 @@ +-- Your SQL goes here +UPDATE roles +SET groups = array_replace(groups, 'merchant_details_view', 'account_view') +WHERE 'merchant_details_view' = ANY(groups); + +UPDATE roles +SET groups = array_replace(groups, 'merchant_details_manage', 'account_manage') +WHERE 'merchant_details_manage' = ANY(groups); + +UPDATE roles +SET groups = array_replace(groups, 'organization_manage', 'account_manage') +WHERE 'organization_manage' = ANY(groups); \ No newline at end of file From e7dee751b58ec3b377d28fab7c1a7ea83aec14d0 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:29:32 +0530 Subject: [PATCH 13/16] feat(payouts): apple pay decrypt payout (#9857) --- api-reference/v1/openapi_spec_v1.json | 72 ++++++- api-reference/v2/openapi_spec_v2.json | 72 ++++++- crates/api_models/src/enums.rs | 4 + crates/api_models/src/payouts.rs | 32 +++ crates/common_enums/src/connector_enums.rs | 1 + .../common_utils/src/payout_method_utils.rs | 21 ++ crates/connector_configs/src/connector.rs | 3 + .../connector_configs/toml/development.toml | 15 ++ crates/connector_configs/toml/production.toml | 14 ++ crates/connector_configs/toml/sandbox.toml | 14 ++ .../src/connectors/adyen/transformers.rs | 6 + .../src/connectors/paypal/transformers.rs | 4 + .../src/connectors/worldpay.rs | 138 +++++++++++++ .../connectors/worldpay/payout_requests.rs | 66 ++++++ .../connectors/worldpay/payout_response.rs | 16 ++ .../worldpay/payout_transformers.rs | 193 ++++++++++++++++++ .../src/default_implementations.rs | 2 - crates/hyperswitch_connectors/src/utils.rs | 109 ++++++++++ crates/openapi/src/openapi.rs | 2 + crates/openapi/src/openapi_v2.rs | 2 + .../router/src/core/payment_methods/vault.rs | 35 ++++ crates/router/src/types/transformers.rs | 1 + 22 files changed, 818 insertions(+), 4 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/worldpay/payout_requests.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/worldpay/payout_response.rs create mode 100644 crates/hyperswitch_connectors/src/connectors/worldpay/payout_transformers.rs diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index c48b122e0d..00b5183c28 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -7776,6 +7776,61 @@ } } }, + "ApplePayDecrypt": { + "type": "object", + "required": [ + "dpan", + "expiry_month", + "expiry_year", + "card_holder_name" + ], + "properties": { + "dpan": { + "type": "string", + "description": "The dpan number associated with card number", + "example": "4242424242424242" + }, + "expiry_month": { + "type": "string", + "description": "The card's expiry month" + }, + "expiry_year": { + "type": "string", + "description": "The card's expiry year" + }, + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Doe" + } + } + }, + "ApplePayDecryptAdditionalData": { + "type": "object", + "description": "Masked payout method details for Apple pay decrypt wallet payout method", + "required": [ + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_exp_month": { + "type": "string", + "description": "Card expiry month", + "example": "01" + }, + "card_exp_year": { + "type": "string", + "description": "Card expiry year", + "example": "2026" + }, + "card_holder_name": { + "type": "string", + "description": "Card holder name", + "example": "John Doe" + } + } + }, "ApplePayPaymentData": { "oneOf": [ { @@ -26991,7 +27046,8 @@ "payone", "paypal", "stripe", - "wise" + "wise", + "worldpay" ] }, "PayoutCreatePayoutLinkConfig": { @@ -33457,6 +33513,17 @@ }, "Wallet": { "oneOf": [ + { + "type": "object", + "required": [ + "apple_pay_decrypt" + ], + "properties": { + "apple_pay_decrypt": { + "$ref": "#/components/schemas/ApplePayDecrypt" + } + } + }, { "type": "object", "required": [ @@ -33488,6 +33555,9 @@ }, { "$ref": "#/components/schemas/VenmoAdditionalData" + }, + { + "$ref": "#/components/schemas/ApplePayDecryptAdditionalData" } ], "description": "Masked payout method details for wallet payout method" diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 7a6803d508..d56065e6dc 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -4808,6 +4808,61 @@ } } }, + "ApplePayDecrypt": { + "type": "object", + "required": [ + "dpan", + "expiry_month", + "expiry_year", + "card_holder_name" + ], + "properties": { + "dpan": { + "type": "string", + "description": "The dpan number associated with card number", + "example": "4242424242424242" + }, + "expiry_month": { + "type": "string", + "description": "The card's expiry month" + }, + "expiry_year": { + "type": "string", + "description": "The card's expiry year" + }, + "card_holder_name": { + "type": "string", + "description": "The card holder's name", + "example": "John Doe" + } + } + }, + "ApplePayDecryptAdditionalData": { + "type": "object", + "description": "Masked payout method details for Apple pay decrypt wallet payout method", + "required": [ + "card_exp_month", + "card_exp_year", + "card_holder_name" + ], + "properties": { + "card_exp_month": { + "type": "string", + "description": "Card expiry month", + "example": "01" + }, + "card_exp_year": { + "type": "string", + "description": "Card expiry year", + "example": "2026" + }, + "card_holder_name": { + "type": "string", + "description": "Card holder name", + "example": "John Doe" + } + } + }, "ApplePayPaymentData": { "oneOf": [ { @@ -21075,7 +21130,8 @@ "payone", "paypal", "stripe", - "wise" + "wise", + "worldpay" ] }, "PayoutCreatePayoutLinkConfig": { @@ -26999,6 +27055,17 @@ }, "Wallet": { "oneOf": [ + { + "type": "object", + "required": [ + "apple_pay_decrypt" + ], + "properties": { + "apple_pay_decrypt": { + "$ref": "#/components/schemas/ApplePayDecrypt" + } + } + }, { "type": "object", "required": [ @@ -27030,6 +27097,9 @@ }, { "$ref": "#/components/schemas/VenmoAdditionalData" + }, + { + "$ref": "#/components/schemas/ApplePayDecryptAdditionalData" } ], "description": "Masked payout method details for wallet payout method" diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index ed1bf12a24..1f3f5bdd15 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -56,6 +56,7 @@ pub enum PayoutConnectors { Paypal, Stripe, Wise, + Worldpay, } #[cfg(feature = "v2")] @@ -85,6 +86,7 @@ impl From for RoutableConnectors { PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Stripe => Self::Stripe, PayoutConnectors::Wise => Self::Wise, + PayoutConnectors::Worldpay => Self::Worldpay, } } } @@ -105,6 +107,7 @@ impl From for Connector { PayoutConnectors::Paypal => Self::Paypal, PayoutConnectors::Stripe => Self::Stripe, PayoutConnectors::Wise => Self::Wise, + PayoutConnectors::Worldpay => Self::Worldpay, } } } @@ -125,6 +128,7 @@ impl TryFrom for PayoutConnectors { Connector::Paypal => Ok(Self::Paypal), Connector::Stripe => Ok(Self::Stripe), Connector::Wise => Ok(Self::Wise), + Connector::Worldpay => Ok(Self::Worldpay), _ => Err(format!("Invalid payout connector {value}")), } } diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 03b5380416..fc41273288 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -375,6 +375,7 @@ pub struct PixBankTransfer { #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] #[serde(rename_all = "snake_case")] pub enum Wallet { + ApplePayDecrypt(ApplePayDecrypt), Paypal(Paypal), Venmo(Venmo), } @@ -414,6 +415,25 @@ pub struct Venmo { pub telephone_number: Option>, } +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +pub struct ApplePayDecrypt { + /// The dpan number associated with card number + #[schema(value_type = String, example = "4242424242424242")] + pub dpan: CardNumber, + + /// The card's expiry month + #[schema(value_type = String)] + pub expiry_month: Secret, + + /// The card's expiry year + #[schema(value_type = String)] + pub expiry_year: Secret, + + /// The card holder's name + #[schema(value_type = String, example = "John Doe")] + pub card_holder_name: Option>, +} + #[derive(Debug, ToSchema, Clone, Serialize, router_derive::PolymorphicSchema)] #[serde(deny_unknown_fields)] pub struct PayoutCreateResponse { @@ -1000,6 +1020,18 @@ impl From for payout_method_utils::WalletAdditionalData { telephone_number: telephone_number.map(From::from), })) } + Wallet::ApplePayDecrypt(ApplePayDecrypt { + expiry_month, + expiry_year, + card_holder_name, + .. + }) => Self::ApplePayDecrypt(Box::new( + payout_method_utils::ApplePayDecryptAdditionalData { + card_exp_month: expiry_month, + card_exp_year: expiry_year, + card_holder_name, + }, + )), } } } diff --git a/crates/common_enums/src/connector_enums.rs b/crates/common_enums/src/connector_enums.rs index 6604aa7a06..f6e7348e1e 100644 --- a/crates/common_enums/src/connector_enums.rs +++ b/crates/common_enums/src/connector_enums.rs @@ -379,6 +379,7 @@ impl Connector { | (Self::Adyenplatform, _) | (Self::Nomupay, _) | (Self::Loonio, _) + | (Self::Worldpay, Some(PayoutType::Wallet)) ) } #[cfg(feature = "payouts")] diff --git a/crates/common_utils/src/payout_method_utils.rs b/crates/common_utils/src/payout_method_utils.rs index 94f23c179d..1becc711f4 100644 --- a/crates/common_utils/src/payout_method_utils.rs +++ b/crates/common_utils/src/payout_method_utils.rs @@ -209,6 +209,8 @@ pub enum WalletAdditionalData { Paypal(Box), /// Additional data for venmo wallet payout method Venmo(Box), + /// Additional data for Apple pay decrypt wallet payout method + ApplePayDecrypt(Box), } /// Masked payout method details for paypal wallet payout method @@ -241,6 +243,25 @@ pub struct VenmoAdditionalData { pub telephone_number: Option, } +/// Masked payout method details for Apple pay decrypt wallet payout method +#[derive( + Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, +)] +#[diesel(sql_type = Jsonb)] +pub struct ApplePayDecryptAdditionalData { + /// Card expiry month + #[schema(value_type = String, example = "01")] + pub card_exp_month: Secret, + + /// Card expiry year + #[schema(value_type = String, example = "2026")] + pub card_exp_year: Secret, + + /// Card holder name + #[schema(value_type = String, example = "John Doe")] + pub card_holder_name: Option>, +} + /// Masked payout method details for wallet payout method #[derive( Eq, PartialEq, Clone, Debug, Deserialize, Serialize, FromSqlRow, AsExpression, ToSchema, diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 9ddf6a1846..e105fab53b 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -362,6 +362,8 @@ pub struct ConnectorConfig { pub wise_payout: Option, pub worldline: Option, pub worldpay: Option, + #[cfg(feature = "payouts")] + pub worldpay_payout: Option, pub worldpayvantiv: Option, pub worldpayxml: Option, pub xendit: Option, @@ -412,6 +414,7 @@ impl ConnectorConfig { PayoutConnectors::Paypal => Ok(connector_data.paypal_payout), PayoutConnectors::Stripe => Ok(connector_data.stripe_payout), PayoutConnectors::Wise => Ok(connector_data.wise_payout), + PayoutConnectors::Worldpay => Ok(connector_data.worldpay_payout), } } diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 6c87d88f13..e6ecce7b30 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -5210,6 +5210,21 @@ required=true type="MultiSelect" options=["PAN_ONLY", "CRYPTOGRAM_3DS"] +[[worldpay_payout.wallet]] + payment_method_type = "apple_pay" +[worldpay_payout.connector_auth.SignatureKey] +key1="Username" +api_key="Password" +api_secret="Merchant Identifier" +[worldpay_payout.connector_webhook_details] +merchant_secret="Source verification key" +[worldpay_payout.metadata.merchant_name] +name="merchant_name" +label="Name of the merchant to de displayed during 3DS challenge" +placeholder="Enter Name of the merchant" +required=true +type="Text" + [zen] [[zen.credit]] diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 5962c55929..787801f797 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -3977,6 +3977,20 @@ required = true type = "MultiSelect" options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] +[[worldpay_payout.wallet]] + payment_method_type = "apple_pay" +[worldpay_payout.connector_auth.SignatureKey] +key1="Username" +api_key="Password" +api_secret="Merchant Identifier" +[worldpay_payout.connector_webhook_details] +merchant_secret="Source verification key" +[worldpay_payout.metadata.merchant_name] +name="merchant_name" +label="Name of the merchant to de displayed during 3DS challenge" +placeholder="Enter Name of the merchant" +required=true +type="Text" [payme] [[payme.credit]] diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 7b79421cc0..1ae6e5be26 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -5169,6 +5169,20 @@ required = true type = "MultiSelect" options = ["PAN_ONLY", "CRYPTOGRAM_3DS"] +[[worldpay_payout.wallet]] + payment_method_type = "apple_pay" +[worldpay_payout.connector_auth.SignatureKey] +key1="Username" +api_key="Password" +api_secret="Merchant Identifier" +[worldpay_payout.connector_webhook_details] +merchant_secret="Source verification key" +[worldpay_payout.metadata.merchant_name] +name="merchant_name" +label="Name of the merchant to de displayed during 3DS challenge" +placeholder="Enter Name of the merchant" +required=true +type="Text" [zen] [[zen.credit]] diff --git a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs index e455882151..ebe056e39a 100644 --- a/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/adyen/transformers.rs @@ -5711,6 +5711,12 @@ impl TryFrom<&AdyenRouterData<&PayoutsRouterData>> for AdyenPayoutCreateRe message: "Venmo Wallet is not supported".to_string(), connector: "Adyen", })?, + payouts::Wallet::ApplePayDecrypt(_) => { + Err(errors::ConnectorError::NotSupported { + message: "Apple Pay Decrypt Wallet is not supported".to_string(), + connector: "Adyen", + })? + } }; let address: &hyperswitch_domain_models::address::AddressDetails = item.router_data.get_billing_address()?; diff --git a/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs b/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs index 23d06999d6..2f27e8b174 100644 --- a/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/paypal/transformers.rs @@ -2544,6 +2544,10 @@ impl TryFrom<&PaypalRouterData<&PayoutsRouterData>> for PaypalPayoutI receiver, } } + WalletPayout::ApplePayDecrypt(_) => Err(errors::ConnectorError::NotSupported { + message: "ApplePayDecrypt PayoutMethodType is not supported".to_string(), + connector: "Paypal", + })?, }, _ => Err(errors::ConnectorError::NotSupported { message: "PayoutMethodType is not supported".to_string(), diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay.rs b/crates/hyperswitch_connectors/src/connectors/worldpay.rs index 9883549ad9..066b8984a1 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay.rs @@ -1,3 +1,9 @@ +#[cfg(feature = "payouts")] +mod payout_requests; +#[cfg(feature = "payouts")] +mod payout_response; +#[cfg(feature = "payouts")] +pub mod payout_transformers; mod requests; mod response; pub mod transformers; @@ -38,6 +44,11 @@ use hyperswitch_domain_models::{ RefundSyncRouterData, RefundsRouterData, SetupMandateRouterData, }, }; +#[cfg(feature = "payouts")] +use hyperswitch_domain_models::{ + router_flow_types::payouts::PoFulfill, router_request_types::PayoutsData, + router_response_types::PayoutsResponseData, types::PayoutsRouterData, +}; use hyperswitch_interfaces::{ api::{ self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorRedirectResponse, @@ -50,6 +61,10 @@ use hyperswitch_interfaces::{ webhooks::{IncomingWebhook, IncomingWebhookRequestDetails}, }; use masking::Mask; +#[cfg(feature = "payouts")] +use payout_requests::WorldpayPayoutRequest; +#[cfg(feature = "payouts")] +use payout_response::WorldpayPayoutResponse; use requests::{ WorldpayCompleteAuthorizationRequest, WorldpayPartialRequest, WorldpayPaymentsRequest, }; @@ -60,6 +75,8 @@ use response::{ }; use ring::hmac; +#[cfg(feature = "payouts")] +use self::payout_transformers as worldpay_payout; use self::transformers as worldpay; use crate::{ constants::headers, @@ -70,6 +87,9 @@ use crate::{ }, }; +#[cfg(feature = "payouts")] +const WORLDPAY_PAYOUT_CONTENT_TYPE: &str = "application/vnd.worldpay.payouts-v4+json"; + #[derive(Clone)] pub struct Worldpay { amount_converter: &'static (dyn AmountConvertor + Sync), @@ -1087,6 +1107,124 @@ impl ConnectorIntegration for Worldpay } } +impl api::Payouts for Worldpay {} +#[cfg(feature = "payouts")] +impl api::PayoutFulfill for Worldpay {} + +#[async_trait::async_trait] +#[cfg(feature = "payouts")] +impl ConnectorIntegration for Worldpay { + fn get_url( + &self, + _req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult { + Ok(format!( + "{}payouts/basicDisbursement", + ConnectorCommon::base_url(self, connectors) + )) + } + + fn get_headers( + &self, + req: &PayoutsRouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = worldpay_payout::WorldpayPayoutAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let headers = vec![ + ( + headers::AUTHORIZATION.to_string(), + auth.api_key.into_masked(), + ), + ( + headers::ACCEPT.to_string(), + WORLDPAY_PAYOUT_CONTENT_TYPE.to_string().into(), + ), + ( + headers::CONTENT_TYPE.to_string(), + WORLDPAY_PAYOUT_CONTENT_TYPE.to_string().into(), + ), + (headers::WP_API_VERSION.to_string(), "2024-06-01".into()), + ]; + + Ok(headers) + } + + fn get_request_body( + &self, + req: &PayoutsRouterData, + _connectors: &Connectors, + ) -> CustomResult { + let connector_router_data = worldpay_payout::WorldpayPayoutRouterData::try_from(( + &self.get_currency_unit(), + req.request.destination_currency, + req.request.minor_amount, + req, + ))?; + let auth = worldpay_payout::WorldpayPayoutAuthType::try_from(&req.connector_auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let connector_req = + WorldpayPayoutRequest::try_from((&connector_router_data, &auth.entity_id))?; + Ok(RequestContent::Json(Box::new(connector_req))) + } + + fn build_request( + &self, + req: &PayoutsRouterData, + connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + let request = RequestBuilder::new() + .method(Method::Post) + .url(&types::PayoutFulfillType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(types::PayoutFulfillType::get_headers( + self, req, connectors, + )?) + .set_body(types::PayoutFulfillType::get_request_body( + self, req, connectors, + )?) + .build(); + Ok(Some(request)) + } + + fn handle_response( + &self, + data: &PayoutsRouterData, + event_builder: Option<&mut ConnectorEvent>, + res: Response, + ) -> CustomResult, errors::ConnectorError> { + let response: WorldpayPayoutResponse = res + .response + .parse_struct("WorldpayPayoutResponse") + .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, + }) + .change_context(errors::ConnectorError::ResponseHandlingFailed) + } + + fn get_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } + + fn get_5xx_error_response( + &self, + res: Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + self.build_error_response(res, event_builder) + } +} + #[async_trait::async_trait] impl IncomingWebhook for Worldpay { fn get_webhook_source_verification_algorithm( diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/payout_requests.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_requests.rs new file mode 100644 index 0000000000..c9247ffd6c --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_requests.rs @@ -0,0 +1,66 @@ +use masking::Secret; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayPayoutRequest { + pub transaction_reference: String, + pub merchant: Merchant, + pub instruction: PayoutInstruction, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PayoutInstruction { + pub payout_instrument: PayoutInstrument, + pub narrative: InstructionNarrative, + pub value: PayoutValue, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PayoutValue { + pub amount: i64, + pub currency: api_models::enums::Currency, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Merchant { + pub entity: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct InstructionNarrative { + pub line1: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum PayoutInstrument { + ApplePayDecrypt(ApplePayDecrypt), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ApplePayDecrypt { + #[serde(rename = "type")] + pub payout_type: PayoutType, + pub dpan: cards::CardNumber, + pub card_expiry_date: PayoutExpiryDate, + #[serde(skip_serializing_if = "Option::is_none")] + pub card_holder_name: Option>, +} + +#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize)] +pub struct PayoutExpiryDate { + pub month: Secret, + pub year: Secret, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PayoutType { + #[serde(rename = "card/networkToken+applepay")] + ApplePayDecrypt, +} diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/payout_response.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_response.rs new file mode 100644 index 0000000000..2c5d948471 --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_response.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct WorldpayPayoutResponse { + pub outcome: PayoutOutcome, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum PayoutOutcome { + RequestReceived, + Refused, + Error, + QueryRequired, +} diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/payout_transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_transformers.rs new file mode 100644 index 0000000000..873dc8100c --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/payout_transformers.rs @@ -0,0 +1,193 @@ +use base64::Engine; +use common_enums::enums; +use common_utils::{consts::BASE64_ENGINE, pii, types::MinorUnit}; +use error_stack::ResultExt; +use hyperswitch_domain_models::{ + router_data::ConnectorAuthType, router_flow_types::payouts::PoFulfill, + router_response_types::PayoutsResponseData, types, +}; +use hyperswitch_interfaces::{api, errors}; +use masking::{PeekInterface, Secret}; +use serde::{Deserialize, Serialize}; + +use super::{payout_requests::*, payout_response::*}; +use crate::{ + types::PayoutsResponseRouterData, + utils::{self, CardData, RouterData as RouterDataTrait}, +}; + +#[derive(Debug, Serialize)] +pub struct WorldpayPayoutRouterData { + amount: i64, + router_data: T, +} +impl TryFrom<(&api::CurrencyUnit, enums::Currency, MinorUnit, T)> + for WorldpayPayoutRouterData +{ + type Error = error_stack::Report; + fn try_from( + (_currency_unit, _currency, minor_amount, item): ( + &api::CurrencyUnit, + enums::Currency, + MinorUnit, + T, + ), + ) -> Result { + Ok(Self { + amount: minor_amount.get_amount_as_i64(), + router_data: item, + }) + } +} + +pub struct WorldpayPayoutAuthType { + pub(super) api_key: Secret, + pub(super) entity_id: Secret, +} + +impl TryFrom<&ConnectorAuthType> for WorldpayPayoutAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &ConnectorAuthType) -> Result { + match auth_type { + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => { + let auth_key = format!("{}:{}", key1.peek(), api_key.peek()); + let auth_header = format!("Basic {}", BASE64_ENGINE.encode(auth_key)); + Ok(Self { + api_key: Secret::new(auth_header), + entity_id: api_secret.clone(), + }) + } + _ => Err(errors::ConnectorError::FailedToObtainAuthType)?, + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct WorldpayPayoutConnectorMetadataObject { + pub merchant_name: Option>, +} + +impl TryFrom> for WorldpayPayoutConnectorMetadataObject { + type Error = error_stack::Report; + fn try_from(meta_data: Option<&pii::SecretSerdeValue>) -> Result { + let metadata: Self = utils::to_connector_meta_from_secret::(meta_data.cloned()) + .change_context(errors::ConnectorError::InvalidConnectorConfig { + config: "metadata", + })?; + Ok(metadata) + } +} + +impl + TryFrom<( + &WorldpayPayoutRouterData<&types::PayoutsRouterData>, + &Secret, + )> for WorldpayPayoutRequest +{ + type Error = error_stack::Report; + + fn try_from( + req: ( + &WorldpayPayoutRouterData<&types::PayoutsRouterData>, + &Secret, + ), + ) -> Result { + let (item, entity_id) = req; + + let worldpay_connector_metadata_object: WorldpayPayoutConnectorMetadataObject = + WorldpayPayoutConnectorMetadataObject::try_from( + item.router_data.connector_meta_data.as_ref(), + )?; + + let merchant_name = worldpay_connector_metadata_object.merchant_name.ok_or( + errors::ConnectorError::InvalidConnectorConfig { + config: "metadata.merchant_name", + }, + )?; + + Ok(Self { + transaction_reference: item.router_data.connector_request_reference_id.clone(), + merchant: Merchant { + entity: entity_id.clone(), + }, + instruction: PayoutInstruction { + value: PayoutValue { + amount: item.amount, + currency: item.router_data.request.destination_currency, + }, + narrative: InstructionNarrative { + line1: merchant_name, + }, + payout_instrument: PayoutInstrument::try_from( + item.router_data.get_payout_method_data()?, + )?, + }, + }) + } +} + +impl TryFrom for PayoutInstrument { + type Error = error_stack::Report; + + fn try_from( + payout_method_data: api_models::payouts::PayoutMethodData, + ) -> Result { + match payout_method_data { + api_models::payouts::PayoutMethodData::Wallet( + api_models::payouts::Wallet::ApplePayDecrypt(apple_pay_decrypted_data), + ) => Ok(Self::ApplePayDecrypt(ApplePayDecrypt { + payout_type: PayoutType::ApplePayDecrypt, + dpan: apple_pay_decrypted_data.dpan.clone(), + card_holder_name: apple_pay_decrypted_data.card_holder_name.clone(), + card_expiry_date: PayoutExpiryDate { + month: apple_pay_decrypted_data.get_expiry_month_as_i8()?, + year: apple_pay_decrypted_data.get_expiry_year_as_4_digit_i32()?, + }, + })), + api_models::payouts::PayoutMethodData::Card(_) + | api_models::payouts::PayoutMethodData::Bank(_) + | api_models::payouts::PayoutMethodData::Wallet(_) + | api_models::payouts::PayoutMethodData::BankRedirect(_) => { + Err(errors::ConnectorError::NotImplemented( + "Selected Payout Method is not implemented for Worldpay".to_string(), + ) + .into()) + } + } + } +} + +impl From for enums::PayoutStatus { + fn from(item: PayoutOutcome) -> Self { + match item { + PayoutOutcome::RequestReceived => Self::Initiated, + PayoutOutcome::Error | PayoutOutcome::Refused => Self::Failed, + PayoutOutcome::QueryRequired => Self::Pending, + } + } +} + +impl TryFrom> + for types::PayoutsRouterData +{ + type Error = error_stack::Report; + fn try_from( + item: PayoutsResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(PayoutsResponseData { + status: Some(enums::PayoutStatus::from(item.response.outcome.clone())), + connector_payout_id: None, + payout_eligible: None, + should_add_next_step_to_process_tracker: false, + error_code: None, + error_message: None, + }), + ..item.data + }) + } +} diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index f143073a03..6aee074756 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -3914,7 +3914,6 @@ default_imp_for_payouts!( connectors::UnifiedAuthenticationService, connectors::Volt, connectors::Worldline, - connectors::Worldpay, connectors::Worldpayvantiv, connectors::Worldpayxml, connectors::Wellsfargo, @@ -4494,7 +4493,6 @@ default_imp_for_payouts_fulfill!( connectors::Tsys, connectors::UnifiedAuthenticationService, connectors::Worldline, - connectors::Worldpay, connectors::Worldpayvantiv, connectors::Worldpayxml, connectors::Wellsfargo, diff --git a/crates/hyperswitch_connectors/src/utils.rs b/crates/hyperswitch_connectors/src/utils.rs index b256ca82ec..68256b5353 100644 --- a/crates/hyperswitch_connectors/src/utils.rs +++ b/crates/hyperswitch_connectors/src/utils.rs @@ -1346,6 +1346,115 @@ impl CardData for CardDetailsForNetworkTransactionId { } } +#[cfg(feature = "payouts")] +impl CardData for api_models::payouts::ApplePayDecrypt { + fn get_card_expiry_year_2_digit(&self) -> Result, errors::ConnectorError> { + let binding = self.expiry_month.clone(); + let year = binding.peek(); + Ok(Secret::new( + year.get(year.len() - 2..) + .ok_or(errors::ConnectorError::RequestEncodingFailed)? + .to_string(), + )) + } + fn get_card_expiry_month_2_digit(&self) -> Result, errors::ConnectorError> { + let exp_month = self + .expiry_month + .peek() + .to_string() + .parse::() + .map_err(|_| errors::ConnectorError::InvalidDataFormat { + field_name: "payout_method_data.apple_pay_decrypt.expiry_month", + })?; + let month = ::cards::CardExpirationMonth::try_from(exp_month).map_err(|_| { + errors::ConnectorError::InvalidDataFormat { + field_name: "payout_method_data.apple_pay_decrypt.expiry_month", + } + })?; + Ok(Secret::new(month.two_digits())) + } + fn get_card_issuer(&self) -> Result { + Err(errors::ConnectorError::ParsingFailed) + .attach_printable("get_card_issuer is not supported for Applepay Decrypted Payout") + } + fn get_card_expiry_month_year_2_digit_with_delimiter( + &self, + delimiter: String, + ) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?; + Ok(Secret::new(format!( + "{}{}{}", + self.expiry_month.peek(), + delimiter, + year.peek() + ))) + } + fn get_expiry_date_as_yyyymm(&self, delimiter: &str) -> Secret { + let year = self.get_expiry_year_4_digit(); + Secret::new(format!( + "{}{}{}", + year.peek(), + delimiter, + self.expiry_month.peek() + )) + } + fn get_expiry_date_as_mmyyyy(&self, delimiter: &str) -> Secret { + let year = self.get_expiry_year_4_digit(); + Secret::new(format!( + "{}{}{}", + self.expiry_month.peek(), + delimiter, + year.peek() + )) + } + fn get_expiry_year_4_digit(&self) -> Secret { + let mut year = self.expiry_year.peek().clone(); + if year.len() == 2 { + year = format!("20{year}"); + } + Secret::new(year) + } + fn get_expiry_date_as_yymm(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); + let month = self.expiry_month.clone().expose(); + Ok(Secret::new(format!("{year}{month}"))) + } + fn get_expiry_date_as_mmyy(&self) -> Result, errors::ConnectorError> { + let year = self.get_card_expiry_year_2_digit()?.expose(); + let month = self.expiry_month.clone().expose(); + Ok(Secret::new(format!("{month}{year}"))) + } + fn get_expiry_month_as_i8(&self) -> Result, Error> { + self.expiry_month + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_expiry_year_as_i32(&self) -> Result, Error> { + self.expiry_year + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_expiry_year_as_4_digit_i32(&self) -> Result, Error> { + self.get_expiry_year_4_digit() + .peek() + .clone() + .parse::() + .change_context(errors::ConnectorError::ResponseDeserializationFailed) + .map(Secret::new) + } + fn get_cardholder_name(&self) -> Result, Error> { + self.card_holder_name + .clone() + .ok_or_else(missing_field_err("card.card_holder_name")) + } +} + #[track_caller] fn get_card_issuer(card_number: &str) -> Result { for (k, v) in CARD_REGEX.iter() { diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 430fb7f0e3..7220e6c8a9 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -240,6 +240,7 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::payout_method_utils::PaypalAdditionalData, common_utils::payout_method_utils::InteracAdditionalData, common_utils::payout_method_utils::VenmoAdditionalData, + common_utils::payout_method_utils::ApplePayDecryptAdditionalData, common_types::payments::SplitPaymentsRequest, common_types::payments::GpayTokenizationData, common_types::payments::GPayPredecryptData, @@ -690,6 +691,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::PayoutMethodDataResponse, api_models::payouts::PayoutLinkResponse, api_models::payouts::Bank, + api_models::payouts::ApplePayDecrypt, api_models::payouts::PayoutCreatePayoutLinkConfig, api_models::enums::PayoutEntityType, api_models::enums::PayoutSendPriority, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index b14a729c0b..6aa19875c0 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -183,6 +183,7 @@ Never share your secret api keys. Keep them guarded and secure. common_utils::payout_method_utils::PaypalAdditionalData, common_utils::payout_method_utils::InteracAdditionalData, common_utils::payout_method_utils::VenmoAdditionalData, + common_utils::payout_method_utils::ApplePayDecryptAdditionalData, common_types::payments::SplitPaymentsRequest, common_types::payments::GpayTokenizationData, common_types::payments::GPayPredecryptData, @@ -661,6 +662,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payouts::PayoutMethodDataResponse, api_models::payouts::PayoutLinkResponse, api_models::payouts::Bank, + api_models::payouts::ApplePayDecrypt, api_models::payouts::PayoutCreatePayoutLinkConfig, api_models::enums::PayoutEntityType, api_models::enums::PayoutSendPriority, diff --git a/crates/router/src/core/payment_methods/vault.rs b/crates/router/src/core/payment_methods/vault.rs index 1b99c1ab33..81a3cfe849 100644 --- a/crates/router/src/core/payment_methods/vault.rs +++ b/crates/router/src/core/payment_methods/vault.rs @@ -551,6 +551,10 @@ pub struct TokenizedWalletSensitiveValues { pub telephone_number: Option>, pub wallet_id: Option>, pub wallet_type: PaymentMethodType, + pub dpan: Option, + pub expiry_month: Option>, + pub expiry_year: Option>, + pub card_holder_name: Option>, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -570,12 +574,30 @@ impl Vaultable for api::WalletPayout { telephone_number: paypal_data.telephone_number.clone(), wallet_id: paypal_data.paypal_id.clone(), wallet_type: PaymentMethodType::Paypal, + dpan: None, + expiry_month: None, + expiry_year: None, + card_holder_name: None, }, Self::Venmo(venmo_data) => TokenizedWalletSensitiveValues { email: None, telephone_number: venmo_data.telephone_number.clone(), wallet_id: None, wallet_type: PaymentMethodType::Venmo, + dpan: None, + expiry_month: None, + expiry_year: None, + card_holder_name: None, + }, + Self::ApplePayDecrypt(apple_pay_decrypt_data) => TokenizedWalletSensitiveValues { + email: None, + telephone_number: None, + wallet_id: None, + wallet_type: PaymentMethodType::ApplePay, + dpan: Some(apple_pay_decrypt_data.dpan.clone()), + expiry_month: Some(apple_pay_decrypt_data.expiry_month.clone()), + expiry_year: Some(apple_pay_decrypt_data.expiry_year.clone()), + card_holder_name: apple_pay_decrypt_data.card_holder_name.clone(), }, }; @@ -620,6 +642,19 @@ impl Vaultable for api::WalletPayout { PaymentMethodType::Venmo => Self::Venmo(api_models::payouts::Venmo { telephone_number: value1.telephone_number, }), + PaymentMethodType::ApplePay => { + match (value1.dpan, value1.expiry_month, value1.expiry_year) { + (Some(dpan), Some(expiry_month), Some(expiry_year)) => { + Self::ApplePayDecrypt(api_models::payouts::ApplePayDecrypt { + dpan, + expiry_month, + expiry_year, + card_holder_name: value1.card_holder_name, + }) + } + _ => Err(errors::VaultError::ResponseDeserializationFailed)?, + } + } _ => Err(errors::VaultError::PayoutMethodNotSupported)?, }; let supp_data = SupplementaryVaultData { diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 910ec21335..90e25b34cf 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1220,6 +1220,7 @@ impl ForeignFrom<&api_models::payouts::Wallet> for api_enums::PaymentMethodType match value { api_models::payouts::Wallet::Paypal(_) => Self::Paypal, api_models::payouts::Wallet::Venmo(_) => Self::Venmo, + api_models::payouts::Wallet::ApplePayDecrypt(_) => Self::ApplePay, } } } From 587588f8709e3e3952a3b0c3eee91aa897daf792 Mon Sep 17 00:00:00 2001 From: Venu Madhav Bandarupalli <110053886+VenuMadhav2541@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:36:12 +0530 Subject: [PATCH 14/16] feat(customers): add time range filtering and count functionality to customer list endpoints (#9767) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/customers.rs | 22 +++++ crates/api_models/src/events/customer.rs | 33 ++++++- crates/diesel_models/src/query/customers.rs | 99 ++++++++++++++++++- crates/diesel_models/src/query/generics.rs | 30 +++++- .../hyperswitch_domain_models/src/customer.rs | 10 ++ .../router/src/compatibility/stripe/errors.rs | 3 + crates/router/src/core/customers.rs | 60 +++++++++++ .../core/errors/customers_error_response.rs | 3 + crates/router/src/core/errors/transformers.rs | 6 ++ crates/router/src/core/locker_migration.rs | 1 + crates/router/src/core/utils.rs | 1 + .../src/core/utils/customer_validation.rs | 21 ++++ crates/router/src/db/kafka_store.rs | 12 +++ crates/router/src/routes/app.rs | 8 ++ crates/router/src/routes/customers.rs | 77 +++++++++++++++ crates/router/src/routes/lock_utils.rs | 3 +- crates/router/src/types/api/customers.rs | 4 +- crates/router_env/src/logger/types.rs | 2 + crates/storage_impl/src/customers.rs | 98 +++++++++++++++++- 19 files changed, 482 insertions(+), 11 deletions(-) create mode 100644 crates/router/src/core/utils/customer_validation.rs diff --git a/crates/api_models/src/customers.rs b/crates/api_models/src/customers.rs index 62fd000ddb..536d630a81 100644 --- a/crates/api_models/src/customers.rs +++ b/crates/api_models/src/customers.rs @@ -46,6 +46,17 @@ pub struct CustomerRequest { #[derive(Debug, Default, Clone, Deserialize, Serialize, ToSchema)] pub struct CustomerListRequest { + /// Offset + #[schema(example = 32)] + pub offset: Option, + /// Limit + #[schema(example = 32)] + pub limit: Option, + pub customer_id: Option, +} + +#[derive(Debug, Default, Clone, Deserialize, Serialize, ToSchema)] +pub struct CustomerListRequestWithConstraints { /// Offset #[schema(example = 32)] pub offset: Option, @@ -54,6 +65,9 @@ pub struct CustomerListRequest { pub limit: Option, /// Unique identifier for a customer pub customer_id: Option, + /// Filter with created time range + #[serde(flatten)] + pub time_range: Option, } #[cfg(feature = "v1")] @@ -388,3 +402,11 @@ pub struct CustomerUpdateRequestInternal { pub id: id_type::GlobalCustomerId, pub request: CustomerUpdateRequest, } + +#[derive(Debug, Serialize, ToSchema)] +pub struct CustomerListResponse { + /// List of customers + pub data: Vec, + /// Total count of customers + pub total_count: usize, +} diff --git a/crates/api_models/src/events/customer.rs b/crates/api_models/src/events/customer.rs index 290ccfdd46..602bdf8a12 100644 --- a/crates/api_models/src/events/customer.rs +++ b/crates/api_models/src/events/customer.rs @@ -1,7 +1,8 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::customers::{ - CustomerDeleteResponse, CustomerRequest, CustomerResponse, CustomerUpdateRequestInternal, + CustomerDeleteResponse, CustomerListRequestWithConstraints, CustomerListResponse, + CustomerRequest, CustomerResponse, CustomerUpdateRequestInternal, }; #[cfg(feature = "v1")] @@ -73,3 +74,33 @@ impl ApiEventMetric for CustomerUpdateRequestInternal { }) } } + +#[cfg(feature = "v1")] +impl ApiEventMetric for CustomerListRequestWithConstraints { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { + customer_id: self.customer_id.clone()?, + }) + } +} + +#[cfg(feature = "v2")] +impl ApiEventMetric for CustomerListRequestWithConstraints { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Customer { customer_id: None }) + } +} + +#[cfg(feature = "v1")] +impl ApiEventMetric for CustomerListResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} + +#[cfg(feature = "v2")] +impl ApiEventMetric for CustomerListResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::ResourceListAPI) + } +} diff --git a/crates/diesel_models/src/query/customers.rs b/crates/diesel_models/src/query/customers.rs index a6121d408c..989bcdc9d9 100644 --- a/crates/diesel_models/src/query/customers.rs +++ b/crates/diesel_models/src/query/customers.rs @@ -21,6 +21,7 @@ pub struct CustomerListConstraints { pub limit: i64, pub offset: Option, pub customer_id: Option, + pub time_range: Option, } impl Customer { @@ -56,7 +57,67 @@ impl Customer { } #[cfg(feature = "v1")] - pub async fn list_by_merchant_id( + pub async fn get_customer_count_by_merchant_id_and_constraints( + conn: &PgPooledConn, + merchant_id: &id_type::MerchantId, + customer_list_constraints: CustomerListConstraints, + ) -> StorageResult { + if let Some(customer_id) = customer_list_constraints.customer_id { + let predicate = dsl::merchant_id + .eq(merchant_id.clone()) + .and(dsl::customer_id.eq(customer_id)); + generics::generic_count::<::Table, _>(conn, predicate).await + } else if let Some(time_range) = customer_list_constraints.time_range { + let start_time = time_range.start_time; + let end_time = time_range + .end_time + .unwrap_or_else(common_utils::date_time::now); + let predicate = dsl::merchant_id + .eq(merchant_id.clone()) + .and(dsl::created_at.between(start_time, end_time)); + + generics::generic_count::<::Table, _>(conn, predicate).await + } else { + generics::generic_count::<::Table, _>( + conn, + dsl::merchant_id.eq(merchant_id.to_owned()), + ) + .await + } + } + + #[cfg(feature = "v2")] + pub async fn get_customer_count_by_merchant_id_and_constraints( + conn: &PgPooledConn, + merchant_id: &id_type::MerchantId, + customer_list_constraints: CustomerListConstraints, + ) -> StorageResult { + if let Some(customer_id) = customer_list_constraints.customer_id { + let predicate = dsl::merchant_id + .eq(merchant_id.clone()) + .and(dsl::merchant_reference_id.eq(customer_id)); + generics::generic_count::<::Table, _>(conn, predicate).await + } else if let Some(time_range) = customer_list_constraints.time_range { + let start_time = time_range.start_time; + let end_time = time_range + .end_time + .unwrap_or_else(common_utils::date_time::now); + let predicate = dsl::merchant_id + .eq(merchant_id.clone()) + .and(dsl::created_at.between(start_time, end_time)); + + generics::generic_count::<::Table, _>(conn, predicate).await + } else { + generics::generic_count::<::Table, _>( + conn, + dsl::merchant_id.eq(merchant_id.to_owned()), + ) + .await + } + } + + #[cfg(feature = "v1")] + pub async fn list_customers_by_merchant_id_and_constraints( conn: &PgPooledConn, merchant_id: &id_type::MerchantId, constraints: CustomerListConstraints, @@ -73,6 +134,23 @@ impl Customer { Some(dsl::created_at), ) .await + } else if let Some(time_range) = constraints.time_range { + let start_time = time_range.start_time; + let end_time = time_range + .end_time + .unwrap_or_else(common_utils::date_time::now); + let predicate = dsl::merchant_id + .eq(merchant_id.clone()) + .and(dsl::created_at.between(start_time, end_time)); + + generics::generic_filter::<::Table, _, _, Self>( + conn, + predicate, + Some(constraints.limit), + constraints.offset, + Some(dsl::created_at), + ) + .await } else { let predicate = dsl::merchant_id.eq(merchant_id.clone()); generics::generic_filter::<::Table, _, _, Self>( @@ -87,7 +165,7 @@ impl Customer { } #[cfg(feature = "v2")] - pub async fn list_by_merchant_id( + pub async fn list_customers_by_merchant_id_and_constraints( conn: &PgPooledConn, merchant_id: &id_type::MerchantId, constraints: CustomerListConstraints, @@ -104,6 +182,23 @@ impl Customer { Some(dsl::created_at), ) .await + } else if let Some(time_range) = constraints.time_range { + let start_time = time_range.start_time; + let end_time = time_range + .end_time + .unwrap_or_else(common_utils::date_time::now); + let predicate = dsl::merchant_id + .eq(merchant_id.clone()) + .and(dsl::created_at.between(start_time, end_time)); + + generics::generic_filter::<::Table, _, _, Self>( + conn, + predicate, + Some(constraints.limit), + constraints.offset, + Some(dsl::created_at), + ) + .await } else { let predicate = dsl::merchant_id.eq(merchant_id.clone()); generics::generic_filter::<::Table, _, _, Self>( diff --git a/crates/diesel_models/src/query/generics.rs b/crates/diesel_models/src/query/generics.rs index d5581932ea..d809f3cb19 100644 --- a/crates/diesel_models/src/query/generics.rs +++ b/crates/diesel_models/src/query/generics.rs @@ -4,7 +4,7 @@ use async_bb8_diesel::AsyncRunQueryDsl; use diesel::{ associations::HasTable, debug_query, - dsl::{Find, IsNotNull, Limit}, + dsl::{count_star, Find, IsNotNull, Limit}, helper_types::{Filter, IntoBoxed}, insertable::CanInsertInSingleQuery, pg::{Pg, PgConnection}, @@ -13,7 +13,7 @@ use diesel::{ QueryId, UpdateStatement, }, query_dsl::{ - methods::{BoxedDsl, FilterDsl, FindDsl, LimitDsl, OffsetDsl, OrderDsl}, + methods::{BoxedDsl, FilterDsl, FindDsl, LimitDsl, OffsetDsl, OrderDsl, SelectDsl}, LoadQuery, RunQueryDsl, }, result::Error as DieselError, @@ -445,6 +445,32 @@ where .attach_printable("Error filtering records by predicate") } +pub async fn generic_count(conn: &PgPooledConn, predicate: P) -> StorageResult +where + T: FilterDsl

+ HasTable + Table + SelectDsl + 'static, + Filter: SelectDsl, + diesel::dsl::Select, count_star>: + LoadQuery<'static, PgConnection, i64> + QueryFragment + Send + 'static, +{ + let query = ::table() + .filter(predicate) + .select(count_star()); + + logger::debug!(query = %debug_query::(&query).to_string()); + + let count_i64: i64 = + track_database_call::(query.get_result_async(conn), DatabaseOperation::Count) + .await + .change_context(errors::DatabaseError::Others) + .attach_printable("Error counting records by predicate")?; + + let count_usize = usize::try_from(count_i64).map_err(|_| { + report!(errors::DatabaseError::Others).attach_printable("Count value does not fit in usize") + })?; + + Ok(count_usize) +} + fn to_optional(arg: StorageResult) -> StorageResult> { match arg { Ok(value) => Ok(Some(value)), diff --git a/crates/hyperswitch_domain_models/src/customer.rs b/crates/hyperswitch_domain_models/src/customer.rs index 84c9510c4b..d440a2d5da 100644 --- a/crates/hyperswitch_domain_models/src/customer.rs +++ b/crates/hyperswitch_domain_models/src/customer.rs @@ -558,6 +558,7 @@ pub struct CustomerListConstraints { pub limit: u16, pub offset: Option, pub customer_id: Option, + pub time_range: Option, } impl From for query::CustomerListConstraints { @@ -566,6 +567,7 @@ impl From for query::CustomerListConstraints { limit: i64::from(value.limit), offset: value.offset.map(i64::from), customer_id: value.customer_id, + time_range: value.time_range, } } } @@ -657,6 +659,14 @@ where constraints: CustomerListConstraints, ) -> CustomResult, Self::Error>; + async fn list_customers_by_merchant_id_with_count( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + key_store: &MerchantKeyStore, + constraints: CustomerListConstraints, + ) -> CustomResult<(Vec, usize), Self::Error>; + async fn insert_customer( &self, customer_data: Customer, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index a0bec1a5a1..c9713475db 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -861,6 +861,9 @@ impl ErrorSwitch for CustomersErrorResponse { match self { Self::CustomerRedacted => SC::CustomerRedacted, Self::InternalServerError => SC::InternalServerError, + Self::InvalidRequestData { message } => SC::InvalidRequestData { + message: message.clone(), + }, Self::MandateActive => SC::MandateActive, Self::CustomerNotFound => SC::CustomerNotFound, Self::CustomerAlreadyExists => SC::DuplicateCustomer, diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 09f12d62ba..42deeddf51 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -22,6 +22,10 @@ use crate::{ core::{ errors::{self, StorageErrorExt}, payment_methods::{cards, network_tokenization}, + utils::{ + self, + customer_validation::{CUSTOMER_LIST_LOWER_LIMIT, CUSTOMER_LIST_UPPER_LIMIT}, + }, }, db::StorageInterface, pii::PeekInterface, @@ -591,6 +595,7 @@ pub async fn list_customers( .unwrap_or(crate::consts::DEFAULT_LIST_API_LIMIT), offset: request.offset, customer_id: request.customer_id, + time_range: None, }; let domain_customers = db @@ -618,6 +623,61 @@ pub async fn list_customers( Ok(services::ApplicationResponse::Json(customers)) } +#[instrument(skip(state))] +pub async fn list_customers_with_count( + state: SessionState, + merchant_id: id_type::MerchantId, + _profile_id_list: Option>, + key_store: domain::MerchantKeyStore, + request: customers::CustomerListRequestWithConstraints, +) -> errors::CustomerResponse { + let db = state.store.as_ref(); + let limit = utils::customer_validation::validate_customer_list_limit(request.limit) + .change_context(errors::CustomersErrorResponse::InvalidRequestData { + message: format!( + "limit should be between {CUSTOMER_LIST_LOWER_LIMIT} and {CUSTOMER_LIST_UPPER_LIMIT}" + ), + })?; + + let customer_list_constraints = crate::db::customers::CustomerListConstraints { + limit: request.limit.unwrap_or(limit), + offset: request.offset, + customer_id: request.customer_id, + time_range: request.time_range, + }; + + let domain_customers = db + .list_customers_by_merchant_id_with_count( + &(&state).into(), + &merchant_id, + &key_store, + customer_list_constraints, + ) + .await + .switch()?; + + #[cfg(feature = "v1")] + let customers: Vec = domain_customers + .0 + .into_iter() + .map(|domain_customer| customers::CustomerResponse::foreign_from((domain_customer, None))) + .collect(); + + #[cfg(feature = "v2")] + let customers: Vec = domain_customers + .0 + .into_iter() + .map(customers::CustomerResponse::foreign_from) + .collect(); + + Ok(services::ApplicationResponse::Json( + customers::CustomerListResponse { + data: customers.into_iter().map(|c| c.0).collect(), + total_count: domain_customers.1, + }, + )) +} + #[cfg(feature = "v2")] #[instrument(skip_all)] pub async fn delete_customer( diff --git a/crates/router/src/core/errors/customers_error_response.rs b/crates/router/src/core/errors/customers_error_response.rs index 5bda9a69a8..cde84198b2 100644 --- a/crates/router/src/core/errors/customers_error_response.rs +++ b/crates/router/src/core/errors/customers_error_response.rs @@ -8,6 +8,9 @@ pub enum CustomersErrorResponse { #[error("Something went wrong")] InternalServerError, + #[error("Invalid request data: {message}")] + InvalidRequestData { message: String }, + #[error("Customer has already been redacted")] MandateActive, diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index f5e90771c1..fc4fb26fcb 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -16,6 +16,12 @@ impl ErrorSwitch for CustomersError Self::InternalServerError => { AER::InternalServerError(ApiError::new("HE", 0, "Something went wrong", None)) } + Self::InvalidRequestData { message } => AER::BadRequest(ApiError::new( + "IR", + 7, + format!("Invalid value provided:{}", message), + None, + )), Self::MandateActive => AER::BadRequest(ApiError::new( "IR", 10, diff --git a/crates/router/src/core/locker_migration.rs b/crates/router/src/core/locker_migration.rs index bea3ff4e58..d40585eb8a 100644 --- a/crates/router/src/core/locker_migration.rs +++ b/crates/router/src/core/locker_migration.rs @@ -55,6 +55,7 @@ pub async fn rust_locker_migration( limit: u16::MAX, offset: None, customer_id: None, + time_range: None, }; let domain_customers = db diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 753129f031..80b522b7a1 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,3 +1,4 @@ +pub mod customer_validation; pub mod refunds_transformers; pub mod refunds_validator; diff --git a/crates/router/src/core/utils/customer_validation.rs b/crates/router/src/core/utils/customer_validation.rs new file mode 100644 index 0000000000..14df0fb0e0 --- /dev/null +++ b/crates/router/src/core/utils/customer_validation.rs @@ -0,0 +1,21 @@ +use crate::core::errors::{self, CustomResult}; + +pub const CUSTOMER_LIST_LOWER_LIMIT: u16 = 1; +pub const CUSTOMER_LIST_UPPER_LIMIT: u16 = 100; +pub const CUSTOMER_LIST_DEFAULT_LIMIT: u16 = 10; + +pub fn validate_customer_list_limit( + limit: Option, +) -> CustomResult { + match limit { + Some(l) if (CUSTOMER_LIST_LOWER_LIMIT..=CUSTOMER_LIST_UPPER_LIMIT).contains(&l) => Ok(l), + Some(_) => Err(errors::ApiErrorResponse::InvalidRequestData { + message: format!( + "limit should be between {} and {}", + CUSTOMER_LIST_LOWER_LIMIT, CUSTOMER_LIST_UPPER_LIMIT + ), + } + .into()), + None => Ok(CUSTOMER_LIST_DEFAULT_LIMIT), + } +} diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 22c3d1022a..befb7329b7 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -510,6 +510,18 @@ impl CustomerInterface for KafkaStore { .await } + async fn list_customers_by_merchant_id_with_count( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + key_store: &domain::MerchantKeyStore, + constraints: super::customers::CustomerListConstraints, + ) -> CustomResult<(Vec, usize), errors::StorageError> { + self.diesel_store + .list_customers_by_merchant_id_with_count(state, merchant_id, key_store, constraints) + .await + } + #[cfg(feature = "v1")] async fn find_customer_by_customer_id_merchant_id( &self, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f575f80af1..989845638d 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1284,6 +1284,10 @@ impl Customers { { route = route .service(web::resource("/list").route(web::get().to(customers::customers_list))) + .service( + web::resource("/list_with_count") + .route(web::get().to(customers::customers_list_with_count)), + ) .service( web::resource("/total-payment-methods") .route(web::get().to(payment_methods::get_total_payment_method_count)), @@ -1321,6 +1325,10 @@ impl Customers { .route(web::get().to(customers::get_customer_mandates)), ) .service(web::resource("/list").route(web::get().to(customers::customers_list))) + .service( + web::resource("/list_with_count") + .route(web::get().to(customers::customers_list_with_count)), + ) } #[cfg(feature = "oltp")] diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index 9673318160..0c37afd10b 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -239,6 +239,83 @@ pub async fn customers_list( .await } +#[cfg(feature = "v2")] +#[instrument(skip_all, fields(flow = ?Flow::CustomersListWithConstraints))] +pub async fn customers_list_with_count( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { + let flow = Flow::CustomersListWithConstraints; + let payload = query.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, request, _| { + list_customers_with_count( + state, + auth.merchant_account.get_id().to_owned(), + None, + auth.key_store, + request, + ) + }, + auth::auth_type( + &auth::V2ApiKeyAuth { + is_connected_allowed: false, + is_platform_allowed: false, + }, + &auth::JWTAuth { + permission: Permission::MerchantCustomerRead, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +#[cfg(feature = "v1")] +#[instrument(skip_all, fields(flow = ?Flow::CustomersListWithConstraints))] +pub async fn customers_list_with_count( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { + let flow = Flow::CustomersListWithConstraints; + let payload = query.into_inner(); + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: auth::AuthenticationData, request, _| { + list_customers_with_count( + state, + auth.merchant_account.get_id().to_owned(), + None, + auth.key_store, + request, + ) + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth { + is_connected_allowed: false, + is_platform_allowed: false, + }), + &auth::JWTAuth { + permission: Permission::MerchantCustomerRead, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "v1")] #[instrument(skip_all, fields(flow = ?Flow::CustomersUpdate))] pub async fn customers_update( diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index a8b7294c1c..8a199c7867 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -114,7 +114,8 @@ impl From for ApiIdentifier { | Flow::CustomersUpdate | Flow::CustomersDelete | Flow::CustomersGetMandates - | Flow::CustomersList => Self::Customers, + | Flow::CustomersList + | Flow::CustomersListWithConstraints => Self::Customers, Flow::EphemeralKeyCreate | Flow::EphemeralKeyDelete => Self::Ephemeral, Flow::DeepHealthCheck | Flow::HealthCheck => Self::Health, Flow::MandatesRetrieve | Flow::MandatesRevoke | Flow::MandatesList => Self::Mandates, diff --git a/crates/router/src/types/api/customers.rs b/crates/router/src/types/api/customers.rs index 149239d2e3..6d7e2430b9 100644 --- a/crates/router/src/types/api/customers.rs +++ b/crates/router/src/types/api/customers.rs @@ -1,7 +1,7 @@ use api_models::customers; pub use api_models::customers::{ - CustomerDeleteResponse, CustomerListRequest, CustomerRequest, CustomerUpdateRequest, - CustomerUpdateRequestInternal, + CustomerDeleteResponse, CustomerListRequest, CustomerListRequestWithConstraints, + CustomerListResponse, CustomerRequest, CustomerUpdateRequest, CustomerUpdateRequestInternal, }; #[cfg(feature = "v2")] use hyperswitch_domain_models::customer; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 1968447a16..76f0e28a3a 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -130,6 +130,8 @@ pub enum Flow { GetPaymentMethodTokenData, /// List Customers for a merchant CustomersList, + ///List Customers for a merchant with constraints. + CustomersListWithConstraints, /// Retrieve countries and currencies for connector and payment method ListCountriesCurrencies, /// Payment method create collect link flow. diff --git a/crates/storage_impl/src/customers.rs b/crates/storage_impl/src/customers.rs index 53056a137c..befbf5d7f6 100644 --- a/crates/storage_impl/src/customers.rs +++ b/crates/storage_impl/src/customers.rs @@ -10,9 +10,8 @@ use hyperswitch_domain_models::{ use masking::PeekInterface; use router_env::{instrument, tracing}; -#[cfg(feature = "v1")] -use crate::diesel_error_to_data_error; use crate::{ + diesel_error_to_data_error, errors::StorageError, kv_router_store, redis::kv_store::{decide_storage_scheme, KvStorePartition, Op, PartitionKey}, @@ -290,6 +289,19 @@ impl domain::CustomerInterface for kv_router_store::KVRouterSt .await } + #[instrument(skip_all)] + async fn list_customers_by_merchant_id_with_count( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + key_store: &MerchantKeyStore, + constraints: domain::CustomerListConstraints, + ) -> CustomResult<(Vec, usize), StorageError> { + self.router_store + .list_customers_by_merchant_id_with_count(state, merchant_id, key_store, constraints) + .await + } + #[cfg(feature = "v2")] #[instrument(skip_all)] async fn insert_customer( @@ -661,11 +673,56 @@ impl domain::CustomerInterface for RouterStore { self.find_resources( state, key_store, - customers::Customer::list_by_merchant_id(&conn, merchant_id, customer_list_constraints), + customers::Customer::list_customers_by_merchant_id_and_constraints( + &conn, + merchant_id, + customer_list_constraints, + ), ) .await } + #[instrument(skip_all)] + async fn list_customers_by_merchant_id_with_count( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + key_store: &MerchantKeyStore, + constraints: domain::CustomerListConstraints, + ) -> CustomResult<(Vec, usize), StorageError> { + let conn = pg_connection_read(self).await?; + let customer_list_constraints = + diesel_models::query::customers::CustomerListConstraints::from(constraints); + let customers_constraints = diesel_models::query::customers::CustomerListConstraints { + limit: customer_list_constraints.limit, + offset: customer_list_constraints.offset, + customer_id: customer_list_constraints.customer_id.clone(), + time_range: customer_list_constraints.time_range, + }; + let customers = self + .find_resources( + state, + key_store, + customers::Customer::list_customers_by_merchant_id_and_constraints( + &conn, + merchant_id, + customers_constraints, + ), + ) + .await?; + let total_count = customers::Customer::get_customer_count_by_merchant_id_and_constraints( + &conn, + merchant_id, + customer_list_constraints, + ) + .await + .map_err(|error| { + let new_err = diesel_error_to_data_error(*error.current_context()); + error.change_context(new_err) + })?; + Ok((customers, total_count)) + } + #[instrument(skip_all)] async fn insert_customer( &self, @@ -822,6 +879,41 @@ impl domain::CustomerInterface for MockDb { Ok(customers) } + async fn list_customers_by_merchant_id_with_count( + &self, + state: &KeyManagerState, + merchant_id: &id_type::MerchantId, + key_store: &MerchantKeyStore, + constraints: domain::CustomerListConstraints, + ) -> CustomResult<(Vec, usize), StorageError> { + let customers = self.customers.lock().await; + + let customers_list = try_join_all( + customers + .iter() + .filter(|customer| customer.merchant_id == *merchant_id) + .take(usize::from(constraints.limit)) + .skip(usize::try_from(constraints.offset.unwrap_or(0)).unwrap_or(0)) + .map(|customer| async { + customer + .to_owned() + .convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(StorageError::DecryptionError) + }), + ) + .await?; + let total_count = customers + .iter() + .filter(|customer| customer.merchant_id == *merchant_id) + .count(); + Ok((customers_list, total_count)) + } + #[cfg(feature = "v1")] #[instrument(skip_all)] async fn update_customer_by_customer_id_merchant_id( From 2c4806d55a8a67861b3fef40e5feeac97e1ad4ce Mon Sep 17 00:00:00 2001 From: Vani Gupta <118043711+Vani-1107@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:41:58 +0530 Subject: [PATCH 15/16] feat(connector): [Finix] Add support for Apple Pay (#9810) Co-authored-by: nihtin Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Vani Gupta --- config/config.example.toml | 3 +- config/deployments/integration_test.toml | 1 + config/deployments/production.toml | 1 + config/deployments/sandbox.toml | 1 + config/development.toml | 3 +- config/docker_compose.toml | 3 +- .../connector_configs/toml/development.toml | 45 ++++++ crates/connector_configs/toml/production.toml | 45 ++++++ crates/connector_configs/toml/sandbox.toml | 45 ++++++ .../src/connectors/finix.rs | 10 ++ .../src/connectors/finix/transformers.rs | 135 ++++++++++++++---- .../connectors/finix/transformers/request.rs | 43 ++++++ loadtest/config/development.toml | 3 +- 13 files changed, 310 insertions(+), 28 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 24b589cf63..e35a691c4a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -359,7 +359,7 @@ stripe = { banks = "alior_bank,bank_millennium,bank_nowy_bfg_sa,bank_pekao_sa,ba # This data is used to call respective connectors for wallets and cards [connectors.supported] -wallets = ["klarna", "mifinity", "braintree", "applepay"] +wallets = ["klarna", "mifinity", "braintree", "applepay","finix"] rewards = ["cashtocode", "zen"] cards = [ "adyen", @@ -964,6 +964,7 @@ apple_pay = {country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD [pm_filters.finix] google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } [connector_customer] payout_connector_list = "nomupay,stripe,wise" diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index b276772142..e2eb4f23c6 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -448,6 +448,7 @@ pix = { country = "BR", currency = "BRL" } credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } [pm_filters.helcim] credit = { country = "US, CA", currency = "USD, CAD" } diff --git a/config/deployments/production.toml b/config/deployments/production.toml index bdeb8e486b..ebc1ba6800 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -531,6 +531,7 @@ pix = { country = "BR", currency = "BRL" } credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } [pm_filters.helcim] credit = { country = "US, CA", currency = "USD, CAD" } diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 2d73dcb60e..aa607823dd 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -513,6 +513,7 @@ pix = { country = "BR", currency = "BRL" } credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } [pm_filters.helcim] credit = { country = "US, CA", currency = "USD, CAD" } diff --git a/config/development.toml b/config/development.toml index fe95cea5d8..faf09e603a 100644 --- a/config/development.toml +++ b/config/development.toml @@ -90,7 +90,7 @@ vault_private_key = "" tunnel_private_key = "" [connectors.supported] -wallets = ["klarna", "mifinity", "braintree", "applepay", "adyen", "amazonpay"] +wallets = ["klarna", "mifinity", "braintree", "applepay", "adyen", "amazonpay", "finix"] rewards = ["cashtocode", "zen"] cards = [ "aci", @@ -715,6 +715,7 @@ pix = { country = "BR", currency = "BRL" } credit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} debit = { country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD,BB,BY,BE,BZ,BJ,BM,BT,BO,BQ,BA,BW,BV,BR,IO,BN,BG,BF,BI,KH,CM,CA,CV,KY,CF,TD,CL,CN,CX,CC,CO,KM,CG,CD,CK,CR,CI,HR,CU,CW,CY,CZ,DK,DJ,DM,DO,EC,EG,SV,GQ,ER,EE,ET,FK,FO,FJ,FI,FR,GF,PF,TF,GA,GM,GE,DE,GH,GI,GR,GL,GD,GP,GU,GT,GG,GN,GW,GY,HT,HM,VA,HN,HK,HU,IS,IN,ID,IR,IQ,IE,IM,IL,IT,JM,JP,JE,JO,KZ,KE,KI,KP,KR,KW,KG,LA,LV,LB,LS,LR,LY,LI,LT,LU,MO,MK,MG,MW,MY,MV,ML,MT,MH,MQ,MR,MU,YT,MX,FM,MD,MC,MN,ME,MS,MA,MZ,MM,NA,NR,NP,NL,NC,NZ,NI,NE,NG,NU,NF,MP,NO,OM,PK,PW,PS,PA,PG,PY,PE,PH,PN,PL,PT,PR,QA,RE,RO,RU,RW,BL,SH,KN,LC,MF,PM,VC,WS,SM,ST,SA,SN,RS,SC,SL,SG,SX,SK,SI,SB,SO,ZA,GS,SS,ES,LK,SD,SR,SJ,SZ,SE,CH,SY,TW,TJ,TZ,TH,TL,TG,TK,TO,TT,TN,TR,TM,TC,TV,UG,UA,AE,GB,UM,UY,UZ,VU,VE,VN,VG,VI,WF,EH,YE,ZM,ZW,US", currency = "AED,AFN,ALL,AMD,ANG,AOA,ARS,AUD,AWG,AZN,BAM,BBD,BDT,BGN,BHD,BIF,BMD,BND,BOB,BRL,BSD,BTN,BWP,BYN,BZD,CAD,CDF,CHF,CLF,CLP,CNY,COP,CRC,CUC,CUP,CVE,CZK,DJF,DKK,DOP,DZD,EGP,ERN,ETB,EUR,FJD,FKP,GBP,GEL,GHS,GIP,GMD,GNF,GTQ,GYD,HKD,HNL,HRK,HTG,HUF,IDR,ILS,INR,IQD,IRR,ISK,JMD,JOD,JPY,KES,KGS,KHR,KMF,KPW,KRW,KWD,KYD,KZT,LAK,LBP,LKR,LRD,LSL,LYD,MAD,MDL,MGA,MKD,MMK,MNT,MOP,MRU,MUR,MVR,MWK,MXN,MYR,MZN,NAD,NGN,NIO,NOK,NPR,NZD,OMR,PAB,PEN,PGK,PHP,PKR,PLN,PYG,QAR,RON,RSD,RUB,RWF,SAR,SBD,SCR,SDG,SEK,SGD,SHP,SLE,SLL,SOS,SRD,SSP,STD,STN,SVC,SYP,SZL,THB,TJS,TMT,TND,TOP,TRY,TTD,TWD,TZS,UAH,UGX,USD,UYU,UZS,VES,VND,VUV,WST,XAF,XCD,XOF,XPF,YER,ZAR,ZMW,ZWL"} google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } [pm_filters.helcim] credit = { country = "US, CA", currency = "USD, CAD" } diff --git a/config/docker_compose.toml b/config/docker_compose.toml index d91dcf71d9..4631498d13 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -264,7 +264,7 @@ zsl.base_url = "https://api.sitoffalb.net/" apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } [connectors.supported] -wallets = ["klarna", "mifinity", "braintree", "applepay", "amazonpay"] +wallets = ["klarna", "mifinity", "braintree", "applepay", "amazonpay","finix"] rewards = ["cashtocode", "zen"] cards = [ "aci", @@ -992,6 +992,7 @@ apple_pay = {country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD [pm_filters.finix] google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } [bank_config.online_banking_fpx] adyen.banks = "affin_bank,agro_bank,alliance_bank,am_bank,bank_islam,bank_muamalat,bank_rakyat,bank_simpanan_nasional,cimb_bank,hong_leong_bank,hsbc_bank,kuwait_finance_house,maybank,ocbc_bank,public_bank,rhb_bank,standard_chartered_bank,uob_bank" diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index e6ecce7b30..3dfcc3cbcf 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -7348,6 +7348,51 @@ payment_method_type = "Interac" payment_method_type = "Maestro" [[finix.wallet]] payment_method_type = "google_pay" +[[finix.wallet]] + payment_method_type = "apple_pay" +[[finix.metadata.apple_pay]] + name = "certificate" + label = "Merchant Certificate (Base64 Encoded)" + placeholder = "Enter Merchant Certificate (Base64 Encoded)" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "certificate_keys" + label = "Merchant PrivateKey (Base64 Encoded)" + placeholder = "Enter Merchant PrivateKey (Base64 Encoded)" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "merchant_identifier" + label = "Apple Merchant Identifier" + placeholder = "Enter Apple Merchant Identifier" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "display_name" + label = "Display Name" + placeholder = "Enter Display Name" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "initiative" + label = "Domain" + placeholder = "Enter Domain" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "initiative_context" + label = "Domain Name" + placeholder = "Enter Domain Name" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "merchant_business_country" + label = "Merchant Business Country" + placeholder = "Enter Merchant Business Country" + required = true + type = "Select" + options = [] [[finix.metadata.google_pay]] name = "merchant_name" label = "Google Pay Merchant Name" diff --git a/crates/connector_configs/toml/production.toml b/crates/connector_configs/toml/production.toml index 787801f797..ec40afab46 100644 --- a/crates/connector_configs/toml/production.toml +++ b/crates/connector_configs/toml/production.toml @@ -6084,6 +6084,51 @@ payment_method_type = "Interac" payment_method_type = "Maestro" [[finix.wallet]] payment_method_type = "google_pay" +[[finix.wallet]] + payment_method_type = "apple_pay" +[[finix.metadata.apple_pay]] + name = "certificate" + label = "Merchant Certificate (Base64 Encoded)" + placeholder = "Enter Merchant Certificate (Base64 Encoded)" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "certificate_keys" + label = "Merchant PrivateKey (Base64 Encoded)" + placeholder = "Enter Merchant PrivateKey (Base64 Encoded)" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "merchant_identifier" + label = "Apple Merchant Identifier" + placeholder = "Enter Apple Merchant Identifier" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "display_name" + label = "Display Name" + placeholder = "Enter Display Name" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "initiative" + label = "Domain" + placeholder = "Enter Domain" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "initiative_context" + label = "Domain Name" + placeholder = "Enter Domain Name" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "merchant_business_country" + label = "Merchant Business Country" + placeholder = "Enter Merchant Business Country" + required = true + type = "Select" + options = [] [[finix.metadata.google_pay]] name = "merchant_name" label = "Google Pay Merchant Name" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 1ae6e5be26..b002be33bc 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -7323,6 +7323,51 @@ payment_method_type = "Interac" payment_method_type = "Maestro" [[finix.wallet]] payment_method_type = "google_pay" +[[finix.wallet]] + payment_method_type = "apple_pay" +[[finix.metadata.apple_pay]] + name = "certificate" + label = "Merchant Certificate (Base64 Encoded)" + placeholder = "Enter Merchant Certificate (Base64 Encoded)" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "certificate_keys" + label = "Merchant PrivateKey (Base64 Encoded)" + placeholder = "Enter Merchant PrivateKey (Base64 Encoded)" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "merchant_identifier" + label = "Apple Merchant Identifier" + placeholder = "Enter Apple Merchant Identifier" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "display_name" + label = "Display Name" + placeholder = "Enter Display Name" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "initiative" + label = "Domain" + placeholder = "Enter Domain" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "initiative_context" + label = "Domain Name" + placeholder = "Enter Domain Name" + required = true + type = "Text" +[[finix.metadata.apple_pay]] + name = "merchant_business_country" + label = "Merchant Business Country" + placeholder = "Enter Merchant Business Country" + required = true + type = "Select" + options = [] [[finix.metadata.google_pay]] name = "merchant_name" label = "Google Pay Merchant Name" diff --git a/crates/hyperswitch_connectors/src/connectors/finix.rs b/crates/hyperswitch_connectors/src/connectors/finix.rs index 15e8f17bfb..9684bb5bd1 100644 --- a/crates/hyperswitch_connectors/src/connectors/finix.rs +++ b/crates/hyperswitch_connectors/src/connectors/finix.rs @@ -974,6 +974,16 @@ static FINIX_SUPPORTED_PAYMENT_METHODS: LazyLock = Lazy specific_features: None, }, ); + finix_supported_payment_methods.add( + common_enums::PaymentMethod::Wallet, + PaymentMethodType::ApplePay, + PaymentMethodDetails { + mandates: enums::FeatureStatus::NotSupported, + refunds: enums::FeatureStatus::Supported, + supported_capture_methods: default_capture_methods.clone(), + specific_features: None, + }, + ); finix_supported_payment_methods }); diff --git a/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs b/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs index d65faedc0e..c8502940c2 100644 --- a/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/finix/transformers.rs @@ -1,5 +1,6 @@ pub mod request; pub mod response; +use base64::Engine; use common_enums::{enums, AttemptStatus, CaptureMethod, CountryAlpha2, CountryAlpha3}; use common_utils::types::MinorUnit; use error_stack::ResultExt; @@ -163,7 +164,7 @@ impl TryFrom<&FinixRouterData<'_, Authorize, PaymentsAuthorizeData, PaymentsResp three_d_secure: None, }) } - PaymentMethodData::Wallet(WalletData::GooglePay(_)) => { + PaymentMethodData::Wallet(WalletData::ApplePay(_) | WalletData::GooglePay(_)) => { let source = item.router_data.get_payment_method_token()?; Ok(Self { amount: item.amount, @@ -185,6 +186,10 @@ impl TryFrom<&FinixRouterData<'_, Authorize, PaymentsAuthorizeData, PaymentsResp three_d_secure: None, }) } + PaymentMethodData::Wallet(_) => Err(ConnectorError::NotImplemented( + "Payment method not supported".to_string(), + ) + .into()), _ => Err( ConnectorError::NotImplemented("Payment method not supported".to_string()).into(), ), @@ -245,30 +250,112 @@ impl third_party_token: None, }) } - PaymentMethodData::Wallet(WalletData::GooglePay(google_pay_wallet_data)) => { - let third_party_token = google_pay_wallet_data - .tokenization_data - .get_encrypted_google_pay_token() - .change_context(ConnectorError::MissingRequiredField { - field_name: "google_pay_token", + PaymentMethodData::Wallet(wallet) => match wallet { + WalletData::GooglePay(google_pay_wallet_data) => { + let third_party_token = google_pay_wallet_data + .tokenization_data + .get_encrypted_google_pay_token() + .change_context(ConnectorError::MissingRequiredField { + field_name: "google_pay_token", + })?; + Ok(Self { + instrument_type: FinixPaymentInstrumentType::GOOGLEPAY, + name: item.router_data.get_optional_billing_full_name(), + identity: item.router_data.get_connector_customer_id()?, + number: None, + security_code: None, + expiration_month: None, + expiration_year: None, + tags: None, + address: None, + card_brand: None, + card_type: None, + additional_data: None, + merchant_identity: Some(item.merchant_identity_id.clone()), + third_party_token: Some(Secret::new(third_party_token)), + }) + } + WalletData::ApplePay(apple_pay_wallet_data) => { + let applepay_encrypt_data = apple_pay_wallet_data + .payment_data + .get_encrypted_apple_pay_payment_data_mandatory() + .change_context(ConnectorError::MissingRequiredField { + field_name: "Apple pay encrypted data", + })?; + + let decoded_data = base64::prelude::BASE64_STANDARD + .decode(applepay_encrypt_data) + .change_context(ConnectorError::InvalidDataFormat { + field_name: "apple_pay_encrypted_data", + })?; + + let apple_pay_token: FinixApplePayEncryptedData = serde_json::from_slice( + &decoded_data, + ) + .change_context(ConnectorError::InvalidDataFormat { + field_name: "apple_pay_token_json", })?; - Ok(Self { - instrument_type: FinixPaymentInstrumentType::GOOGLEPAY, - name: item.router_data.get_optional_billing_full_name(), - identity: item.router_data.get_connector_customer_id()?, - number: None, - security_code: None, - expiration_month: None, - expiration_year: None, - tags: None, - address: None, - card_brand: None, - card_type: None, - additional_data: None, - merchant_identity: Some(item.merchant_identity_id.clone()), - third_party_token: Some(Secret::new(third_party_token)), - }) - } + + let finix_token = FinixApplePayPaymentToken { + token: FinixApplePayToken { + payment_data: FinixApplePayEncryptedData { + data: apple_pay_token.data.clone(), + signature: apple_pay_token.signature.clone(), + header: FinixApplePayHeader { + public_key_hash: apple_pay_token.header.public_key_hash.clone(), + ephemeral_public_key: apple_pay_token + .header + .ephemeral_public_key + .clone(), + transaction_id: apple_pay_token.header.transaction_id.clone(), + }, + version: apple_pay_token.version.clone(), + }, + payment_method: FinixApplePayPaymentMethod { + display_name: Secret::new( + apple_pay_wallet_data.payment_method.display_name.clone(), + ), + network: Secret::new( + apple_pay_wallet_data.payment_method.network.clone(), + ), + method_type: Secret::new( + apple_pay_wallet_data.payment_method.pm_type.clone(), + ), + }, + transaction_identifier: apple_pay_wallet_data + .transaction_identifier + .clone(), + }, + }; + + let third_party_token = serde_json::to_string(&finix_token).change_context( + ConnectorError::InvalidDataFormat { + field_name: "apple pay token", + }, + )?; + + Ok(Self { + instrument_type: FinixPaymentInstrumentType::ApplePay, + name: item.router_data.get_optional_billing_full_name(), + number: None, + security_code: None, + expiration_month: None, + expiration_year: None, + identity: item.router_data.get_connector_customer_id()?, + tags: None, + address: None, + card_brand: None, + card_type: None, + additional_data: None, + merchant_identity: Some(item.merchant_identity_id.clone()), + third_party_token: Some(Secret::new(third_party_token)), + }) + } + _ => Err(ConnectorError::NotImplemented( + "Payment method not supported for tokenization".to_string(), + ) + .into()), + }, _ => Err(ConnectorError::NotImplemented( "Payment method not supported for tokenization".to_string(), ) diff --git a/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs b/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs index 399cf95350..d9ba0eee26 100644 --- a/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs +++ b/crates/hyperswitch_connectors/src/connectors/finix/transformers/request.rs @@ -50,6 +50,45 @@ pub struct FinixIdentityEntity { pub personal_address: Option, } +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +pub struct FinixApplePayPaymentToken { + pub token: FinixApplePayToken, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FinixApplePayHeader { + pub public_key_hash: String, + pub ephemeral_public_key: String, + pub transaction_id: String, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FinixApplePayEncryptedData { + pub data: Secret, + pub signature: Secret, + pub header: FinixApplePayHeader, + pub version: Secret, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FinixApplePayPaymentMethod { + pub display_name: Secret, + pub network: Secret, + #[serde(rename = "type")] + pub method_type: Secret, +} + +#[derive(Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FinixApplePayToken { + pub payment_data: FinixApplePayEncryptedData, + pub payment_method: FinixApplePayPaymentMethod, + pub transaction_identifier: String, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FinixCreatePaymentInstrumentRequest { #[serde(rename = "type")] @@ -155,6 +194,10 @@ pub enum FinixPaymentInstrumentType { #[serde(rename = "BANK_ACCOUNT")] BankAccount, + + #[serde(rename = "APPLE_PAY")] + ApplePay, + #[serde(other)] Unknown, } diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 34ff72eb48..3f0b631978 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -230,7 +230,7 @@ zsl.base_url = "https://api.sitoffalb.net/" apple_pay = { country = "AU,CN,HK,JP,MO,MY,NZ,SG,TW,AM,AT,AZ,BY,BE,BG,HR,CY,CZ,DK,EE,FO,FI,FR,GE,DE,GR,GL,GG,HU,IS,IE,IM,IT,KZ,JE,LV,LI,LT,LU,MT,MD,MC,ME,NL,NO,PL,PT,RO,SM,RS,SK,SI,ES,SE,CH,UA,GB,AR,CO,CR,BR,MX,PE,BH,IL,JO,KW,PS,QA,SA,AE,CA,UM,US,KR,VN,MA,ZA,VA,CL,SV,GT,HN,PA", currency = "AED,AUD,CHF,CAD,EUR,GBP,HKD,SGD,USD" } [connectors.supported] -wallets = ["klarna", "mifinity", "braintree", "applepay"] +wallets = ["klarna", "mifinity", "braintree", "applepay","finix"] rewards = ["cashtocode", "zen"] cards = [ "aci", @@ -701,6 +701,7 @@ apple_pay = {country = "AF,AX,AL,DZ,AS,AD,AO,AI,AQ,AG,AR,AM,AW,AU,AT,AZ,BS,BH,BD [pm_filters.finix] google_pay = { country = "AD, AE, AG, AI, AM, AO, AQ, AR, AS, AT, AU, AW, AX, AZ, BA, BB, BD, BE, BF, BG, BH, BI, BJ, BM, BN, BO, BQ, BR, BS, BT, BV, BW, BZ, CA, CC, CG, CH, CI, CK, CL, CM, CN, CO, CR, CV, CW, CX, CY, CZ, DE, DJ, DK, DM, DO, DZ, EC, EE, EG, EH, ER, ES, FI, FJ, FK, FM, FO, FR, GA, GB, GD, GE, GF, GG, GH, GI, GL, GM, GN, GP, GQ, GR, GS, GT, GU, GW, GY, HK, HM, HN, HR, HT, HU, ID, IE, IL, IM, IN, IO, IS, IT, JE, JM, JO, JP, KE, KG, KH, KI, KM, KN, KR, KW, KY, KZ, LA, LC, LI, LK, LR, LS, LT, LU, LV, MA, MC, MD, ME, MF, MG, MH, MK, MN, MO, MP, MQ, MR, MS, MT, MU, MV, MW, MX, MY, MZ, NA, NC, NE, NF, NG, NL, NO, NP, NR, NU, NZ, OM, PA, PE, PF, PG, PH, PK, PL, PM, PN, PR, PT, PW, PY, QA, RE, RO, RS, RW, SA, SB, SC, SE, SG, SH, SI, SJ, SK, SL, SM, SN, SR, ST, SV, SX, SZ, TC, TD, TF, TG, TH, TJ, TK, TL, TM, TN, TO, TT, TV, TZ, UG, UM, UY, UZ, VA, VC, VG, VI, VN, VU, WF, WS, YT, ZA, ZM, US", currency = "USD, CAD" } +apple_pay = { currency = "USD, CAD" } #tokenization configuration which describe token lifetime and payment method for specific connector [tokenization] From bd853345441e10441cc5c57d1d4582d6ebe0a206 Mon Sep 17 00:00:00 2001 From: awasthi21 <107559116+awasthi21@users.noreply.github.com> Date: Thu, 16 Oct 2025 12:56:39 +0530 Subject: [PATCH 16/16] feat(core): Add profile-level configuration for L2/L3 data enablement (#9683) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: likhinbopanna <131246334+likhinbopanna@users.noreply.github.com> --- api-reference/v1/openapi_spec_v1.json | 10 ++ api-reference/v2/openapi_spec_v2.json | 10 ++ crates/api_models/src/admin.rs | 20 ++++ crates/diesel_models/src/business_profile.rs | 10 ++ crates/diesel_models/src/schema.rs | 1 + crates/diesel_models/src/schema_v2.rs | 1 + .../src/business_profile.rs | 28 ++++++ crates/router/src/core/admin.rs | 2 + crates/router/src/core/payments.rs | 1 + .../payments/operations/payment_approve.rs | 1 + .../payments/operations/payment_cancel.rs | 1 + .../operations/payment_cancel_post_capture.rs | 1 + .../payments/operations/payment_capture.rs | 1 + .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 1 + .../payments/operations/payment_create.rs | 1 + .../operations/payment_post_session_tokens.rs | 1 + .../payments/operations/payment_reject.rs | 1 + .../payments/operations/payment_session.rs | 1 + .../core/payments/operations/payment_start.rs | 1 + .../payments/operations/payment_status.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../operations/payment_update_metadata.rs | 1 + .../payments_incremental_authorization.rs | 1 + .../payments/operations/tax_calculation.rs | 1 + .../router/src/core/payments/transformers.rs | 99 ++++++++++--------- crates/router/src/db/events.rs | 1 + crates/router/src/types/api/admin.rs | 3 + .../down.sql | 2 + .../up.sql | 2 + 30 files changed, 157 insertions(+), 49 deletions(-) create mode 100644 migrations/2025-10-06-093228_add_l2_l3_to_business_profile/down.sql create mode 100644 migrations/2025-10-06-093228_add_l2_l3_to_business_profile/up.sql diff --git a/api-reference/v1/openapi_spec_v1.json b/api-reference/v1/openapi_spec_v1.json index 00b5183c28..ac27acf5b3 100644 --- a/api-reference/v1/openapi_spec_v1.json +++ b/api-reference/v1/openapi_spec_v1.json @@ -29198,6 +29198,11 @@ "type": "string", "description": "Merchant Connector id to be stored for billing_processor connector", "nullable": true + }, + "is_l2_l3_enabled": { + "type": "boolean", + "description": "Flag to enable Level 2 and Level 3 processing data for card transactions", + "nullable": true } }, "additionalProperties": false @@ -29555,6 +29560,11 @@ "type": "string", "description": "Merchant Connector id to be stored for billing_processor connector", "nullable": true + }, + "is_l2_l3_enabled": { + "type": "boolean", + "description": "Flag to enable Level 2 and Level 3 processing data for card transactions", + "nullable": true } } }, diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index d56065e6dc..ca554a988b 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -22776,6 +22776,11 @@ "type": "string", "description": "Merchant Connector id to be stored for billing_processor connector", "nullable": true + }, + "is_l2_l3_enabled": { + "type": "boolean", + "description": "Flag to enable Level 2 and Level 3 processing data for card transactions", + "nullable": true } }, "additionalProperties": false @@ -23083,6 +23088,11 @@ "type": "string", "description": "Merchant Connector id to be stored for billing_processor connector", "nullable": true + }, + "is_l2_l3_enabled": { + "type": "boolean", + "description": "Flag to enable Level 2 and Level 3 processing data for card transactions", + "nullable": true } } }, diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 08b3a08f61..f1fc3d971e 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2214,6 +2214,10 @@ pub struct ProfileCreate { /// Merchant Connector id to be stored for billing_processor connector #[schema(value_type = Option)] pub billing_processor_id: Option, + + /// Flag to enable Level 2 and Level 3 processing data for card transactions + #[schema(value_type = Option)] + pub is_l2_l3_enabled: Option, } #[nutype::nutype( @@ -2375,6 +2379,10 @@ pub struct ProfileCreate { /// Merchant Connector id to be stored for billing_processor connector #[schema(value_type = Option)] pub billing_processor_id: Option, + + /// Flag to enable Level 2 and Level 3 processing data for card transactions + #[schema(value_type = Option)] + pub is_l2_l3_enabled: Option, } #[cfg(feature = "v1")] @@ -2579,6 +2587,10 @@ pub struct ProfileResponse { /// Merchant Connector id to be stored for billing_processor connector #[schema(value_type = Option)] pub billing_processor_id: Option, + + /// Flag to enable Level 2 and Level 3 processing data for card transactions + #[schema(value_type = Option)] + pub is_l2_l3_enabled: Option, } #[cfg(feature = "v2")] @@ -2753,6 +2765,10 @@ pub struct ProfileResponse { /// Merchant Connector id to be stored for billing_processor connector #[schema(value_type = Option)] pub billing_processor_id: Option, + + /// Flag to enable Level 2 and Level 3 processing data for card transactions + #[schema(value_type = Option)] + pub is_l2_l3_enabled: Option, } #[cfg(feature = "v1")] @@ -2947,6 +2963,10 @@ pub struct ProfileUpdate { /// Merchant Connector id to be stored for billing_processor connector #[schema(value_type = Option)] pub billing_processor_id: Option, + + /// Flag to enable Level 2 and Level 3 processing data for card transactions + #[schema(value_type = Option)] + pub is_l2_l3_enabled: Option, } #[cfg(feature = "v2")] diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index d0e4492712..854bf12f0f 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -83,6 +83,7 @@ pub struct Profile { pub billing_processor_id: Option, pub is_external_vault_enabled: Option, pub external_vault_connector_details: Option, + pub is_l2_l3_enabled: Option, } #[cfg(feature = "v1")] @@ -145,6 +146,7 @@ pub struct ProfileNew { pub billing_processor_id: Option, pub is_external_vault_enabled: Option, pub external_vault_connector_details: Option, + pub is_l2_l3_enabled: Option, } #[cfg(feature = "v1")] @@ -181,6 +183,7 @@ pub struct ProfileUpdateInternal { pub always_collect_shipping_details_from_wallet_connector: Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, + pub is_l2_l3_enabled: Option, pub dynamic_routing_algorithm: Option, pub is_network_tokenization_enabled: Option, pub is_auto_retries_enabled: Option, @@ -243,6 +246,7 @@ impl ProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector, tax_connector_id, is_tax_connector_enabled, + is_l2_l3_enabled, dynamic_routing_algorithm, is_network_tokenization_enabled, is_auto_retries_enabled, @@ -320,6 +324,7 @@ impl ProfileUpdateInternal { .or(source.always_collect_shipping_details_from_wallet_connector), tax_connector_id: tax_connector_id.or(source.tax_connector_id), is_tax_connector_enabled: is_tax_connector_enabled.or(source.is_tax_connector_enabled), + is_l2_l3_enabled: is_l2_l3_enabled.or(source.is_l2_l3_enabled), version: source.version, dynamic_routing_algorithm: dynamic_routing_algorithm .or(source.dynamic_routing_algorithm), @@ -431,6 +436,7 @@ pub struct Profile { pub billing_processor_id: Option, pub is_external_vault_enabled: Option, pub external_vault_connector_details: Option, + pub is_l2_l3_enabled: Option, pub routing_algorithm_id: Option, pub order_fulfillment_time: Option, pub order_fulfillment_time_origin: Option, @@ -519,6 +525,7 @@ pub struct ProfileNew { pub is_iframe_redirection_enabled: Option, pub is_external_vault_enabled: Option, pub external_vault_connector_details: Option, + pub is_l2_l3_enabled: Option, pub split_txns_enabled: Option, } @@ -580,6 +587,7 @@ pub struct ProfileUpdateInternal { pub is_iframe_redirection_enabled: Option, pub is_external_vault_enabled: Option, pub external_vault_connector_details: Option, + pub is_l2_l3_enabled: Option, pub split_txns_enabled: Option, } @@ -639,6 +647,7 @@ impl ProfileUpdateInternal { merchant_category_code, merchant_country_code, split_txns_enabled, + is_l2_l3_enabled, } = self; Profile { id: source.id, @@ -738,6 +747,7 @@ impl ProfileUpdateInternal { split_txns_enabled: split_txns_enabled.or(source.split_txns_enabled), is_manual_retry_enabled: None, always_enable_overcapture: None, + is_l2_l3_enabled: None, billing_processor_id: billing_processor_id.or(source.billing_processor_id), } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index daf84015cf..7d4e1609e4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -261,6 +261,7 @@ diesel::table! { billing_processor_id -> Nullable, is_external_vault_enabled -> Nullable, external_vault_connector_details -> Nullable, + is_l2_l3_enabled -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index e21c1fa101..11533805ce 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -256,6 +256,7 @@ diesel::table! { billing_processor_id -> Nullable, is_external_vault_enabled -> Nullable, external_vault_connector_details -> Nullable, + is_l2_l3_enabled -> Nullable, #[max_length = 64] routing_algorithm_id -> Nullable, order_fulfillment_time -> Nullable, diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index f0c656c38a..eec8a8e740 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -62,6 +62,7 @@ pub struct Profile { pub always_collect_shipping_details_from_wallet_connector: Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, + pub is_l2_l3_enabled: bool, pub version: common_enums::ApiVersion, pub dynamic_routing_algorithm: Option, pub is_network_tokenization_enabled: bool, @@ -222,6 +223,7 @@ pub struct ProfileSetter { pub always_collect_shipping_details_from_wallet_connector: Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: bool, + pub is_l2_l3_enabled: bool, pub dynamic_routing_algorithm: Option, pub is_network_tokenization_enabled: bool, pub is_auto_retries_enabled: bool, @@ -288,6 +290,7 @@ impl From for Profile { .always_collect_shipping_details_from_wallet_connector, tax_connector_id: value.tax_connector_id, is_tax_connector_enabled: value.is_tax_connector_enabled, + is_l2_l3_enabled: value.is_l2_l3_enabled, version: common_types::consts::API_VERSION, dynamic_routing_algorithm: value.dynamic_routing_algorithm, is_network_tokenization_enabled: value.is_network_tokenization_enabled, @@ -360,6 +363,7 @@ pub struct ProfileGeneralUpdate { Option, pub tax_connector_id: Option, pub is_tax_connector_enabled: Option, + pub is_l2_l3_enabled: Option, pub dynamic_routing_algorithm: Option, pub is_network_tokenization_enabled: Option, pub is_auto_retries_enabled: Option, @@ -448,6 +452,7 @@ impl From for ProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector, tax_connector_id, is_tax_connector_enabled, + is_l2_l3_enabled, dynamic_routing_algorithm, is_network_tokenization_enabled, is_auto_retries_enabled, @@ -512,6 +517,7 @@ impl From for ProfileUpdateInternal { always_collect_shipping_details_from_wallet_connector, tax_connector_id, is_tax_connector_enabled, + is_l2_l3_enabled, dynamic_routing_algorithm, is_network_tokenization_enabled, is_auto_retries_enabled, @@ -598,6 +604,7 @@ impl From for ProfileUpdateInternal { is_external_vault_enabled: None, external_vault_connector_details: None, billing_processor_id: None, + is_l2_l3_enabled: None, }, ProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -656,6 +663,7 @@ impl From for ProfileUpdateInternal { is_external_vault_enabled: None, external_vault_connector_details: None, billing_processor_id: None, + is_l2_l3_enabled: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -714,6 +722,7 @@ impl From for ProfileUpdateInternal { is_external_vault_enabled: None, external_vault_connector_details: None, billing_processor_id: None, + is_l2_l3_enabled: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -772,6 +781,7 @@ impl From for ProfileUpdateInternal { is_external_vault_enabled: None, external_vault_connector_details: None, billing_processor_id: None, + is_l2_l3_enabled: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -830,6 +840,7 @@ impl From for ProfileUpdateInternal { is_external_vault_enabled: None, external_vault_connector_details: None, billing_processor_id: None, + is_l2_l3_enabled: None, }, ProfileUpdate::CardTestingSecretKeyUpdate { card_testing_secret_key, @@ -888,6 +899,7 @@ impl From for ProfileUpdateInternal { is_external_vault_enabled: None, external_vault_connector_details: None, billing_processor_id: None, + is_l2_l3_enabled: None, }, ProfileUpdate::AcquirerConfigMapUpdate { acquirer_config_map, @@ -946,6 +958,7 @@ impl From for ProfileUpdateInternal { is_external_vault_enabled: None, external_vault_connector_details: None, billing_processor_id: None, + is_l2_l3_enabled: None, }, } } @@ -1001,6 +1014,7 @@ impl Conversion for Profile { .always_collect_shipping_details_from_wallet_connector, tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), + is_l2_l3_enabled: Some(self.is_l2_l3_enabled), version: self.version, dynamic_routing_algorithm: self.dynamic_routing_algorithm, is_network_tokenization_enabled: self.is_network_tokenization_enabled, @@ -1124,6 +1138,7 @@ impl Conversion for Profile { outgoing_webhook_custom_http_headers, tax_connector_id: item.tax_connector_id, is_tax_connector_enabled: item.is_tax_connector_enabled.unwrap_or(false), + is_l2_l3_enabled: item.is_l2_l3_enabled.unwrap_or(false), version: item.version, dynamic_routing_algorithm: item.dynamic_routing_algorithm, is_network_tokenization_enabled: item.is_network_tokenization_enabled, @@ -1198,6 +1213,7 @@ impl Conversion for Profile { .always_collect_shipping_details_from_wallet_connector, tax_connector_id: self.tax_connector_id, is_tax_connector_enabled: Some(self.is_tax_connector_enabled), + is_l2_l3_enabled: Some(self.is_l2_l3_enabled), version: self.version, is_network_tokenization_enabled: self.is_network_tokenization_enabled, is_auto_retries_enabled: Some(self.is_auto_retries_enabled), @@ -1727,6 +1743,7 @@ impl From for ProfileUpdateInternal { should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, + is_l2_l3_enabled: None, is_network_tokenization_enabled, is_auto_retries_enabled: None, max_auto_retries_enabled: None, @@ -1785,6 +1802,7 @@ impl From for ProfileUpdateInternal { should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, + is_l2_l3_enabled: None, is_network_tokenization_enabled: None, is_auto_retries_enabled: None, max_auto_retries_enabled: None, @@ -1841,6 +1859,7 @@ impl From for ProfileUpdateInternal { should_collect_cvv_during_payment: None, tax_connector_id: None, is_tax_connector_enabled: None, + is_l2_l3_enabled: None, is_network_tokenization_enabled: None, is_auto_retries_enabled: None, max_auto_retries_enabled: None, @@ -1876,6 +1895,7 @@ impl From for ProfileUpdateInternal { is_recon_enabled: None, applepay_verified_domains: None, payment_link_config: None, + is_l2_l3_enabled: None, session_expiry: None, authentication_connector_details: None, payout_link_config: None, @@ -1931,6 +1951,7 @@ impl From for ProfileUpdateInternal { metadata: None, is_recon_enabled: None, applepay_verified_domains: None, + is_l2_l3_enabled: None, payment_link_config: None, session_expiry: None, authentication_connector_details: None, @@ -1981,6 +2002,7 @@ impl From for ProfileUpdateInternal { modified_at: now, return_url: None, enable_payment_response_hash: None, + is_l2_l3_enabled: None, payment_response_hash_key: None, redirect_to_merchant_with_http_post: None, webhook_details: None, @@ -2070,6 +2092,7 @@ impl From for ProfileUpdateInternal { max_auto_retries_enabled: None, is_click_to_pay_enabled: None, authentication_product_ids: None, + is_l2_l3_enabled: None, three_ds_decision_manager_config: None, card_testing_guard_config: None, card_testing_secret_key: None, @@ -2129,6 +2152,7 @@ impl From for ProfileUpdateInternal { three_ds_decision_manager_config: Some(three_ds_decision_manager_config), card_testing_guard_config: None, card_testing_secret_key: None, + is_l2_l3_enabled: None, is_clear_pan_retries_enabled: None, is_debit_routing_enabled: None, merchant_business_country: None, @@ -2187,6 +2211,7 @@ impl From for ProfileUpdateInternal { card_testing_secret_key: card_testing_secret_key.map(Encryption::from), is_clear_pan_retries_enabled: None, is_debit_routing_enabled: None, + is_l2_l3_enabled: None, merchant_business_country: None, revenue_recovery_retry_algorithm_type: None, revenue_recovery_retry_algorithm_data: None, @@ -2226,6 +2251,7 @@ impl From for ProfileUpdateInternal { always_collect_billing_details_from_wallet_connector: None, always_collect_shipping_details_from_wallet_connector: None, routing_algorithm_id: None, + is_l2_l3_enabled: None, payout_routing_algorithm_id: None, order_fulfillment_time: None, order_fulfillment_time_origin: None, @@ -2335,6 +2361,7 @@ impl Conversion for Profile { dispute_polling_interval: None, split_txns_enabled: Some(self.split_txns_enabled), is_manual_retry_enabled: None, + is_l2_l3_enabled: None, always_enable_overcapture: None, billing_processor_id: self.billing_processor_id, }) @@ -2503,6 +2530,7 @@ impl Conversion for Profile { is_external_vault_enabled: self.is_external_vault_enabled, external_vault_connector_details: self.external_vault_connector_details, merchant_category_code: self.merchant_category_code, + is_l2_l3_enabled: None, merchant_country_code: self.merchant_country_code, split_txns_enabled: Some(self.split_txns_enabled), billing_processor_id: self.billing_processor_id, diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 9652bfdd52..ad7e8e1fae 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -3523,6 +3523,7 @@ impl ProfileCreateBridge for api::ProfileCreate { .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error while generating external vault details")?, billing_processor_id: self.billing_processor_id, + is_l2_l3_enabled: self.is_l2_l3_enabled.unwrap_or(false), })) } @@ -4027,6 +4028,7 @@ impl ProfileUpdateBridge for api::ProfileUpdate { .external_vault_connector_details .map(ForeignInto::foreign_into), billing_processor_id: self.billing_processor_id, + is_l2_l3_enabled: self.is_l2_l3_enabled, }, ))) } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 903dfa58d6..0fbf9927b3 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -7993,6 +7993,7 @@ where pub threeds_method_comp_ind: Option, pub whole_connector_response: Option>, pub is_manual_retry_enabled: Option, + pub is_l2_l3_enabled: bool, } #[derive(Clone, serde::Serialize, Debug)] diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 77764e1abe..37abdc4d9c 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -205,6 +205,7 @@ impl GetTracker, api::PaymentsCaptureR threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index e6e645cfa6..4b004fad2a 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -216,6 +216,7 @@ impl GetTracker, api::PaymentsCancelRe threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_cancel_post_capture.rs b/crates/router/src/core/payments/operations/payment_cancel_post_capture.rs index c02c0d7fa8..33890876bf 100644 --- a/crates/router/src/core/payments/operations/payment_cancel_post_capture.rs +++ b/crates/router/src/core/payments/operations/payment_cancel_post_capture.rs @@ -199,6 +199,7 @@ impl GetTracker, api::PaymentsCancelPo threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index 51eef70f5a..24a32fc0c7 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -271,6 +271,7 @@ impl GetTracker, api::Paymen threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 793b3c1d3c..94a1bb70fa 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -364,6 +364,7 @@ impl GetTracker, api::PaymentsRequest> threeds_method_comp_ind: request.threeds_method_comp_ind.clone(), whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: business_profile.is_l2_l3_enabled, }; let customer_details = Some(CustomerDetails { diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index d0e08ee0c8..293cfb2cda 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -832,6 +832,7 @@ impl GetTracker, api::PaymentsRequest> threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: business_profile.is_manual_retry_enabled, + is_l2_l3_enabled: business_profile.is_l2_l3_enabled, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 854659a61f..18873c346a 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -630,6 +630,7 @@ impl GetTracker, api::PaymentsRequest> threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: business_profile.is_l2_l3_enabled, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs index d4f9703bd8..da4c5211c0 100644 --- a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs +++ b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs @@ -177,6 +177,7 @@ impl GetTracker, api::PaymentsPostSess threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 28f0c05e68..f40b90c667 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -203,6 +203,7 @@ impl GetTracker, PaymentsCancelRequest threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 0e36d1485e..125957f70b 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -226,6 +226,7 @@ impl GetTracker, api::PaymentsSessionR threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index c782494b56..af50f42408 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -213,6 +213,7 @@ impl GetTracker, api::PaymentsStartReq threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 31d86bfcec..5ab7fe34f7 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -554,6 +554,7 @@ async fn get_tracker_for_sync< threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: business_profile.is_manual_retry_enabled, + is_l2_l3_enabled: business_profile.is_l2_l3_enabled, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index cfd5734662..f741eb48f1 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -522,6 +522,7 @@ impl GetTracker, api::PaymentsRequest> threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: business_profile.is_l2_l3_enabled, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/payment_update_metadata.rs b/crates/router/src/core/payments/operations/payment_update_metadata.rs index 265b4da26b..47622f657c 100644 --- a/crates/router/src/core/payments/operations/payment_update_metadata.rs +++ b/crates/router/src/core/payments/operations/payment_update_metadata.rs @@ -163,6 +163,7 @@ impl GetTracker, api::PaymentsUpdateMe threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index 3528b491a2..44fcf23e5a 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -182,6 +182,7 @@ impl threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { diff --git a/crates/router/src/core/payments/operations/tax_calculation.rs b/crates/router/src/core/payments/operations/tax_calculation.rs index 5a3e44f053..9d9cfb82ec 100644 --- a/crates/router/src/core/payments/operations/tax_calculation.rs +++ b/crates/router/src/core/payments/operations/tax_calculation.rs @@ -192,6 +192,7 @@ impl threeds_method_comp_ind: None, whole_connector_response: None, is_manual_retry_enabled: None, + is_l2_l3_enabled: false, }; let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 53876336f0..51730d485f 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1747,57 +1747,58 @@ where .collect::, _>>() }) .transpose()?; - let l2_l3_data = state.conf.l2_l3_data_config.enabled.then(|| { - let shipping_address = unified_address.get_shipping(); - let billing_address = unified_address.get_payment_billing(); - let merchant_tax_registration_id = merchant_context - .get_merchant_account() - .get_merchant_tax_registration_id(); + let l2_l3_data = + (state.conf.l2_l3_data_config.enabled && payment_data.is_l2_l3_enabled).then(|| { + let shipping_address = unified_address.get_shipping(); + let billing_address = unified_address.get_payment_billing(); + let merchant_tax_registration_id = merchant_context + .get_merchant_account() + .get_merchant_tax_registration_id(); - types::L2L3Data { - order_date: payment_data.payment_intent.order_date, - tax_status: payment_data.payment_intent.tax_status, - customer_tax_registration_id: customer.as_ref().and_then(|c| { - c.tax_registration_id + types::L2L3Data { + order_date: payment_data.payment_intent.order_date, + tax_status: payment_data.payment_intent.tax_status, + customer_tax_registration_id: customer.as_ref().and_then(|c| { + c.tax_registration_id + .as_ref() + .map(|e| e.clone().into_inner()) + }), + order_details: order_details.clone(), + discount_amount: payment_data.payment_intent.discount_amount, + shipping_cost: payment_data.payment_intent.shipping_cost, + shipping_amount_tax: payment_data.payment_intent.shipping_amount_tax, + duty_amount: payment_data.payment_intent.duty_amount, + order_tax_amount: payment_data + .payment_attempt + .net_amount + .get_order_tax_amount(), + merchant_order_reference_id: payment_data + .payment_intent + .merchant_order_reference_id + .clone(), + customer_id: payment_data.payment_intent.customer_id.clone(), + shipping_origin_zip: shipping_address + .and_then(|addr| addr.address.as_ref()) + .and_then(|details| details.origin_zip.clone()), + shipping_state: shipping_address .as_ref() - .map(|e| e.clone().into_inner()) - }), - order_details: order_details.clone(), - discount_amount: payment_data.payment_intent.discount_amount, - shipping_cost: payment_data.payment_intent.shipping_cost, - shipping_amount_tax: payment_data.payment_intent.shipping_amount_tax, - duty_amount: payment_data.payment_intent.duty_amount, - order_tax_amount: payment_data - .payment_attempt - .net_amount - .get_order_tax_amount(), - merchant_order_reference_id: payment_data - .payment_intent - .merchant_order_reference_id - .clone(), - customer_id: payment_data.payment_intent.customer_id.clone(), - shipping_origin_zip: shipping_address - .and_then(|addr| addr.address.as_ref()) - .and_then(|details| details.origin_zip.clone()), - shipping_state: shipping_address - .as_ref() - .and_then(|addr| addr.address.as_ref()) - .and_then(|details| details.state.clone()), - shipping_country: shipping_address - .as_ref() - .and_then(|addr| addr.address.as_ref()) - .and_then(|details| details.country), - shipping_destination_zip: shipping_address - .as_ref() - .and_then(|addr| addr.address.as_ref()) - .and_then(|details| details.zip.clone()), - billing_address_city: billing_address - .as_ref() - .and_then(|addr| addr.address.as_ref()) - .and_then(|details| details.city.clone()), - merchant_tax_registration_id, - } - }); + .and_then(|addr| addr.address.as_ref()) + .and_then(|details| details.state.clone()), + shipping_country: shipping_address + .as_ref() + .and_then(|addr| addr.address.as_ref()) + .and_then(|details| details.country), + shipping_destination_zip: shipping_address + .as_ref() + .and_then(|addr| addr.address.as_ref()) + .and_then(|details| details.zip.clone()), + billing_address_city: billing_address + .as_ref() + .and_then(|addr| addr.address.as_ref()) + .and_then(|details| details.city.clone()), + merchant_tax_registration_id, + } + }); crate::logger::debug!("unified address details {:?}", unified_address); let router_data = types::RouterData { diff --git a/crates/router/src/db/events.rs b/crates/router/src/db/events.rs index 10c1b8ddb2..f7d2e4bec0 100644 --- a/crates/router/src/db/events.rs +++ b/crates/router/src/db/events.rs @@ -1290,6 +1290,7 @@ mod tests { always_enable_overcapture: None, external_vault_details: domain::ExternalVaultDetails::Skip, billing_processor_id: None, + is_l2_l3_enabled: false, }); let business_profile = state diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index eedac7c401..004b04f0c3 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -241,6 +241,7 @@ impl ForeignTryFrom for ProfileResponse { external_vault_connector_details: external_vault_connector_details .map(ForeignFrom::foreign_from), billing_processor_id: item.billing_processor_id, + is_l2_l3_enabled: Some(item.is_l2_l3_enabled), }) } } @@ -322,6 +323,7 @@ impl ForeignTryFrom for ProfileResponse { merchant_business_country: item.merchant_business_country, is_iframe_redirection_enabled: item.is_iframe_redirection_enabled, is_external_vault_enabled: item.is_external_vault_enabled, + is_l2_l3_enabled: None, external_vault_connector_details: item .external_vault_connector_details .map(ForeignInto::foreign_into), @@ -511,5 +513,6 @@ pub async fn create_profile_from_merchant_account( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error while generating external_vault_details")?, billing_processor_id: request.billing_processor_id, + is_l2_l3_enabled: request.is_l2_l3_enabled.unwrap_or(false), })) } diff --git a/migrations/2025-10-06-093228_add_l2_l3_to_business_profile/down.sql b/migrations/2025-10-06-093228_add_l2_l3_to_business_profile/down.sql new file mode 100644 index 0000000000..e7c9b7e695 --- /dev/null +++ b/migrations/2025-10-06-093228_add_l2_l3_to_business_profile/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile DROP COLUMN IF EXISTS is_l2_l3_enabled; diff --git a/migrations/2025-10-06-093228_add_l2_l3_to_business_profile/up.sql b/migrations/2025-10-06-093228_add_l2_l3_to_business_profile/up.sql new file mode 100644 index 0000000000..3acb8b0c05 --- /dev/null +++ b/migrations/2025-10-06-093228_add_l2_l3_to_business_profile/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS is_l2_l3_enabled BOOLEAN;