From 4b989fe0fb7931479e127fecbaace42d989c0620 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:23:31 +0530 Subject: [PATCH 01/20] feat(users): Incorporate themes in user APIs (#6772) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- config/config.example.toml | 9 +- config/deployments/env_specific.toml | 11 +- config/development.toml | 11 +- config/docker_compose.toml | 9 +- crates/api_models/src/user.rs | 4 +- crates/api_models/src/user/theme.rs | 8 +- crates/common_utils/src/types/theme.rs | 112 +++++++++++++++-- crates/diesel_models/src/query/user/theme.rs | 65 ++++++++-- crates/diesel_models/src/schema.rs | 9 ++ crates/diesel_models/src/schema_v2.rs | 9 ++ crates/diesel_models/src/user/theme.rs | 39 +++++- .../src/configs/secrets_transformers.rs | 2 +- crates/router/src/configs/settings.rs | 13 +- crates/router/src/core/errors/user.rs | 6 + crates/router/src/core/recon.rs | 47 +++++++- crates/router/src/core/user.rs | 114 ++++++++++++++++-- .../src/core/user/dashboard_metadata.rs | 22 +++- crates/router/src/core/user/theme.rs | 13 ++ crates/router/src/db/kafka_store.rs | 9 ++ crates/router/src/db/user/theme.rs | 54 +++++++-- crates/router/src/routes/app.rs | 2 +- crates/router/src/routes/user.rs | 52 +++++--- crates/router/src/services/email/types.rs | 43 ++++++- crates/router/src/utils/user/theme.rs | 78 +++++++++++- crates/router/src/workflows/api_key_expiry.rs | 23 +++- .../down.sql | 6 + .../up.sql | 6 + 27 files changed, 687 insertions(+), 89 deletions(-) create mode 100644 migrations/2024-12-05-131123_add-email-theme-data-in-themes/down.sql create mode 100644 migrations/2024-12-05-131123_add-email-theme-data-in-themes/up.sql diff --git a/config/config.example.toml b/config/config.example.toml index 5aa9e543fb22..0fe6b8caa2c7 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -797,5 +797,12 @@ host = "localhost" # Client Host port = 7000 # Client Port service = "dynamo" # Service name -[theme_storage] +[theme.storage] file_storage_backend = "file_system" # Theme storage backend to be used + +[theme.email_config] +entity_name = "Hyperswitch" # Name of the entity to be showed in emails +entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails +foreground_color = "#000000" # Foreground color of email text +primary_color = "#006DF9" # Primary color of email body +background_color = "#FFFFFF" # Background color of email body diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 5cadc66ddfc5..848a2305ae46 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -328,9 +328,16 @@ host = "localhost" # Client Host port = 7000 # Client Port service = "dynamo" # Service name -[theme_storage] +[theme.storage] file_storage_backend = "aws_s3" # Theme storage backend to be used -[theme_storage.aws_s3] +[theme.storage.aws_s3] region = "bucket_region" # AWS region where the S3 bucket for theme storage is located bucket_name = "bucket" # AWS S3 bucket name for theme storage + +[theme.email_config] +entity_name = "Hyperswitch" # Name of the entity to be showed in emails +entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails +foreground_color = "#000000" # Foreground color of email text +primary_color = "#006DF9" # Primary color of email body +background_color = "#FFFFFF" # Background color of email body diff --git a/config/development.toml b/config/development.toml index fa577a78e6ad..077e09f04635 100644 --- a/config/development.toml +++ b/config/development.toml @@ -797,5 +797,12 @@ host = "localhost" port = 7000 service = "dynamo" -[theme_storage] -file_storage_backend = "file_system" +[theme.storage] +file_storage_backend = "file_system" # Theme storage backend to be used + +[theme.email_config] +entity_name = "Hyperswitch" # Name of the entity to be showed in emails +entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails +foreground_color = "#000000" # Foreground color of email text +primary_color = "#006DF9" # Primary color of email body +background_color = "#FFFFFF" # Background color of email body diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 3cc690203e68..dbd5286b2900 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -694,5 +694,12 @@ prod_intent_recipient_email = "business@example.com" # Recipient email for prod email_role_arn = "" # The amazon resource name ( arn ) of the role which has permission to send emails sts_role_session_name = "" # An identifier for the assumed role session, used to uniquely identify a session. -[theme_storage] +[theme.storage] file_storage_backend = "file_system" # Theme storage backend to be used + +[theme.email_config] +entity_name = "Hyperswitch" # Name of the entity to be showed in emails +entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be used in emails +foreground_color = "#000000" # Foreground color of email text +primary_color = "#006DF9" # Primary color of email body +background_color = "#FFFFFF" # Background color of email body diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index e61696803a20..7b5911cf1a84 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -150,6 +150,7 @@ pub struct GetUserDetailsResponse { pub recovery_codes_left: Option, pub profile_id: id_type::ProfileId, pub entity_type: EntityType, + pub theme_id: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -345,8 +346,9 @@ pub struct SsoSignInRequest { } #[derive(Debug, serde::Deserialize, serde::Serialize)] -pub struct AuthIdQueryParam { +pub struct AuthIdAndThemeIdQueryParam { pub auth_id: Option, + pub theme_id: Option, } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/api_models/src/user/theme.rs b/crates/api_models/src/user/theme.rs index 23218c4a1633..c79da307b9a8 100644 --- a/crates/api_models/src/user/theme.rs +++ b/crates/api_models/src/user/theme.rs @@ -1,6 +1,9 @@ use actix_multipart::form::{bytes::Bytes, text::Text, MultipartForm}; use common_enums::EntityType; -use common_utils::{id_type, types::theme::ThemeLineage}; +use common_utils::{ + id_type, + types::theme::{EmailThemeConfig, ThemeLineage}, +}; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -13,6 +16,7 @@ pub struct GetThemeResponse { pub org_id: Option, pub merchant_id: Option, pub profile_id: Option, + pub email_config: EmailThemeConfig, pub theme_data: ThemeData, } @@ -35,12 +39,14 @@ pub struct CreateThemeRequest { pub lineage: ThemeLineage, pub theme_name: String, pub theme_data: ThemeData, + pub email_config: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct UpdateThemeRequest { pub lineage: ThemeLineage, pub theme_data: ThemeData, + // TODO: Add support to update email config } // All the below structs are for the theme.json file, diff --git a/crates/common_utils/src/types/theme.rs b/crates/common_utils/src/types/theme.rs index 239cffc40d45..05ae26b565a7 100644 --- a/crates/common_utils/src/types/theme.rs +++ b/crates/common_utils/src/types/theme.rs @@ -1,4 +1,5 @@ use common_enums::EntityType; +use serde::{Deserialize, Serialize}; use crate::{ events::{ApiEventMetric, ApiEventsType}, @@ -7,15 +8,14 @@ use crate::{ /// Enum for having all the required lineage for every level. /// Currently being used for theme related APIs and queries. -#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[derive(Debug, Deserialize, Serialize)] #[serde(tag = "entity_type", rename_all = "snake_case")] pub enum ThemeLineage { - // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType - // /// Tenant lineage variant - // Tenant { - // /// tenant_id: String - // tenant_id: String, - // }, + /// Tenant lineage variant + Tenant { + /// tenant_id: TenantId + tenant_id: id_type::TenantId, + }, /// Org lineage variant Organization { /// tenant_id: TenantId @@ -48,9 +48,35 @@ pub enum ThemeLineage { impl_api_event_type!(Miscellaneous, (ThemeLineage)); impl ThemeLineage { + /// Constructor for ThemeLineage + pub fn new( + entity_type: EntityType, + tenant_id: id_type::TenantId, + org_id: id_type::OrganizationId, + merchant_id: id_type::MerchantId, + profile_id: id_type::ProfileId, + ) -> Self { + match entity_type { + EntityType::Tenant => Self::Tenant { tenant_id }, + EntityType::Organization => Self::Organization { tenant_id, org_id }, + EntityType::Merchant => Self::Merchant { + tenant_id, + org_id, + merchant_id, + }, + EntityType::Profile => Self::Profile { + tenant_id, + org_id, + merchant_id, + profile_id, + }, + } + } + /// Get the entity_type from the lineage pub fn entity_type(&self) -> EntityType { match self { + Self::Tenant { .. } => EntityType::Tenant, Self::Organization { .. } => EntityType::Organization, Self::Merchant { .. } => EntityType::Merchant, Self::Profile { .. } => EntityType::Profile, @@ -60,7 +86,8 @@ impl ThemeLineage { /// Get the tenant_id from the lineage pub fn tenant_id(&self) -> &id_type::TenantId { match self { - Self::Organization { tenant_id, .. } + Self::Tenant { tenant_id } + | Self::Organization { tenant_id, .. } | Self::Merchant { tenant_id, .. } | Self::Profile { tenant_id, .. } => tenant_id, } @@ -69,6 +96,7 @@ impl ThemeLineage { /// Get the org_id from the lineage pub fn org_id(&self) -> Option<&id_type::OrganizationId> { match self { + Self::Tenant { .. } => None, Self::Organization { org_id, .. } | Self::Merchant { org_id, .. } | Self::Profile { org_id, .. } => Some(org_id), @@ -78,7 +106,7 @@ impl ThemeLineage { /// Get the merchant_id from the lineage pub fn merchant_id(&self) -> Option<&id_type::MerchantId> { match self { - Self::Organization { .. } => None, + Self::Tenant { .. } | Self::Organization { .. } => None, Self::Merchant { merchant_id, .. } | Self::Profile { merchant_id, .. } => { Some(merchant_id) } @@ -88,8 +116,72 @@ impl ThemeLineage { /// Get the profile_id from the lineage pub fn profile_id(&self) -> Option<&id_type::ProfileId> { match self { - Self::Organization { .. } | Self::Merchant { .. } => None, + Self::Tenant { .. } | Self::Organization { .. } | Self::Merchant { .. } => None, Self::Profile { profile_id, .. } => Some(profile_id), } } + + /// Get higher lineages from the current lineage + pub fn get_same_and_higher_lineages(self) -> Vec { + match &self { + Self::Tenant { .. } => vec![self], + Self::Organization { tenant_id, .. } => vec![ + Self::Tenant { + tenant_id: tenant_id.clone(), + }, + self, + ], + Self::Merchant { + tenant_id, org_id, .. + } => vec![ + Self::Tenant { + tenant_id: tenant_id.clone(), + }, + Self::Organization { + tenant_id: tenant_id.clone(), + org_id: org_id.clone(), + }, + self, + ], + Self::Profile { + tenant_id, + org_id, + merchant_id, + .. + } => vec![ + Self::Tenant { + tenant_id: tenant_id.clone(), + }, + Self::Organization { + tenant_id: tenant_id.clone(), + org_id: org_id.clone(), + }, + Self::Merchant { + tenant_id: tenant_id.clone(), + org_id: org_id.clone(), + merchant_id: merchant_id.clone(), + }, + self, + ], + } + } +} + +/// Struct for holding the theme settings for email +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct EmailThemeConfig { + /// The entity name to be used in the email + pub entity_name: String, + + /// The URL of the entity logo to be used in the email + pub entity_logo_url: String, + + /// The primary color to be used in the email + pub primary_color: String, + + /// The foreground color to be used in the email + pub foreground_color: String, + + /// The background color to be used in the email + pub background_color: String, } diff --git a/crates/diesel_models/src/query/user/theme.rs b/crates/diesel_models/src/query/user/theme.rs index a26e401ffbb3..945ce1a9bb22 100644 --- a/crates/diesel_models/src/query/user/theme.rs +++ b/crates/diesel_models/src/query/user/theme.rs @@ -1,13 +1,22 @@ +use async_bb8_diesel::AsyncRunQueryDsl; use common_utils::types::theme::ThemeLineage; use diesel::{ associations::HasTable, + debug_query, pg::Pg, + result::Error as DieselError, sql_types::{Bool, Nullable}, - BoolExpressionMethods, ExpressionMethods, + BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl, }; +use error_stack::{report, ResultExt}; +use router_env::logger; use crate::{ - query::generics, + errors::DatabaseError, + query::generics::{ + self, + db_metrics::{track_database_call, DatabaseOperation}, + }, schema::themes::dsl, user::theme::{Theme, ThemeNew}, PgPooledConn, StorageResult, @@ -27,15 +36,14 @@ impl Theme { + 'static, > { match lineage { - // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType - // ThemeLineage::Tenant { tenant_id } => Box::new( - // dsl::tenant_id - // .eq(tenant_id) - // .and(dsl::org_id.is_null()) - // .and(dsl::merchant_id.is_null()) - // .and(dsl::profile_id.is_null()) - // .nullable(), - // ), + ThemeLineage::Tenant { tenant_id } => Box::new( + dsl::tenant_id + .eq(tenant_id) + .and(dsl::org_id.is_null()) + .and(dsl::merchant_id.is_null()) + .and(dsl::profile_id.is_null()) + .nullable(), + ), ThemeLineage::Organization { tenant_id, org_id } => Box::new( dsl::tenant_id .eq(tenant_id) @@ -77,6 +85,41 @@ impl Theme { .await } + pub async fn find_most_specific_theme_in_lineage( + conn: &PgPooledConn, + lineage: ThemeLineage, + ) -> StorageResult { + let query = ::table().into_boxed(); + + let query = + lineage + .get_same_and_higher_lineages() + .into_iter() + .fold(query, |mut query, lineage| { + query = query.or_filter(Self::lineage_filter(lineage)); + query + }); + + logger::debug!(query = %debug_query::(&query).to_string()); + + let data: Vec = match track_database_call::( + query.get_results_async(conn), + DatabaseOperation::Filter, + ) + .await + { + Ok(value) => Ok(value), + Err(err) => match err { + DieselError::NotFound => Err(report!(err)).change_context(DatabaseError::NotFound), + _ => Err(report!(err)).change_context(DatabaseError::Others), + }, + }?; + + data.into_iter() + .min_by_key(|theme| theme.entity_type) + .ok_or(report!(DatabaseError::NotFound)) + } + pub async fn find_by_lineage( conn: &PgPooledConn, lineage: ThemeLineage, diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 782bdb1b9d38..178f5600542a 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -1321,6 +1321,15 @@ diesel::table! { entity_type -> Varchar, #[max_length = 64] theme_name -> Varchar, + #[max_length = 64] + email_primary_color -> Varchar, + #[max_length = 64] + email_foreground_color -> Varchar, + #[max_length = 64] + email_background_color -> Varchar, + #[max_length = 64] + email_entity_name -> Varchar, + email_entity_logo_url -> Text, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index cb1eaad64f4a..da2298d934bb 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -1268,6 +1268,15 @@ diesel::table! { entity_type -> Varchar, #[max_length = 64] theme_name -> Varchar, + #[max_length = 64] + email_primary_color -> Varchar, + #[max_length = 64] + email_foreground_color -> Varchar, + #[max_length = 64] + email_background_color -> Varchar, + #[max_length = 64] + email_entity_name -> Varchar, + email_entity_logo_url -> Text, } } diff --git a/crates/diesel_models/src/user/theme.rs b/crates/diesel_models/src/user/theme.rs index e43a182126b7..46cc90a45eca 100644 --- a/crates/diesel_models/src/user/theme.rs +++ b/crates/diesel_models/src/user/theme.rs @@ -1,5 +1,8 @@ use common_enums::EntityType; -use common_utils::{date_time, id_type, types::theme::ThemeLineage}; +use common_utils::{ + date_time, id_type, + types::theme::{EmailThemeConfig, ThemeLineage}, +}; use diesel::{Identifiable, Insertable, Queryable, Selectable}; use time::PrimitiveDateTime; @@ -17,6 +20,11 @@ pub struct Theme { pub last_modified_at: PrimitiveDateTime, pub entity_type: EntityType, pub theme_name: String, + pub email_primary_color: String, + pub email_foreground_color: String, + pub email_background_color: String, + pub email_entity_name: String, + pub email_entity_logo_url: String, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -31,10 +39,20 @@ pub struct ThemeNew { pub last_modified_at: PrimitiveDateTime, pub entity_type: EntityType, pub theme_name: String, + pub email_primary_color: String, + pub email_foreground_color: String, + pub email_background_color: String, + pub email_entity_name: String, + pub email_entity_logo_url: String, } impl ThemeNew { - pub fn new(theme_id: String, theme_name: String, lineage: ThemeLineage) -> Self { + pub fn new( + theme_id: String, + theme_name: String, + lineage: ThemeLineage, + email_config: EmailThemeConfig, + ) -> Self { let now = date_time::now(); Self { @@ -47,6 +65,23 @@ impl ThemeNew { entity_type: lineage.entity_type(), created_at: now, last_modified_at: now, + email_primary_color: email_config.primary_color, + email_foreground_color: email_config.foreground_color, + email_background_color: email_config.background_color, + email_entity_name: email_config.entity_name, + email_entity_logo_url: email_config.entity_logo_url, + } + } +} + +impl Theme { + pub fn email_config(&self) -> EmailThemeConfig { + EmailThemeConfig { + primary_color: self.email_primary_color.clone(), + foreground_color: self.email_foreground_color.clone(), + background_color: self.email_background_color.clone(), + entity_name: self.email_entity_name.clone(), + entity_logo_url: self.email_entity_logo_url.clone(), } } } diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 3ab56266b555..3862f70536fc 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -545,6 +545,6 @@ pub(crate) async fn fetch_raw_secrets( .network_tokenization_supported_card_networks, network_tokenization_service, network_tokenization_supported_connectors: conf.network_tokenization_supported_connectors, - theme_storage: conf.theme_storage, + theme: conf.theme, } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 1da4a33f5f33..4ab2a8fd5c77 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -6,7 +6,7 @@ use std::{ #[cfg(feature = "olap")] use analytics::{opensearch::OpenSearchConfig, ReportConfig}; use api_models::{enums, payment_methods::RequiredFieldInfo}; -use common_utils::{ext_traits::ConfigExt, id_type}; +use common_utils::{ext_traits::ConfigExt, id_type, types::theme::EmailThemeConfig}; use config::{Environment, File}; use error_stack::ResultExt; #[cfg(feature = "email")] @@ -128,7 +128,7 @@ pub struct Settings { pub network_tokenization_supported_card_networks: NetworkTokenizationSupportedCardNetworks, pub network_tokenization_service: Option>, pub network_tokenization_supported_connectors: NetworkTokenizationSupportedConnectors, - pub theme_storage: FileStorageConfig, + pub theme: ThemeSettings, } #[derive(Debug, Deserialize, Clone, Default)] @@ -887,7 +887,8 @@ impl Settings { .validate() .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.into()))?; - self.theme_storage + self.theme + .storage .validate() .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?; @@ -992,6 +993,12 @@ impl Default for CellInformation { } } +#[derive(Debug, Deserialize, Clone, Default)] +pub struct ThemeSettings { + pub storage: FileStorageConfig, + pub email_config: EmailThemeConfig, +} + fn deserialize_hashmap_inner( value: HashMap, ) -> Result>, String> diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 33477df6330a..fa5f185ab824 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -106,6 +106,8 @@ pub enum UserErrors { ThemeAlreadyExists, #[error("Invalid field: {0} in lineage")] InvalidThemeLineage(String), + #[error("Missing required field: email_config")] + MissingEmailConfig, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -275,6 +277,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 55, self.get_error_message(), None)) } + Self::MissingEmailConfig => { + AER::BadRequest(ApiError::new(sub_code, 56, self.get_error_message(), None)) + } } } } @@ -341,6 +346,7 @@ impl UserErrors { Self::InvalidThemeLineage(field_name) => { format!("Invalid field: {} in lineage", field_name) } + Self::MissingEmailConfig => "Missing required field: email_config".to_string(), } } } diff --git a/crates/router/src/core/recon.rs b/crates/router/src/core/recon.rs index 13cf4c488ec5..c2f289d4cc02 100644 --- a/crates/router/src/core/recon.rs +++ b/crates/router/src/core/recon.rs @@ -1,12 +1,14 @@ use api_models::recon as recon_api; #[cfg(feature = "email")] -use common_utils::ext_traits::AsyncExt; +use common_utils::{ext_traits::AsyncExt, types::theme::ThemeLineage}; use error_stack::ResultExt; #[cfg(feature = "email")] use masking::{ExposeInterface, PeekInterface, Secret}; #[cfg(feature = "email")] -use crate::{consts, services::email::types as email_types, types::domain}; +use crate::{ + consts, services::email::types as email_types, types::domain, utils::user::theme as theme_utils, +}; use crate::{ core::errors::{self, RouterResponse, UserErrors, UserResponse}, services::{api as service_api, authentication}, @@ -35,6 +37,21 @@ pub async fn send_recon_request( let user_in_db = &auth_data.user; let merchant_id = auth_data.merchant_account.get_id().clone(); + let theme = theme_utils::get_most_specific_theme_using_lineage( + &state.clone(), + ThemeLineage::Merchant { + tenant_id: state.tenant.tenant_id.clone(), + org_id: auth_data.merchant_account.get_org_id().clone(), + merchant_id: merchant_id.clone(), + }, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Failed to fetch theme for merchant_id = {:?}", + merchant_id + ))?; + let user_email = user_in_db.email.clone(); let email_contents = email_types::ProFeatureRequest { feature_name: consts::RECON_FEATURE_TAG.to_string(), @@ -55,6 +72,10 @@ pub async fn send_recon_request( consts::EMAIL_SUBJECT_DASHBOARD_FEATURE_REQUEST, user_email.expose().peek() ), + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; state .email_client @@ -141,7 +162,7 @@ pub async fn recon_merchant_account_update( let updated_merchant_account = db .update_merchant( key_manager_state, - auth.merchant_account, + auth.merchant_account.clone(), updated_merchant_account, &auth.key_store, ) @@ -154,6 +175,22 @@ pub async fn recon_merchant_account_update( #[cfg(feature = "email")] { let user_email = &req.user_email.clone(); + + let theme = theme_utils::get_most_specific_theme_using_lineage( + &state.clone(), + ThemeLineage::Merchant { + tenant_id: state.tenant.tenant_id, + org_id: auth.merchant_account.get_org_id().clone(), + merchant_id: merchant_id.clone(), + }, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable(format!( + "Failed to fetch theme for merchant_id = {:?}", + merchant_id + ))?; + let email_contents = email_types::ReconActivation { recipient_email: domain::UserEmail::from_pii_email(user_email.clone()) .change_context(errors::ApiErrorResponse::InternalServerError) @@ -164,6 +201,10 @@ pub async fn recon_merchant_account_update( .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to form username")?, subject: consts::EMAIL_SUBJECT_APPROVAL_RECON_REQUEST, + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; if req.recon_status == enums::ReconStatus::Active { let _ = state diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 629476b2591e..642473bdf8e0 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -42,7 +42,10 @@ use crate::{ routes::{app::ReqState, SessionState}, services::{authentication as auth, authorization::roles, openidconnect, ApplicationResponse}, types::{domain, transformers::ForeignInto}, - utils::{self, user::two_factor_auth as tfa_utils}, + utils::{ + self, + user::{theme as theme_utils, two_factor_auth as tfa_utils}, + }, }; pub mod dashboard_metadata; @@ -55,6 +58,7 @@ pub async fn signup_with_merchant_id( state: SessionState, request: user_api::SignUpWithMerchantIdRequest, auth_id: Option, + theme_id: Option, ) -> UserResponse { let new_user = domain::NewUser::try_from(request.clone())?; new_user @@ -75,12 +79,18 @@ pub async fn signup_with_merchant_id( ) .await?; + let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?; + let email_contents = email_types::ResetPassword { recipient_email: user_from_db.get_email().try_into()?, user_name: domain::UserName::new(user_from_db.get_name())?, settings: state.conf.clone(), subject: consts::user::EMAIL_SUBJECT_RESET_PASSWORD, auth_id, + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; let send_email_result = state @@ -112,6 +122,13 @@ pub async fn get_user_details( .await .change_context(UserErrors::InternalServerError)?; + let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity( + &state, + &user_from_token, + EntityType::Profile, + ) + .await?; + Ok(ApplicationResponse::Json( user_api::GetUserDetailsResponse { merchant_id: user_from_token.merchant_id, @@ -125,6 +142,7 @@ pub async fn get_user_details( recovery_codes_left: user.get_recovery_codes().map(|codes| codes.len()), profile_id: user_from_token.profile_id, entity_type: role_info.get_entity_type(), + theme_id: theme.map(|theme| theme.theme_id), }, )) } @@ -194,6 +212,7 @@ pub async fn connect_account( state: SessionState, request: user_api::ConnectAccountRequest, auth_id: Option, + theme_id: Option, ) -> UserResponse { let find_user = state .global_store @@ -203,12 +222,18 @@ pub async fn connect_account( if let Ok(found_user) = find_user { let user_from_db: domain::UserFromStorage = found_user.into(); + let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?; + let email_contents = email_types::MagicLink { recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, settings: state.conf.clone(), user_name: domain::UserName::new(user_from_db.get_name())?, subject: consts::user::EMAIL_SUBJECT_MAGIC_LINK, auth_id, + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; let send_email_result = state @@ -253,11 +278,17 @@ pub async fn connect_account( ) .await?; + let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?; + let magic_link_email = email_types::VerifyEmail { recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, settings: state.conf.clone(), subject: consts::user::EMAIL_SUBJECT_SIGNUP, auth_id, + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; let magic_link_result = state @@ -270,20 +301,22 @@ pub async fn connect_account( logger::info!(?magic_link_result); - let welcome_to_community_email = email_types::WelcomeToCommunity { - recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, - subject: consts::user::EMAIL_SUBJECT_WELCOME_TO_COMMUNITY, - }; + if state.tenant.tenant_id.get_string_repr() == common_utils::consts::DEFAULT_TENANT { + let welcome_to_community_email = email_types::WelcomeToCommunity { + recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, + subject: consts::user::EMAIL_SUBJECT_WELCOME_TO_COMMUNITY, + }; - let welcome_email_result = state - .email_client - .compose_and_send_email( - Box::new(welcome_to_community_email), - state.conf.proxy.https_url.as_ref(), - ) - .await; + let welcome_email_result = state + .email_client + .compose_and_send_email( + Box::new(welcome_to_community_email), + state.conf.proxy.https_url.as_ref(), + ) + .await; - logger::info!(?welcome_email_result); + logger::info!(?welcome_email_result); + } return Ok(ApplicationResponse::Json( user_api::ConnectAccountResponse { @@ -371,6 +404,7 @@ pub async fn forgot_password( state: SessionState, request: user_api::ForgotPasswordRequest, auth_id: Option, + theme_id: Option, ) -> UserResponse<()> { let user_email = domain::UserEmail::from_pii_email(request.email)?; @@ -387,12 +421,18 @@ pub async fn forgot_password( }) .map(domain::UserFromStorage::from)?; + let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?; + let email_contents = email_types::ResetPassword { recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?, settings: state.conf.clone(), user_name: domain::UserName::new(user_from_db.get_name())?, subject: consts::user::EMAIL_SUBJECT_RESET_PASSWORD, auth_id, + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; state @@ -782,6 +822,13 @@ async fn handle_existing_user_invitation( }, }; + let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity( + state, + user_from_token, + role_info.get_entity_type(), + ) + .await?; + let email_contents = email_types::InviteUser { recipient_email: invitee_email, user_name: domain::UserName::new(invitee_user_from_db.get_name())?, @@ -789,6 +836,10 @@ async fn handle_existing_user_invitation( subject: consts::user::EMAIL_SUBJECT_INVITATION, entity, auth_id: auth_id.clone(), + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; is_email_sent = state @@ -927,6 +978,13 @@ async fn handle_new_user_invitation( }, }; + let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity( + state, + user_from_token, + role_info.get_entity_type(), + ) + .await?; + let email_contents = email_types::InviteUser { recipient_email: invitee_email, user_name: domain::UserName::new(new_user.get_name())?, @@ -934,6 +992,10 @@ async fn handle_new_user_invitation( subject: consts::user::EMAIL_SUBJECT_INVITATION, entity, auth_id: auth_id.clone(), + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; let send_email_result = state .email_client @@ -1055,6 +1117,21 @@ pub async fn resend_invite( .get_entity_id_and_type() .ok_or(UserErrors::InternalServerError)?; + let invitee_role_info = roles::RoleInfo::from_role_id_and_org_id( + &state, + &user_role.role_id, + &user_from_token.org_id, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity( + &state, + &user_from_token, + invitee_role_info.get_entity_type(), + ) + .await?; + let email_contents = email_types::InviteUser { recipient_email: invitee_email, user_name: domain::UserName::new(user.get_name())?, @@ -1065,6 +1142,10 @@ pub async fn resend_invite( entity_type, }, auth_id: auth_id.clone(), + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; state @@ -1666,6 +1747,7 @@ pub async fn send_verification_mail( state: SessionState, req: user_api::SendVerifyEmailRequest, auth_id: Option, + theme_id: Option, ) -> UserResponse<()> { let user_email = domain::UserEmail::try_from(req.email)?; let user = state @@ -1684,11 +1766,17 @@ pub async fn send_verification_mail( return Err(UserErrors::UserAlreadyVerified.into()); } + let theme = theme_utils::get_theme_using_optional_theme_id(&state, theme_id).await?; + let email_contents = email_types::VerifyEmail { recipient_email: domain::UserEmail::from_pii_email(user.email)?, settings: state.conf.clone(), subject: consts::user::EMAIL_SUBJECT_SIGNUP, auth_id, + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; state diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs index a64b0f39dc03..88380363b4e3 100644 --- a/crates/router/src/core/user/dashboard_metadata.rs +++ b/crates/router/src/core/user/dashboard_metadata.rs @@ -1,4 +1,6 @@ use api_models::user::dashboard_metadata::{self as api, GetMultipleMetaDataPayload}; +#[cfg(feature = "email")] +use common_enums::EntityType; use diesel_models::{ enums::DashboardMetadata as DBEnum, user::dashboard_metadata::DashboardMetadata, }; @@ -8,8 +10,6 @@ use masking::ExposeInterface; #[cfg(feature = "email")] use router_env::logger; -#[cfg(feature = "email")] -use crate::services::email::types as email_types; use crate::{ core::errors::{UserErrors, UserResponse, UserResult}, routes::{app::ReqState, SessionState}, @@ -17,6 +17,8 @@ use crate::{ types::domain::{self, user::dashboard_metadata as types, MerchantKeyStore}, utils::user::dashboard_metadata as utils, }; +#[cfg(feature = "email")] +use crate::{services::email::types as email_types, utils::user::theme as theme_utils}; pub async fn set_metadata( state: SessionState, @@ -476,7 +478,21 @@ async fn insert_metadata( .expose(); if utils::is_prod_email_required(&data, user_email) { - let email_contents = email_types::BizEmailProd::new(state, data)?; + let theme = theme_utils::get_most_specific_theme_using_token_and_min_entity( + state, + &user, + EntityType::Merchant, + ) + .await?; + + let email_contents = email_types::BizEmailProd::new( + state, + data, + theme.as_ref().map(|theme| theme.theme_id.clone()), + theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), + )?; let send_email_result = state .email_client .compose_and_send_email( diff --git a/crates/router/src/core/user/theme.rs b/crates/router/src/core/user/theme.rs index dbf11256a502..27b342d90538 100644 --- a/crates/router/src/core/user/theme.rs +++ b/crates/router/src/core/user/theme.rs @@ -38,6 +38,7 @@ pub async fn get_theme_using_lineage( .change_context(UserErrors::InternalServerError)?; Ok(ApplicationResponse::Json(theme_api::GetThemeResponse { + email_config: theme.email_config(), theme_id: theme.theme_id, theme_name: theme.theme_name, entity_type: theme.entity_type, @@ -71,6 +72,7 @@ pub async fn get_theme_using_theme_id( .change_context(UserErrors::InternalServerError)?; Ok(ApplicationResponse::Json(theme_api::GetThemeResponse { + email_config: theme.email_config(), theme_id: theme.theme_id, theme_name: theme.theme_name, entity_type: theme.entity_type, @@ -113,10 +115,19 @@ pub async fn create_theme( ) -> UserResponse { theme_utils::validate_lineage(&state, &request.lineage).await?; + let email_config = if cfg!(feature = "email") { + request.email_config.ok_or(UserErrors::MissingEmailConfig)? + } else { + request + .email_config + .unwrap_or(state.conf.theme.email_config.clone()) + }; + let new_theme = ThemeNew::new( Uuid::new_v4().to_string(), request.theme_name, request.lineage, + email_config, ); let db_theme = state @@ -147,6 +158,7 @@ pub async fn create_theme( .change_context(UserErrors::InternalServerError)?; Ok(ApplicationResponse::Json(theme_api::GetThemeResponse { + email_config: db_theme.email_config(), theme_id: db_theme.theme_id, entity_type: db_theme.entity_type, tenant_id: db_theme.tenant_id, @@ -195,6 +207,7 @@ pub async fn update_theme( .change_context(UserErrors::InternalServerError)?; Ok(ApplicationResponse::Json(theme_api::GetThemeResponse { + email_config: db_theme.email_config(), theme_id: db_theme.theme_id, entity_type: db_theme.entity_type, tenant_id: db_theme.tenant_id, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 2fd30a3610b5..a7e71284aafd 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -3755,6 +3755,15 @@ impl ThemeInterface for KafkaStore { self.diesel_store.find_theme_by_theme_id(theme_id).await } + async fn find_most_specific_theme_in_lineage( + &self, + lineage: ThemeLineage, + ) -> CustomResult { + self.diesel_store + .find_most_specific_theme_in_lineage(lineage) + .await + } + async fn find_theme_by_lineage( &self, lineage: ThemeLineage, diff --git a/crates/router/src/db/user/theme.rs b/crates/router/src/db/user/theme.rs index 9b55a98d0ad3..51c0a52b3a01 100644 --- a/crates/router/src/db/user/theme.rs +++ b/crates/router/src/db/user/theme.rs @@ -21,6 +21,11 @@ pub trait ThemeInterface { theme_id: String, ) -> CustomResult; + async fn find_most_specific_theme_in_lineage( + &self, + lineage: ThemeLineage, + ) -> CustomResult; + async fn find_theme_by_lineage( &self, lineage: ThemeLineage, @@ -56,6 +61,16 @@ impl ThemeInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } + async fn find_most_specific_theme_in_lineage( + &self, + lineage: ThemeLineage, + ) -> CustomResult { + let conn = connection::pg_connection_read(self).await?; + storage::Theme::find_most_specific_theme_in_lineage(&conn, lineage) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } + async fn find_theme_by_lineage( &self, lineage: ThemeLineage, @@ -80,13 +95,12 @@ impl ThemeInterface for Store { fn check_theme_with_lineage(theme: &storage::Theme, lineage: &ThemeLineage) -> bool { match lineage { - // TODO: Add back Tenant variant when we introduce Tenant Variant in EntityType - // ThemeLineage::Tenant { tenant_id } => { - // &theme.tenant_id == tenant_id - // && theme.org_id.is_none() - // && theme.merchant_id.is_none() - // && theme.profile_id.is_none() - // } + ThemeLineage::Tenant { tenant_id } => { + &theme.tenant_id == tenant_id + && theme.org_id.is_none() + && theme.merchant_id.is_none() + && theme.profile_id.is_none() + } ThemeLineage::Organization { tenant_id, org_id } => { &theme.tenant_id == tenant_id && theme @@ -174,6 +188,11 @@ impl ThemeInterface for MockDb { last_modified_at: new_theme.last_modified_at, entity_type: new_theme.entity_type, theme_name: new_theme.theme_name, + email_primary_color: new_theme.email_primary_color, + email_foreground_color: new_theme.email_foreground_color, + email_background_color: new_theme.email_background_color, + email_entity_name: new_theme.email_entity_name, + email_entity_logo_url: new_theme.email_entity_logo_url, }; themes.push(theme.clone()); @@ -198,6 +217,27 @@ impl ThemeInterface for MockDb { ) } + async fn find_most_specific_theme_in_lineage( + &self, + lineage: ThemeLineage, + ) -> CustomResult { + let themes = self.themes.lock().await; + let lineages = lineage.get_same_and_higher_lineages(); + + themes + .iter() + .filter(|theme| { + lineages + .iter() + .any(|lineage| check_theme_with_lineage(theme, lineage)) + }) + .min_by_key(|theme| theme.entity_type) + .ok_or( + errors::StorageError::ValueNotFound("No theme found in lineage".to_string()).into(), + ) + .cloned() + } + async fn find_theme_by_lineage( &self, lineage: ThemeLineage, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index ca7c4847ad4a..5f04581fbb39 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -369,7 +369,7 @@ impl AppState { let email_client = Arc::new(create_email_client(&conf).await); let file_storage_client = conf.file_storage.get_file_storage_client().await; - let theme_storage_client = conf.theme_storage.get_file_storage_client().await; + let theme_storage_client = conf.theme.storage.get_file_storage_client().await; let grpc_client = conf.grpc_client.get_grpc_client_interface().await; diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index c32746c87185..d075048ddb6b 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -41,18 +41,23 @@ pub async fn user_signup_with_merchant_id( state: web::Data, http_req: HttpRequest, json_payload: web::Json, - query: web::Query, + query: web::Query, ) -> HttpResponse { let flow = Flow::UserSignUpWithMerchantId; let req_payload = json_payload.into_inner(); - let auth_id = query.into_inner().auth_id; + let query_params = query.into_inner(); Box::pin(api::server_wrap( flow.clone(), state, &http_req, req_payload.clone(), |state, _, req_body, _| { - user_core::signup_with_merchant_id(state, req_body, auth_id.clone()) + user_core::signup_with_merchant_id( + state, + req_body, + query_params.auth_id.clone(), + query_params.theme_id.clone(), + ) }, &auth::AdminApiAuth, api_locking::LockAction::NotApplicable, @@ -107,17 +112,24 @@ pub async fn user_connect_account( state: web::Data, http_req: HttpRequest, json_payload: web::Json, - query: web::Query, + query: web::Query, ) -> HttpResponse { let flow = Flow::UserConnectAccount; let req_payload = json_payload.into_inner(); - let auth_id = query.into_inner().auth_id; + let query_params = query.into_inner(); Box::pin(api::server_wrap( flow.clone(), state, &http_req, req_payload.clone(), - |state, _: (), req_body, _| user_core::connect_account(state, req_body, auth_id.clone()), + |state, _: (), req_body, _| { + user_core::connect_account( + state, + req_body, + query_params.auth_id.clone(), + query_params.theme_id.clone(), + ) + }, &auth::NoAuth, api_locking::LockAction::NotApplicable, )) @@ -381,16 +393,23 @@ pub async fn forgot_password( state: web::Data, req: HttpRequest, payload: web::Json, - query: web::Query, + query: web::Query, ) -> HttpResponse { let flow = Flow::ForgotPassword; - let auth_id = query.into_inner().auth_id; + let query_params = query.into_inner(); Box::pin(api::server_wrap( flow, state.clone(), &req, payload.into_inner(), - |state, _: (), payload, _| user_core::forgot_password(state, payload, auth_id.clone()), + |state, _: (), payload, _| { + user_core::forgot_password( + state, + payload, + query_params.auth_id.clone(), + query_params.theme_id.clone(), + ) + }, &auth::NoAuth, api_locking::LockAction::NotApplicable, )) @@ -420,7 +439,7 @@ pub async fn invite_multiple_user( state: web::Data, req: HttpRequest, payload: web::Json>, - auth_id_query_param: web::Query, + auth_id_query_param: web::Query, ) -> HttpResponse { let flow = Flow::InviteMultipleUser; let auth_id = auth_id_query_param.into_inner().auth_id; @@ -445,7 +464,7 @@ pub async fn resend_invite( state: web::Data, req: HttpRequest, payload: web::Json, - query: web::Query, + query: web::Query, ) -> HttpResponse { let flow = Flow::ReInviteUser; let auth_id = query.into_inner().auth_id; @@ -512,17 +531,22 @@ pub async fn verify_email_request( state: web::Data, http_req: HttpRequest, json_payload: web::Json, - query: web::Query, + query: web::Query, ) -> HttpResponse { let flow = Flow::VerifyEmailRequest; - let auth_id = query.into_inner().auth_id; + let query_params = query.into_inner(); Box::pin(api::server_wrap( flow, state.clone(), &http_req, json_payload.into_inner(), |state, _: (), req_body, _| { - user_core::send_verification_mail(state, req_body, auth_id.clone()) + user_core::send_verification_mail( + state, + req_body, + query_params.auth_id.clone(), + query_params.theme_id.clone(), + ) }, &auth::NoAuth, api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs index 66e730f08246..128250a61aa8 100644 --- a/crates/router/src/services/email/types.rs +++ b/crates/router/src/services/email/types.rs @@ -1,6 +1,6 @@ use api_models::user::dashboard_metadata::ProdIntent; use common_enums::EntityType; -use common_utils::{errors::CustomResult, pii}; +use common_utils::{errors::CustomResult, pii, types::theme::EmailThemeConfig}; use error_stack::ResultExt; use external_services::email::{EmailContents, EmailData, EmailError}; use masking::{ExposeInterface, Secret}; @@ -212,13 +212,17 @@ pub fn get_link_with_token( token: impl std::fmt::Display, action: impl std::fmt::Display, auth_id: &Option, + theme_id: &Option, ) -> String { - let email_url = format!("{base_url}/user/{action}?token={token}"); + let mut email_url = format!("{base_url}/user/{action}?token={token}"); if let Some(auth_id) = auth_id { - format!("{email_url}&auth_id={auth_id}") - } else { - email_url + email_url = format!("{email_url}&auth_id={auth_id}"); } + if let Some(theme_id) = theme_id { + email_url = format!("{email_url}&theme_id={theme_id}"); + } + + email_url } pub struct VerifyEmail { @@ -226,6 +230,8 @@ pub struct VerifyEmail { pub settings: std::sync::Arc, pub subject: &'static str, pub auth_id: Option, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } /// Currently only HTML is supported @@ -246,6 +252,7 @@ impl EmailData for VerifyEmail { token, "verify_email", &self.auth_id, + &self.theme_id, ); let body = html::get_html_body(EmailBody::Verify { @@ -266,6 +273,8 @@ pub struct ResetPassword { pub settings: std::sync::Arc, pub subject: &'static str, pub auth_id: Option, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } #[async_trait::async_trait] @@ -285,6 +294,7 @@ impl EmailData for ResetPassword { token, "set_password", &self.auth_id, + &self.theme_id, ); let body = html::get_html_body(EmailBody::Reset { @@ -306,6 +316,8 @@ pub struct MagicLink { pub settings: std::sync::Arc, pub subject: &'static str, pub auth_id: Option, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } #[async_trait::async_trait] @@ -325,6 +337,7 @@ impl EmailData for MagicLink { token, "verify_email", &self.auth_id, + &self.theme_id, ); let body = html::get_html_body(EmailBody::MagicLink { @@ -347,6 +360,8 @@ pub struct InviteUser { pub subject: &'static str, pub entity: Entity, pub auth_id: Option, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } #[async_trait::async_trait] @@ -366,6 +381,7 @@ impl EmailData for InviteUser { token, "accept_invite_from_email", &self.auth_id, + &self.theme_id, ); let body = html::get_html_body(EmailBody::AcceptInviteFromEmail { link: invite_user_link, @@ -384,6 +400,8 @@ pub struct ReconActivation { pub recipient_email: domain::UserEmail, pub user_name: domain::UserName, pub subject: &'static str, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } #[async_trait::async_trait] @@ -410,10 +428,17 @@ pub struct BizEmailProd { pub business_website: String, pub settings: std::sync::Arc, pub subject: &'static str, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } impl BizEmailProd { - pub fn new(state: &SessionState, data: ProdIntent) -> UserResult { + pub fn new( + state: &SessionState, + data: ProdIntent, + theme_id: Option, + theme_config: EmailThemeConfig, + ) -> UserResult { Ok(Self { recipient_email: domain::UserEmail::from_pii_email( state.conf.email.prod_intent_recipient_email.clone(), @@ -428,6 +453,8 @@ impl BizEmailProd { .unwrap_or(common_enums::CountryAlpha2::AD) .to_string(), business_website: data.business_website.unwrap_or_default(), + theme_id, + theme_config, }) } } @@ -458,6 +485,8 @@ pub struct ProFeatureRequest { pub user_name: domain::UserName, pub user_email: domain::UserEmail, pub subject: String, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } #[async_trait::async_trait] @@ -486,6 +515,8 @@ pub struct ApiKeyExpiryReminder { pub expires_in: u8, pub api_key_name: String, pub prefix: String, + pub theme_id: Option, + pub theme_config: EmailThemeConfig, } #[async_trait::async_trait] diff --git a/crates/router/src/utils/user/theme.rs b/crates/router/src/utils/user/theme.rs index 13452380d9d9..1c8b76989ae8 100644 --- a/crates/router/src/utils/user/theme.rs +++ b/crates/router/src/utils/user/theme.rs @@ -1,12 +1,15 @@ use std::path::PathBuf; -use common_utils::{id_type, types::theme::ThemeLineage}; +use common_enums::EntityType; +use common_utils::{ext_traits::AsyncExt, id_type, types::theme::ThemeLineage}; +use diesel_models::user::theme::Theme; use error_stack::ResultExt; use hyperswitch_domain_models::merchant_key_store::MerchantKeyStore; use crate::{ core::errors::{StorageErrorExt, UserErrors, UserResult}, routes::SessionState, + services::authentication::UserFromToken, }; fn get_theme_dir_key(theme_id: &str) -> PathBuf { @@ -54,6 +57,10 @@ pub async fn upload_file_to_theme_bucket( pub async fn validate_lineage(state: &SessionState, lineage: &ThemeLineage) -> UserResult<()> { match lineage { + ThemeLineage::Tenant { tenant_id } => { + validate_tenant(state, tenant_id)?; + Ok(()) + } ThemeLineage::Organization { tenant_id, org_id } => { validate_tenant(state, tenant_id)?; validate_org(state, org_id).await?; @@ -96,8 +103,8 @@ async fn validate_org(state: &SessionState, org_id: &id_type::OrganizationId) -> .store .find_organization_by_org_id(org_id) .await - .to_not_found_response(UserErrors::InvalidThemeLineage("org_id".to_string()))?; - Ok(()) + .to_not_found_response(UserErrors::InvalidThemeLineage("org_id".to_string())) + .map(|_| ()) } async fn validate_merchant_and_get_key_store( @@ -153,6 +160,67 @@ async fn validate_profile( profile_id, ) .await - .to_not_found_response(UserErrors::InvalidThemeLineage("profile_id".to_string()))?; - Ok(()) + .to_not_found_response(UserErrors::InvalidThemeLineage("profile_id".to_string())) + .map(|_| ()) +} + +pub async fn get_most_specific_theme_using_token_and_min_entity( + state: &SessionState, + user_from_token: &UserFromToken, + min_entity: EntityType, +) -> UserResult> { + get_most_specific_theme_using_lineage( + state, + ThemeLineage::new( + min_entity, + user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), + user_from_token.org_id.clone(), + user_from_token.merchant_id.clone(), + user_from_token.profile_id.clone(), + ), + ) + .await +} + +pub async fn get_most_specific_theme_using_lineage( + state: &SessionState, + lineage: ThemeLineage, +) -> UserResult> { + match state + .global_store + .find_most_specific_theme_in_lineage(lineage) + .await + { + Ok(theme) => Ok(Some(theme)), + Err(e) => { + if e.current_context().is_db_not_found() { + Ok(None) + } else { + Err(e.change_context(UserErrors::InternalServerError)) + } + } + } +} + +pub async fn get_theme_using_optional_theme_id( + state: &SessionState, + theme_id: Option, +) -> UserResult> { + match theme_id + .async_map(|theme_id| state.global_store.find_theme_by_theme_id(theme_id)) + .await + .transpose() + { + Ok(theme) => Ok(theme), + Err(e) => { + if e.current_context().is_db_not_found() { + Ok(None) + } else { + Err(e.change_context(UserErrors::InternalServerError)) + } + } + } } diff --git a/crates/router/src/workflows/api_key_expiry.rs b/crates/router/src/workflows/api_key_expiry.rs index bf6100179bf2..cdc4959c4562 100644 --- a/crates/router/src/workflows/api_key_expiry.rs +++ b/crates/router/src/workflows/api_key_expiry.rs @@ -1,4 +1,4 @@ -use common_utils::{errors::ValidationError, ext_traits::ValueExt}; +use common_utils::{errors::ValidationError, ext_traits::ValueExt, types::theme::ThemeLineage}; use diesel_models::{ enums as storage_enums, process_tracker::business_status, ApiKeyExpiryTrackingData, }; @@ -11,7 +11,7 @@ use crate::{ routes::{metrics, SessionState}, services::email::types::ApiKeyExpiryReminder, types::{api, domain::UserEmail, storage}, - utils::OptionExt, + utils::{user::theme as theme_utils, OptionExt}, }; pub struct ApiKeyExpiryWorkflow; @@ -48,6 +48,7 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { let email_id = merchant_account .merchant_details + .clone() .parse_value::("MerchantDetails")? .primary_email .ok_or(errors::ProcessTrackerError::EValidationError( @@ -73,6 +74,20 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { ) .ok_or(errors::ProcessTrackerError::EApiErrorResponse)?; + let theme = theme_utils::get_most_specific_theme_using_lineage( + state, + ThemeLineage::Merchant { + tenant_id: state.tenant.tenant_id.clone(), + org_id: merchant_account.get_org_id().clone(), + merchant_id: merchant_account.get_id().clone(), + }, + ) + .await + .map_err(|err| { + logger::error!(?err, "Failed to get theme"); + errors::ProcessTrackerError::EApiErrorResponse + })?; + let email_contents = ApiKeyExpiryReminder { recipient_email: UserEmail::from_pii_email(email_id).map_err(|error| { logger::error!( @@ -85,6 +100,10 @@ impl ProcessTrackerWorkflow for ApiKeyExpiryWorkflow { expires_in: *expires_in, api_key_name, prefix, + theme_id: theme.as_ref().map(|theme| theme.theme_id.clone()), + theme_config: theme + .map(|theme| theme.email_config()) + .unwrap_or(state.conf.theme.email_config.clone()), }; state diff --git a/migrations/2024-12-05-131123_add-email-theme-data-in-themes/down.sql b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/down.sql new file mode 100644 index 000000000000..34732b5dd695 --- /dev/null +++ b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE themes DROP COLUMN IF EXISTS email_primary_color; +ALTER TABLE themes DROP COLUMN IF EXISTS email_foreground_color; +ALTER TABLE themes DROP COLUMN IF EXISTS email_background_color; +ALTER TABLE themes DROP COLUMN IF EXISTS email_entity_name; +ALTER TABLE themes DROP COLUMN IF EXISTS email_entity_logo_url; diff --git a/migrations/2024-12-05-131123_add-email-theme-data-in-themes/up.sql b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/up.sql new file mode 100644 index 000000000000..1004302ab7f6 --- /dev/null +++ b/migrations/2024-12-05-131123_add-email-theme-data-in-themes/up.sql @@ -0,0 +1,6 @@ +-- Your SQL goes here +ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_primary_color VARCHAR(64) NOT NULL DEFAULT '#006DF9'; +ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_foreground_color VARCHAR(64) NOT NULL DEFAULT '#000000'; +ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_background_color VARCHAR(64) NOT NULL DEFAULT '#FFFFFF'; +ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_entity_name VARCHAR(64) NOT NULL DEFAULT 'Hyperswitch'; +ALTER TABLE themes ADD COLUMN IF NOT EXISTS email_entity_logo_url TEXT NOT NULL DEFAULT 'https://app.hyperswitch.io/email-assets/HyperswitchLogo.png'; From 71574a85e6aba6bc614e1d7f6775dcef4b481201 Mon Sep 17 00:00:00 2001 From: Kartikeya Hegde Date: Mon, 16 Dec 2024 14:23:56 +0530 Subject: [PATCH 02/20] chore(deps): update scylla driver (#6799) --- Cargo.lock | 17 ++++++++--------- crates/masking/Cargo.toml | 2 +- crates/masking/src/cassandra.rs | 22 ++++++---------------- 3 files changed, 15 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 581480f43553..9c020d9d4d9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7090,9 +7090,8 @@ dependencies = [ [[package]] name = "scylla" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8139623d3fb0c8205b15e84fa587f3aa0ba61f876c19a9157b688f7c1763a7c5" +version = "0.15.0" +source = "git+https://github.com/juspay/scylla-rust-driver.git?rev=5700aa2847b25437cdd4fcf34d707aa90dca8b89#5700aa2847b25437cdd4fcf34d707aa90dca8b89" dependencies = [ "arc-swap", "async-trait", @@ -7121,9 +7120,8 @@ dependencies = [ [[package]] name = "scylla-cql" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de7020bcd1f6fdbeaed356cd426bf294b2071bd7120d48d2e8e319295e2acdcd" +version = "0.4.0" +source = "git+https://github.com/juspay/scylla-rust-driver.git?rev=5700aa2847b25437cdd4fcf34d707aa90dca8b89#5700aa2847b25437cdd4fcf34d707aa90dca8b89" dependencies = [ "async-trait", "byteorder", @@ -7131,16 +7129,17 @@ dependencies = [ "lz4_flex", "scylla-macros", "snap", + "stable_deref_trait", "thiserror", "tokio 1.40.0", "uuid", + "yoke", ] [[package]] name = "scylla-macros" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3859b6938663fc5062e3b26f3611649c9bd26fb252e85f6fdfa581e0d2ce74b6" +version = "0.7.0" +source = "git+https://github.com/juspay/scylla-rust-driver.git?rev=5700aa2847b25437cdd4fcf34d707aa90dca8b89#5700aa2847b25437cdd4fcf34d707aa90dca8b89" dependencies = [ "darling 0.20.10", "proc-macro2", diff --git a/crates/masking/Cargo.toml b/crates/masking/Cargo.toml index 425c1ca36ec4..003007989760 100644 --- a/crates/masking/Cargo.toml +++ b/crates/masking/Cargo.toml @@ -28,7 +28,7 @@ subtle = "2.5.0" time = { version = "0.3.35", optional = true, features = ["serde-human-readable"] } url = { version = "2.5.0", features = ["serde"] } zeroize = { version = "1.7", default-features = false } -scylla = { version = "0.14.0", optional = true} +scylla = { git = "https://github.com/juspay/scylla-rust-driver.git",rev = "5700aa2847b25437cdd4fcf34d707aa90dca8b89", optional = true} [dev-dependencies] serde_json = "1.0.115" diff --git a/crates/masking/src/cassandra.rs b/crates/masking/src/cassandra.rs index dc81512e79d5..2544ab18a7aa 100644 --- a/crates/masking/src/cassandra.rs +++ b/crates/masking/src/cassandra.rs @@ -1,7 +1,6 @@ use scylla::{ - cql_to_rust::FromCqlVal, deserialize::DeserializeValue, - frame::response::result::{ColumnType, CqlValue}, + frame::response::result::ColumnType, serialize::{ value::SerializeValue, writers::{CellWriter, WrittenCellProof}, @@ -17,34 +16,25 @@ where { fn serialize<'b>( &self, - typ: &ColumnType, + typ: &ColumnType<'_>, writer: CellWriter<'b>, ) -> Result, SerializationError> { self.peek().serialize(typ, writer) } } -impl<'frame, T> DeserializeValue<'frame> for StrongSecret +impl<'frame, 'metadata, T> DeserializeValue<'frame, 'metadata> for StrongSecret where - T: DeserializeValue<'frame> + zeroize::Zeroize + Clone, + T: DeserializeValue<'frame, 'metadata> + zeroize::Zeroize + Clone, { - fn type_check(typ: &ColumnType) -> Result<(), scylla::deserialize::TypeCheckError> { + fn type_check(typ: &ColumnType<'_>) -> Result<(), scylla::deserialize::TypeCheckError> { T::type_check(typ) } fn deserialize( - typ: &'frame ColumnType, + typ: &'metadata ColumnType<'metadata>, v: Option>, ) -> Result { Ok(Self::new(T::deserialize(typ, v)?)) } } - -impl FromCqlVal for StrongSecret -where - T: FromCqlVal + zeroize::Zeroize + Clone, -{ - fn from_cql(cql_val: CqlValue) -> Result { - Ok(Self::new(T::from_cql(cql_val)?)) - } -} From 6081283afc5ab5a6503c8f0f81181cd323b12297 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:24:54 +0530 Subject: [PATCH 03/20] refactor(authz): Make connector list accessible by operation groups (#6792) --- crates/router/src/routes/admin.rs | 5 ++++- .../src/services/authorization/permission_groups.rs | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 80d22ad22bf6..a57fd56e6c84 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -431,7 +431,10 @@ pub async fn connector_retrieve( &auth::AdminApiAuthWithMerchantIdFromHeader, &auth::JWTAuthMerchantFromRoute { merchant_id, - required_permission: Permission::ProfileConnectorRead, + // This should ideally be ProfileConnectorRead, but since this API responds with + // sensitive data, keeping this as ProfileConnectorWrite + // TODO: Convert this to ProfileConnectorRead once data is masked. + required_permission: Permission::ProfileConnectorWrite, }, req.headers(), ), diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs index 14eda547e883..ceb943950d52 100644 --- a/crates/router/src/services/authorization/permission_groups.rs +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -61,8 +61,12 @@ impl PermissionGroupExt for PermissionGroup { fn accessible_groups(&self) -> Vec { match self { - Self::OperationsView => vec![Self::OperationsView], - Self::OperationsManage => vec![Self::OperationsView, Self::OperationsManage], + Self::OperationsView => vec![Self::OperationsView, Self::ConnectorsView], + Self::OperationsManage => vec![ + Self::OperationsView, + Self::OperationsManage, + Self::ConnectorsView, + ], Self::ConnectorsView => vec![Self::ConnectorsView], Self::ConnectorsManage => vec![Self::ConnectorsView, Self::ConnectorsManage], From ed276ecc0017f7f98b6f8fa3841e6b8971f609f1 Mon Sep 17 00:00:00 2001 From: DEEPANSHU BANSAL <41580413+deepanshu-iiitu@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:25:43 +0530 Subject: [PATCH 04/20] feat(connector): [AIRWALLEX] Add refferer data to whitelist hyperswitch (#6806) --- .../src/connectors/airwallex/transformers.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs index 97c9ed4d36a4..e8ab479c978c 100644 --- a/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/airwallex/transformers.rs @@ -43,6 +43,14 @@ impl TryFrom<&ConnectorAuthType> for AirwallexAuthType { } } } + +#[derive(Default, Debug, Serialize, Eq, PartialEq)] +pub struct ReferrerData { + #[serde(rename = "type")] + r_type: String, + version: String, +} + #[derive(Default, Debug, Serialize, Eq, PartialEq)] pub struct AirwallexIntentRequest { // Unique ID to be sent for each transaction/operation request to the connector @@ -51,10 +59,16 @@ pub struct AirwallexIntentRequest { currency: enums::Currency, //ID created in merchant's order system that corresponds to this PaymentIntent. merchant_order_id: String, + // This data is required to whitelist Hyperswitch at Airwallex. + referrer_data: ReferrerData, } impl TryFrom<&types::PaymentsPreProcessingRouterData> for AirwallexIntentRequest { type Error = error_stack::Report; fn try_from(item: &types::PaymentsPreProcessingRouterData) -> Result { + let referrer_data = ReferrerData { + r_type: "hyperswitch".to_string(), + version: "1.0.0".to_string(), + }; // amount and currency will always be Some since PaymentsPreProcessingData is constructed using PaymentsAuthorizeData let amount = item .request @@ -73,6 +87,7 @@ impl TryFrom<&types::PaymentsPreProcessingRouterData> for AirwallexIntentRequest amount: utils::to_currency_base_unit(amount, currency)?, currency, merchant_order_id: item.connector_request_reference_id.clone(), + referrer_data, }) } } From 986de77b4868e48d00161c9d30071d809360e9a6 Mon Sep 17 00:00:00 2001 From: Riddhiagrawal001 <50551695+Riddhiagrawal001@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:17:59 +0530 Subject: [PATCH 05/20] fix(user_roles): migrations for backfilling user_roles entity_id (#6837) --- .../down.sql | 2 +- .../up.sql | 1 + .../down.sql | 2 ++ .../up.sql | 10 ++++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/down.sql create mode 100644 migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/up.sql diff --git a/migrations/2024-12-02-110129_update-user-role-entity-type/down.sql b/migrations/2024-12-02-110129_update-user-role-entity-type/down.sql index c7c9cbeb4017..dff040ebc9e6 100644 --- a/migrations/2024-12-02-110129_update-user-role-entity-type/down.sql +++ b/migrations/2024-12-02-110129_update-user-role-entity-type/down.sql @@ -1,2 +1,2 @@ -- This file should undo anything in `up.sql` -SELECT 1; \ No newline at end of file +UPDATE user_roles SET entity_type = NULL WHERE version = 'v1'; \ No newline at end of file diff --git a/migrations/2024-12-02-110129_update-user-role-entity-type/up.sql b/migrations/2024-12-02-110129_update-user-role-entity-type/up.sql index f2759f030d50..b982a8b13730 100644 --- a/migrations/2024-12-02-110129_update-user-role-entity-type/up.sql +++ b/migrations/2024-12-02-110129_update-user-role-entity-type/up.sql @@ -1,4 +1,5 @@ -- Your SQL goes here +-- Incomplete migration, also run migrations/2024-12-13-080558_entity-id-backfill-for-user-roles UPDATE user_roles SET entity_type = CASE diff --git a/migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/down.sql b/migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/down.sql new file mode 100644 index 000000000000..fb372f88a12b --- /dev/null +++ b/migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +UPDATE user_roles SET entity_id = NULL WHERE version = 'v1'; \ No newline at end of file diff --git a/migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/up.sql b/migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/up.sql new file mode 100644 index 000000000000..7de41d880db6 --- /dev/null +++ b/migrations/2024-12-13-080558_entity-id-backfill-for-user-roles/up.sql @@ -0,0 +1,10 @@ +-- Your SQL goes here +UPDATE user_roles +SET + entity_id = CASE + WHEN role_id = 'org_admin' THEN org_id + ELSE merchant_id + END +WHERE + version = 'v1' + AND entity_id IS NULL; \ No newline at end of file From ae00a103de5bd283695969270a421c7609a699e8 Mon Sep 17 00:00:00 2001 From: Rutam Prita Mishra Date: Mon, 16 Dec 2024 16:49:21 +0530 Subject: [PATCH 06/20] feat(payments): Add audit events for PaymentStatus update (#6520) --- .../core/payments/operations/payment_status.rs | 17 +++++++++++++++-- crates/router/src/events/audit_events.rs | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 69f2d85d6a91..9aa905345c8c 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -16,6 +16,7 @@ use crate::{ PaymentData, }, }, + events::audit_events::{AuditEvent, AuditEventType}, routes::{app::ReqState, SessionState}, services, types::{ @@ -141,7 +142,7 @@ impl UpdateTracker, api::PaymentsRequest> for async fn update_trackers<'b>( &'b self, _state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, @@ -156,6 +157,12 @@ impl UpdateTracker, api::PaymentsRequest> for where F: 'b + Send, { + req_state + .event_context + .event(AuditEvent::new(AuditEventType::PaymentStatus)) + .with(payment_data.to_event()) + .emit(); + Ok((Box::new(self), payment_data)) } } @@ -167,7 +174,7 @@ impl UpdateTracker, api::PaymentsRetrieveRequ async fn update_trackers<'b>( &'b self, _state: &'b SessionState, - _req_state: ReqState, + req_state: ReqState, payment_data: PaymentData, _customer: Option, _storage_scheme: enums::MerchantStorageScheme, @@ -182,6 +189,12 @@ impl UpdateTracker, api::PaymentsRetrieveRequ where F: 'b + Send, { + req_state + .event_context + .event(AuditEvent::new(AuditEventType::PaymentStatus)) + .with(payment_data.to_event()) + .emit(); + Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/events/audit_events.rs b/crates/router/src/events/audit_events.rs index c314fa8409fa..a0f651b93c39 100644 --- a/crates/router/src/events/audit_events.rs +++ b/crates/router/src/events/audit_events.rs @@ -33,6 +33,7 @@ pub enum AuditEventType { }, PaymentApprove, PaymentCreate, + PaymentStatus, PaymentCompleteAuthorize, PaymentReject { error_code: Option, @@ -79,6 +80,7 @@ impl Event for AuditEvent { AuditEventType::PaymentUpdate { .. } => "payment_update", AuditEventType::PaymentApprove { .. } => "payment_approve", AuditEventType::PaymentCreate { .. } => "payment_create", + AuditEventType::PaymentStatus { .. } => "payment_status", AuditEventType::PaymentCompleteAuthorize => "payment_complete_authorize", AuditEventType::PaymentReject { .. } => "payment_rejected", }; From f95ee51bb3b879762d493953b4b6e7c2e0359946 Mon Sep 17 00:00:00 2001 From: Sakil Mostak <73734619+Sakilmostak@users.noreply.github.com> Date: Mon, 16 Dec 2024 17:26:51 +0530 Subject: [PATCH 07/20] fix(router): handle default case for card_network for co-badged cards (#6825) --- crates/router/src/core/payments/helpers.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index fa5f5e03e478..ba14457e023c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4433,7 +4433,7 @@ pub async fn get_additional_payment_data( api_models::payments::AdditionalPaymentData::Card(Box::new( api_models::payments::AdditionalCardInfo { card_issuer: card_info.card_issuer, - card_network: card_network.or(card_info.card_network), + card_network: card_network.clone().or(card_info.card_network), bank_code: card_info.bank_code, card_type: card_info.card_type, card_issuing_country: card_info.card_issuing_country, @@ -4453,7 +4453,7 @@ pub async fn get_additional_payment_data( api_models::payments::AdditionalPaymentData::Card(Box::new( api_models::payments::AdditionalCardInfo { card_issuer: None, - card_network: None, + card_network, bank_code: None, card_type: None, card_issuing_country: None, @@ -4696,7 +4696,7 @@ pub async fn get_additional_payment_data( api_models::payments::AdditionalPaymentData::Card(Box::new( api_models::payments::AdditionalCardInfo { card_issuer: card_info.card_issuer, - card_network: card_network.or(card_info.card_network), + card_network: card_network.clone().or(card_info.card_network), bank_code: card_info.bank_code, card_type: card_info.card_type, card_issuing_country: card_info.card_issuing_country, @@ -4716,7 +4716,7 @@ pub async fn get_additional_payment_data( api_models::payments::AdditionalPaymentData::Card(Box::new( api_models::payments::AdditionalCardInfo { card_issuer: None, - card_network: None, + card_network, bank_code: None, card_type: None, card_issuing_country: None, From c22be0c9274350a531cd74b64eb6b311579dca79 Mon Sep 17 00:00:00 2001 From: Debarati Ghatak <88573135+cookieg13@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:01:20 +0530 Subject: [PATCH 08/20] feat(connector): [Adyen ] Add fixes for AdyenPaymentRequest struct (#6803) --- .../src/connector/adyen/transformers.rs | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 16cab2fda470..7aaf9b5e2bb2 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -89,6 +89,7 @@ pub enum AuthType { #[default] PreAuth, } +#[serde_with::skip_serializing_none] #[derive(Clone, Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdditionalData { @@ -107,6 +108,7 @@ pub struct AdditionalData { funds_availability: Option, } +#[serde_with::skip_serializing_none] #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ShopperName { @@ -114,6 +116,7 @@ pub struct ShopperName { last_name: Option>, } +#[serde_with::skip_serializing_none] #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Address { @@ -122,9 +125,10 @@ pub struct Address { house_number_or_name: Secret, postal_code: Secret, state_or_province: Option>, - street: Secret, + street: Option>, } +#[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct LineItem { @@ -143,7 +147,6 @@ pub struct AdyenPaymentRequest<'a> { amount: Amount, merchant_account: Secret, payment_method: AdyenPaymentMethod<'a>, - #[serde(skip_serializing_if = "Option::is_none")] mpi_data: Option, reference: String, return_url: String, @@ -170,13 +173,13 @@ pub struct AdyenPaymentRequest<'a> { merchant_order_reference: Option, } +#[serde_with::skip_serializing_none] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct AdyenMpiData { directory_response: String, authentication_response: String, token_authentication_verification_value: Secret, - #[serde(skip_serializing_if = "Option::is_none")] eci: Option, } @@ -500,7 +503,7 @@ pub struct Amount { } #[derive(Debug, Clone, Serialize)] -#[serde(tag = "type")] +#[serde(untagged)] #[serde(rename_all = "lowercase")] pub enum AdyenPaymentMethod<'a> { AdyenAffirm(Box), @@ -1088,6 +1091,7 @@ pub struct BlikRedirectionData { blik_code: Secret, } +#[serde_with::skip_serializing_none] #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct BankRedirectionWithIssuer<'a> { @@ -1104,6 +1108,7 @@ pub struct AdyenMandate { stored_payment_method_id: Secret, } +#[serde_with::skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenCard { @@ -1211,6 +1216,7 @@ pub struct AdyenApplePay { apple_pay_token: Secret, } +#[serde_with::skip_serializing_none] #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DokuBankData { @@ -1219,6 +1225,7 @@ pub struct DokuBankData { shopper_email: Email, } // Refunds Request and Response +#[serde_with::skip_serializing_none] #[derive(Default, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct AdyenRefundRequest { @@ -1726,16 +1733,22 @@ fn get_additional_data(item: &types::PaymentsAuthorizeRouterData) -> Option additionalData: {} + //returning None, ensures that additionalData key will not be present in the serialized JSON + None + } else { + Some(AdditionalData { + authorisation_type, + manual_capture, + execute_three_d, + network_tx_reference: None, + recurring_detail_reference: None, + recurring_shopper_reference: None, + recurring_processing_model: None, + ..AdditionalData::default() + }) + } } fn get_channel_type(pm_type: Option) -> Option { @@ -1766,7 +1779,7 @@ pub fn get_address_info( house_number_or_name: a.get_line1()?.to_owned(), postal_code: a.get_zip()?.to_owned(), state_or_province: a.state.clone(), - street: a.get_line2()?.to_owned(), + street: a.get_optional_line2().to_owned(), }) }, ) From 165ead61084a48f268829c281e932b278f0a6730 Mon Sep 17 00:00:00 2001 From: Sahkal Poddar Date: Mon, 16 Dec 2024 19:15:32 +0530 Subject: [PATCH 09/20] feat(core): Add click to pay support in hyperswitch (#6769) Co-authored-by: sai-harsha-vardhan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> --- .typos.toml | 1 + api-reference-v2/openapi_spec.json | 5 +- api-reference/openapi_spec.json | 5 +- crates/api_models/src/connector_enums.rs | 2 + crates/common_enums/src/enums.rs | 7 +- crates/connector_configs/src/connector.rs | 7 + .../hyperswitch_connectors/src/connectors.rs | 9 +- .../src/connectors/ctp_mastercard.rs | 124 +++++ .../unified_authentication_service.rs | 389 ++++----------- .../transformers.rs | 451 ++++++++++++------ .../hyperswitch_connectors/src/constants.rs | 1 + .../src/default_implementations.rs | 241 ++++++++-- .../src/default_implementations_v2.rs | 22 + .../src/payment_method_data.rs | 1 + .../src/router_flow_types.rs | 2 + .../unified_authentication_service.rs | 5 + .../unified_authentication_service.rs | 2 +- crates/hyperswitch_domain_models/src/types.rs | 13 +- crates/hyperswitch_interfaces/src/api.rs | 64 ++- crates/hyperswitch_interfaces/src/configs.rs | 13 +- crates/hyperswitch_interfaces/src/types.rs | 19 + crates/router/src/connector.rs | 19 +- .../src/connector/adyen/transformers.rs | 216 +++++++-- crates/router/src/core/admin.rs | 1 + crates/router/src/core/payment_methods.rs | 2 +- .../payment_methods/network_tokenization.rs | 1 + crates/router/src/core/payments.rs | 11 + .../connector_integration_v2_impls.rs | 85 +--- crates/router/src/core/payments/flows.rs | 143 +----- crates/router/src/core/payments/helpers.rs | 13 +- .../operations/payment_complete_authorize.rs | 1 + .../payments/operations/payment_confirm.rs | 57 ++- .../payments/operations/payment_create.rs | 1 + .../payments/operations/payment_update.rs | 1 + .../router/src/core/payments/transformers.rs | 1 + .../core/unified_authentication_service.rs | 52 +- .../transformers.rs | 15 +- .../unified_authentication_service/types.rs | 2 +- crates/router/src/types/api.rs | 9 +- crates/router/src/types/api/authentication.rs | 6 + .../api/unified_authentication_service.rs | 59 --- .../api/unified_authentication_service_v2.rs | 35 -- crates/router/src/types/transformers.rs | 5 + .../cypress/e2e/configs/Payment/Commons.js | 2 +- .../cypress/e2e/PaymentUtils/Commons.js | 3 +- .../cypress/e2e/PaymentUtils/Paybox.js | 3 +- .../Payments - Confirm/event.test.js | 4 +- .../Payments - Confirm/event.test.js | 4 +- .../checkout.postman_collection.json | 4 +- .../paypal.postman_collection.json | 4 +- 50 files changed, 1287 insertions(+), 855 deletions(-) create mode 100644 crates/hyperswitch_connectors/src/connectors/ctp_mastercard.rs create mode 100644 crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs delete mode 100644 crates/router/src/types/api/unified_authentication_service.rs delete mode 100644 crates/router/src/types/api/unified_authentication_service_v2.rs diff --git a/.typos.toml b/.typos.toml index 983ead3f75d6..67f819c2c52c 100644 --- a/.typos.toml +++ b/.typos.toml @@ -60,6 +60,7 @@ nin = "nin" # National identification number, a field used by PayU connector requestor = "requestor" #Used in external 3ds flows substituters = "substituters" # Present in `flake.nix` unsuccess = "unsuccess" # Used in cryptopay request +authetication = "authetication" #UAS pre-authentication URL [files] extend-exclude = [ diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index ee5654cfb6b1..4725e0552f00 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -3488,7 +3488,9 @@ "enum": [ "threedsecureio", "netcetera", - "gpayments" + "gpayments", + "ctp_mastercard", + "unified_authentication_service" ] }, "AuthenticationStatus": { @@ -6430,6 +6432,7 @@ "checkout", "coinbase", "cryptopay", + "ctp_mastercard", "cybersource", "datatrans", "deutschebank", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d7b86b05a194..0c8736ce85e3 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -5925,7 +5925,9 @@ "enum": [ "threedsecureio", "netcetera", - "gpayments" + "gpayments", + "ctp_mastercard", + "unified_authentication_service" ] }, "AuthenticationStatus": { @@ -8792,6 +8794,7 @@ "checkout", "coinbase", "cryptopay", + "ctp_mastercard", "cybersource", "datatrans", "deutschebank", diff --git a/crates/api_models/src/connector_enums.rs b/crates/api_models/src/connector_enums.rs index c70c54f47920..0989614c8a04 100644 --- a/crates/api_models/src/connector_enums.rs +++ b/crates/api_models/src/connector_enums.rs @@ -68,6 +68,7 @@ pub enum Connector { Checkout, Coinbase, Cryptopay, + CtpMastercard, Cybersource, Datatrans, Deutschebank, @@ -278,6 +279,7 @@ impl Connector { | Self::Threedsecureio | Self::Datatrans | Self::Netcetera + | Self::CtpMastercard | Self::Noon | Self::Stripe => false, Self::Checkout | Self::Nmi | Self::Cybersource => true, diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index e3b11ffaa97b..5aac1a07457b 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2591,12 +2591,17 @@ pub enum AuthenticationConnectors { Threedsecureio, Netcetera, Gpayments, + CtpMastercard, + UnifiedAuthenticationService, } impl AuthenticationConnectors { pub fn is_separate_version_call_required(self) -> bool { match self { - Self::Threedsecureio | Self::Netcetera => false, + Self::Threedsecureio + | Self::Netcetera + | Self::CtpMastercard + | Self::UnifiedAuthenticationService => false, Self::Gpayments => true, } } diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index c1f7cfc366c7..7e893a6415e3 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -242,6 +242,8 @@ pub struct ConnectorConfig { pub zen: Option, pub zsl: Option, pub taxjar: Option, + pub ctp_mastercard: Option, + pub unified_authentication_service: Option, } impl ConnectorConfig { @@ -296,6 +298,10 @@ impl ConnectorConfig { AuthenticationConnectors::Threedsecureio => Ok(connector_data.threedsecureio), AuthenticationConnectors::Netcetera => Ok(connector_data.netcetera), AuthenticationConnectors::Gpayments => Ok(connector_data.gpayments), + AuthenticationConnectors::CtpMastercard => Ok(connector_data.ctp_mastercard), + AuthenticationConnectors::UnifiedAuthenticationService => { + Ok(connector_data.unified_authentication_service) + } } } @@ -412,6 +418,7 @@ impl ConnectorConfig { #[cfg(feature = "dummy_connector")] Connector::DummyConnector7 => Ok(connector_data.paypal_test), Connector::Netcetera => Ok(connector_data.netcetera), + Connector::CtpMastercard => Ok(connector_data.ctp_mastercard), } } } diff --git a/crates/hyperswitch_connectors/src/connectors.rs b/crates/hyperswitch_connectors/src/connectors.rs index 391d9446cbc6..d5a695fe01d7 100644 --- a/crates/hyperswitch_connectors/src/connectors.rs +++ b/crates/hyperswitch_connectors/src/connectors.rs @@ -9,6 +9,7 @@ pub mod boku; pub mod cashtocode; pub mod coinbase; pub mod cryptopay; +pub mod ctp_mastercard; pub mod datatrans; pub mod deutschebank; pub mod digitalvirgo; @@ -55,10 +56,10 @@ pub mod zsl; pub use self::{ airwallex::Airwallex, amazonpay::Amazonpay, bambora::Bambora, bamboraapac::Bamboraapac, billwerk::Billwerk, bitpay::Bitpay, bluesnap::Bluesnap, boku::Boku, cashtocode::Cashtocode, - coinbase::Coinbase, cryptopay::Cryptopay, datatrans::Datatrans, deutschebank::Deutschebank, - digitalvirgo::Digitalvirgo, dlocal::Dlocal, elavon::Elavon, fiserv::Fiserv, - fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, globepay::Globepay, gocardless::Gocardless, - helcim::Helcim, inespay::Inespay, jpmorgan::Jpmorgan, mollie::Mollie, + coinbase::Coinbase, cryptopay::Cryptopay, ctp_mastercard::CtpMastercard, datatrans::Datatrans, + deutschebank::Deutschebank, digitalvirgo::Digitalvirgo, dlocal::Dlocal, elavon::Elavon, + fiserv::Fiserv, fiservemea::Fiservemea, fiuu::Fiuu, forte::Forte, globepay::Globepay, + gocardless::Gocardless, helcim::Helcim, inespay::Inespay, jpmorgan::Jpmorgan, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nexixpay::Nexixpay, nomupay::Nomupay, novalnet::Novalnet, paybox::Paybox, payeezy::Payeezy, payu::Payu, placetopay::Placetopay, powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, razorpay::Razorpay, diff --git a/crates/hyperswitch_connectors/src/connectors/ctp_mastercard.rs b/crates/hyperswitch_connectors/src/connectors/ctp_mastercard.rs new file mode 100644 index 000000000000..1d82b98af8af --- /dev/null +++ b/crates/hyperswitch_connectors/src/connectors/ctp_mastercard.rs @@ -0,0 +1,124 @@ +use common_utils::errors::CustomResult; +use error_stack::report; +use hyperswitch_domain_models::{ + router_data::{AccessToken, RouterData}, + router_flow_types::{ + access_token_auth::AccessTokenAuth, + payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, + refunds::{Execute, RSync}, + }, + router_request_types::{ + AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, + PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, + RefundsData, SetupMandateRequestData, + }, + router_response_types::{PaymentsResponseData, RefundsResponseData}, +}; +use hyperswitch_interfaces::{ + api::{self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorValidation}, + configs::Connectors, + errors, webhooks, +}; + +use crate::constants::headers; + +#[derive(Clone)] +pub struct CtpMastercard; + +impl api::Payment for CtpMastercard {} +impl api::PaymentSession for CtpMastercard {} +impl api::ConnectorAccessToken for CtpMastercard {} +impl api::MandateSetup for CtpMastercard {} +impl api::PaymentAuthorize for CtpMastercard {} +impl api::PaymentSync for CtpMastercard {} +impl api::PaymentCapture for CtpMastercard {} +impl api::PaymentVoid for CtpMastercard {} +impl api::Refund for CtpMastercard {} +impl api::RefundExecute for CtpMastercard {} +impl api::RefundSync for CtpMastercard {} +impl api::PaymentToken for CtpMastercard {} + +impl ConnectorIntegration + for CtpMastercard +{ + // Not Implemented (R) +} + +impl ConnectorCommonExt for CtpMastercard +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for CtpMastercard { + fn id(&self) -> &'static str { + "ctp_mastercard" + } + + fn base_url<'a>(&self, _connectors: &'a Connectors) -> &'a str { + "" + } +} + +impl ConnectorValidation for CtpMastercard {} + +impl ConnectorIntegration for CtpMastercard {} + +impl ConnectorIntegration for CtpMastercard {} + +impl ConnectorIntegration + for CtpMastercard +{ +} + +impl ConnectorIntegration + for CtpMastercard +{ +} + +impl ConnectorIntegration for CtpMastercard {} + +impl ConnectorIntegration for CtpMastercard {} + +impl ConnectorIntegration for CtpMastercard {} + +impl ConnectorIntegration for CtpMastercard {} + +impl ConnectorIntegration for CtpMastercard {} + +#[async_trait::async_trait] +impl webhooks::IncomingWebhook for CtpMastercard { + fn get_webhook_object_reference_id( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_event_type( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } + + fn get_webhook_resource_object( + &self, + _request: &webhooks::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(report!(errors::ConnectorError::WebhooksNotImplemented)) + } +} diff --git a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs index e85fe5dd7de7..e820a07b350c 100644 --- a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs +++ b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service.rs @@ -4,49 +4,51 @@ use common_utils::{ errors::CustomResult, ext_traits::BytesExt, request::{Method, Request, RequestBuilder, RequestContent}, - types::{AmountConvertor, StringMinorUnit, StringMinorUnitForConnector}, + types::{AmountConvertor, FloatMajorUnit, FloatMajorUnitForConnector}, }; use error_stack::{report, ResultExt}; use hyperswitch_domain_models::{ - router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, + router_data::{AccessToken, ErrorResponse, RouterData}, router_flow_types::{ access_token_auth::AccessTokenAuth, payments::{Authorize, Capture, PSync, PaymentMethodToken, Session, SetupMandate, Void}, refunds::{Execute, RSync}, + PostAuthenticate, PreAuthenticate, }, router_request_types::{ + unified_authentication_service::{ + UasAuthenticationResponseData, UasPostAuthenticationRequestData, + UasPreAuthenticationRequestData, + }, AccessTokenRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsSessionData, PaymentsSyncData, RefundsData, SetupMandateRequestData, }, router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{ - PaymentsAuthorizeRouterData, PaymentsCaptureRouterData, PaymentsSyncRouterData, - RefundSyncRouterData, RefundsRouterData, - }, + types::{UasPostAuthenticationRouterData, UasPreAuthenticationRouterData}, }; use hyperswitch_interfaces::{ api::{self, ConnectorCommon, ConnectorCommonExt, ConnectorIntegration, ConnectorValidation}, configs::Connectors, + consts::NO_ERROR_MESSAGE, errors, events::connector_api_logs::ConnectorEvent, types::{self, Response}, webhooks, }; -use masking::{ExposeInterface, Mask}; use transformers as unified_authentication_service; use crate::{constants::headers, types::ResponseRouterData, utils}; #[derive(Clone)] pub struct UnifiedAuthenticationService { - amount_converter: &'static (dyn AmountConvertor + Sync), + amount_converter: &'static (dyn AmountConvertor + Sync), } impl UnifiedAuthenticationService { pub fn new() -> &'static Self { &Self { - amount_converter: &StringMinorUnitForConnector, + amount_converter: &FloatMajorUnitForConnector, } } } @@ -63,11 +65,18 @@ impl api::Refund for UnifiedAuthenticationService {} impl api::RefundExecute for UnifiedAuthenticationService {} impl api::RefundSync for UnifiedAuthenticationService {} impl api::PaymentToken for UnifiedAuthenticationService {} +impl api::UnifiedAuthenticationService for UnifiedAuthenticationService {} +impl api::UasPreAuthentication for UnifiedAuthenticationService {} +impl api::UasPostAuthentication for UnifiedAuthenticationService {} impl ConnectorIntegration for UnifiedAuthenticationService { - // Not Implemented (R) +} + +impl ConnectorIntegration + for UnifiedAuthenticationService +{ } impl ConnectorCommonExt @@ -77,15 +86,19 @@ where { fn build_headers( &self, - req: &RouterData, + _req: &RouterData, _connectors: &Connectors, ) -> CustomResult)>, errors::ConnectorError> { - let mut header = vec![( - headers::CONTENT_TYPE.to_string(), - self.get_content_type().to_string().into(), - )]; - let mut api_key = self.get_auth_header(&req.connector_auth_type)?; - header.append(&mut api_key); + let header = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + headers::SOURCE.to_string(), + self.get_content_type().to_string().into(), + ), + ]; Ok(header) } } @@ -97,9 +110,6 @@ impl ConnectorCommon for UnifiedAuthenticationService { fn get_currency_unit(&self) -> api::CurrencyUnit { api::CurrencyUnit::Base - // TODO! Check connector documentation, on which unit they are processing the currency. - // If the connector accepts amount in lower unit ( i.e cents for USD) then return api::CurrencyUnit::Minor, - // if connector accepts amount in base unit (i.e dollars for USD) then return api::CurrencyUnit::Base } fn common_get_content_type(&self) -> &'static str { @@ -110,20 +120,6 @@ impl ConnectorCommon for UnifiedAuthenticationService { connectors.unified_authentication_service.base_url.as_ref() } - fn get_auth_header( - &self, - auth_type: &ConnectorAuthType, - ) -> CustomResult)>, errors::ConnectorError> { - let auth = unified_authentication_service::UnifiedAuthenticationServiceAuthType::try_from( - auth_type, - ) - .change_context(errors::ConnectorError::FailedToObtainAuthType)?; - Ok(vec![( - headers::AUTHORIZATION.to_string(), - auth.api_key.expose().into_masked(), - )]) - } - fn build_error_response( &self, res: Response, @@ -139,9 +135,9 @@ impl ConnectorCommon for UnifiedAuthenticationService { Ok(ErrorResponse { status_code: res.status_code, - code: response.code, - message: response.message, - reason: response.reason, + code: response.error.clone(), + message: NO_ERROR_MESSAGE.to_owned(), + reason: Some(response.error), attempt_status: None, connector_transaction_id: None, }) @@ -168,12 +164,16 @@ impl ConnectorIntegration - for UnifiedAuthenticationService +impl + ConnectorIntegration< + PreAuthenticate, + UasPreAuthenticationRequestData, + UasAuthenticationResponseData, + > for UnifiedAuthenticationService { fn get_headers( &self, - req: &PaymentsAuthorizeRouterData, + req: &UasPreAuthenticationRouterData, connectors: &Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -185,21 +185,29 @@ impl ConnectorIntegration CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}pre_authetication_processing", + self.base_url(connectors) + )) } fn get_request_body( &self, - req: &PaymentsAuthorizeRouterData, + req: &UasPreAuthenticationRouterData, _connectors: &Connectors, ) -> CustomResult { + let transaction_details = req.request.transaction_details.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "transaction_details", + }, + )?; let amount = utils::convert_amount( self.amount_converter, - req.request.minor_amount, - req.request.currency, + transaction_details.amount, + transaction_details.currency, )?; let connector_router_data = @@ -207,7 +215,7 @@ impl ConnectorIntegration CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() .method(Method::Post) - .url(&types::PaymentsAuthorizeType::get_url( + .url(&types::UasPreAuthenticationType::get_url( self, req, connectors, )?) .attach_default_headers() - .headers(types::PaymentsAuthorizeType::get_headers( + .headers(types::UasPreAuthenticationType::get_headers( self, req, connectors, )?) - .set_body(types::PaymentsAuthorizeType::get_request_body( + .set_body(types::UasPreAuthenticationType::get_request_body( self, req, connectors, )?) .build(), @@ -237,13 +245,13 @@ impl ConnectorIntegration, res: Response, - ) -> CustomResult { - let response: unified_authentication_service::UnifiedAuthenticationServicePaymentsResponse = + ) -> CustomResult { + let response: unified_authentication_service::UnifiedAuthenticationServicePreAuthenticateResponse = res.response - .parse_struct("UnifiedAuthenticationService PaymentsAuthorizeResponse") + .parse_struct("UnifiedAuthenticationService UnifiedAuthenticationServicePreAuthenticateResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -263,12 +271,16 @@ impl ConnectorIntegration - for UnifiedAuthenticationService +impl + ConnectorIntegration< + PostAuthenticate, + UasPostAuthenticationRequestData, + UasAuthenticationResponseData, + > for UnifiedAuthenticationService { fn get_headers( &self, - req: &PaymentsSyncRouterData, + req: &UasPostAuthenticationRouterData, connectors: &Connectors, ) -> CustomResult)>, errors::ConnectorError> { self.build_headers(req, connectors) @@ -280,100 +292,43 @@ impl ConnectorIntegration fn get_url( &self, - _req: &PaymentsSyncRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn build_request( - &self, - req: &PaymentsSyncRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Get) - .url(&types::PaymentsSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::PaymentsSyncType::get_headers(self, req, connectors)?) - .build(), - )) - } - - fn handle_response( - &self, - data: &PaymentsSyncRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: unified_authentication_service::UnifiedAuthenticationServicePaymentsResponse = - res.response - .parse_struct("unified_authentication_service PaymentsSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - RouterData::try_from(ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - - fn get_error_response( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) - } -} - -impl ConnectorIntegration - for UnifiedAuthenticationService -{ - fn get_headers( - &self, - req: &PaymentsCaptureRouterData, + _req: &UasPostAuthenticationRouterData, connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &PaymentsCaptureRouterData, - _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) + Ok(format!( + "{}post_authentication_sync", + self.base_url(connectors) + )) } fn get_request_body( &self, - _req: &PaymentsCaptureRouterData, + req: &UasPostAuthenticationRouterData, _connectors: &Connectors, ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_request_body method".to_string()).into()) + let connector_req = + unified_authentication_service::UnifiedAuthenticationServicePostAuthenticateRequest::try_from( + req, + )?; + Ok(RequestContent::Json(Box::new(connector_req))) } fn build_request( &self, - req: &PaymentsCaptureRouterData, + req: &UasPostAuthenticationRouterData, connectors: &Connectors, ) -> CustomResult, errors::ConnectorError> { Ok(Some( RequestBuilder::new() .method(Method::Post) - .url(&types::PaymentsCaptureType::get_url(self, req, connectors)?) + .url(&types::UasPostAuthenticationType::get_url( + self, req, connectors, + )?) .attach_default_headers() - .headers(types::PaymentsCaptureType::get_headers( + .headers(types::UasPostAuthenticationType::get_headers( self, req, connectors, )?) - .set_body(types::PaymentsCaptureType::get_request_body( + .set_body(types::UasPostAuthenticationType::get_request_body( self, req, connectors, )?) .build(), @@ -382,13 +337,13 @@ impl ConnectorIntegration fn handle_response( &self, - data: &PaymentsCaptureRouterData, + data: &UasPostAuthenticationRouterData, event_builder: Option<&mut ConnectorEvent>, res: Response, - ) -> CustomResult { - let response: unified_authentication_service::UnifiedAuthenticationServicePaymentsResponse = + ) -> CustomResult { + let response: unified_authentication_service::UnifiedAuthenticationServicePostAuthenticateResponse = res.response - .parse_struct("UnifiedAuthenticationService PaymentsCaptureResponse") + .parse_struct("UnifiedAuthenticationService UnifiedAuthenticationServicePostAuthenticateResponse") .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; event_builder.map(|i| i.set_response_body(&response)); router_env::logger::info!(connector_response=?response); @@ -408,171 +363,29 @@ impl ConnectorIntegration } } -impl ConnectorIntegration +impl ConnectorIntegration for UnifiedAuthenticationService { } -impl ConnectorIntegration +impl ConnectorIntegration for UnifiedAuthenticationService { - fn get_headers( - &self, - req: &RefundsRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &RefundsRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn get_request_body( - &self, - req: &RefundsRouterData, - _connectors: &Connectors, - ) -> CustomResult { - let refund_amount = utils::convert_amount( - self.amount_converter, - req.request.minor_refund_amount, - req.request.currency, - )?; - - let connector_router_data = - unified_authentication_service::UnifiedAuthenticationServiceRouterData::from(( - refund_amount, - req, - )); - let connector_req = - unified_authentication_service::UnifiedAuthenticationServiceRefundRequest::try_from( - &connector_router_data, - )?; - Ok(RequestContent::Json(Box::new(connector_req))) - } - - fn build_request( - &self, - req: &RefundsRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - let request = RequestBuilder::new() - .method(Method::Post) - .url(&types::RefundExecuteType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundExecuteType::get_headers( - self, req, connectors, - )?) - .set_body(types::RefundExecuteType::get_request_body( - self, req, connectors, - )?) - .build(); - Ok(Some(request)) - } +} - fn handle_response( - &self, - data: &RefundsRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult, errors::ConnectorError> { - let response: unified_authentication_service::RefundResponse = res - .response - .parse_struct("UnifiedAuthenticationService RefundResponse") - .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, - }) - } +impl ConnectorIntegration + for UnifiedAuthenticationService +{ +} - fn get_error_response( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) - } +impl ConnectorIntegration + for UnifiedAuthenticationService +{ } impl ConnectorIntegration for UnifiedAuthenticationService { - fn get_headers( - &self, - req: &RefundSyncRouterData, - connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - self.build_headers(req, connectors) - } - - fn get_content_type(&self) -> &'static str { - self.common_get_content_type() - } - - fn get_url( - &self, - _req: &RefundSyncRouterData, - _connectors: &Connectors, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("get_url method".to_string()).into()) - } - - fn build_request( - &self, - req: &RefundSyncRouterData, - connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - Ok(Some( - RequestBuilder::new() - .method(Method::Get) - .url(&types::RefundSyncType::get_url(self, req, connectors)?) - .attach_default_headers() - .headers(types::RefundSyncType::get_headers(self, req, connectors)?) - .set_body(types::RefundSyncType::get_request_body( - self, req, connectors, - )?) - .build(), - )) - } - - fn handle_response( - &self, - data: &RefundSyncRouterData, - event_builder: Option<&mut ConnectorEvent>, - res: Response, - ) -> CustomResult { - let response: unified_authentication_service::RefundResponse = res - .response - .parse_struct("UnifiedAuthenticationService RefundSyncResponse") - .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; - event_builder.map(|i| i.set_response_body(&response)); - router_env::logger::info!(connector_response=?response); - RouterData::try_from(ResponseRouterData { - response, - data: data.clone(), - http_code: res.status_code, - }) - } - - fn get_error_response( - &self, - res: Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - self.build_error_response(res, event_builder) - } } #[async_trait::async_trait] diff --git a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs index 6a9347dbeed7..9af21164d064 100644 --- a/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/unified_authentication_service/transformers.rs @@ -1,30 +1,29 @@ use common_enums::enums; -use common_utils::types::StringMinorUnit; +use common_utils::types::FloatMajorUnit; use hyperswitch_domain_models::{ - payment_method_data::PaymentMethodData, router_data::{ConnectorAuthType, RouterData}, - router_flow_types::refunds::{Execute, RSync}, - router_request_types::ResponseId, - router_response_types::{PaymentsResponseData, RefundsResponseData}, - types::{PaymentsAuthorizeRouterData, RefundsRouterData}, + router_request_types::unified_authentication_service::{ + DynamicData, PostAuthenticationDetails, TokenDetails, UasAuthenticationResponseData, + }, + types::{UasPostAuthenticationRouterData, UasPreAuthenticationRouterData}, }; use hyperswitch_interfaces::errors; use masking::Secret; use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; -use crate::{ - types::{RefundsResponseRouterData, ResponseRouterData}, - utils::PaymentsAuthorizeRequestData, -}; +use crate::types::ResponseRouterData; + +const CTP_MASTERCARD: &str = "ctp_mastercard"; //TODO: Fill the struct with respective fields pub struct UnifiedAuthenticationServiceRouterData { - pub amount: StringMinorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. + pub amount: FloatMajorUnit, // The type of amount that a connector accepts, for example, String, i64, f64, etc. pub router_data: T, } -impl From<(StringMinorUnit, T)> for UnifiedAuthenticationServiceRouterData { - fn from((amount, item): (StringMinorUnit, T)) -> Self { +impl From<(FloatMajorUnit, T)> for UnifiedAuthenticationServiceRouterData { + fn from((amount, item): (FloatMajorUnit, T)) -> Self { //Todo : use utils to convert the amount to the type of amount that a connector accepts Self { amount, @@ -33,11 +32,143 @@ impl From<(StringMinorUnit, T)> for UnifiedAuthenticationServiceRouterData } } -//TODO: Fill the struct with respective fields +#[derive(Debug, Serialize, PartialEq)] +pub struct UnifiedAuthenticationServicePreAuthenticateRequest { + pub authenticate_by: String, + pub session_id: String, + pub source_authentication_id: String, + pub authentication_info: Option, + pub service_details: Option, + pub customer_details: Option, + pub pmt_details: Option, + pub auth_creds: AuthType, + pub transaction_details: Option, +} + +#[derive(Debug, Serialize, PartialEq)] +#[serde(tag = "auth_type")] +pub enum AuthType { + HeaderKey { api_key: Secret }, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct PaymentDetails { + pub pan: cards::CardNumber, + pub digital_card_id: Option, + pub payment_data_type: Option, + pub encrypted_src_card_details: Option, + pub card_expiry_date: Secret, + pub cardholder_name: Secret, + pub card_token_number: Secret, + pub account_type: u8, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct TransactionDetails { + pub amount: FloatMajorUnit, + pub currency: enums::Currency, + pub date: Option, + pub pan_source: Option, + pub protection_type: Option, + pub entry_mode: Option, + pub transaction_type: Option, + pub otp_value: Option, + pub three_ds_data: Option, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct ThreeDSData { + pub browser: BrowserInfo, + pub acquirer: Acquirer, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct Acquirer { + pub merchant_id: String, + pub bin: u32, +} + #[derive(Default, Debug, Serialize, PartialEq)] -pub struct UnifiedAuthenticationServicePaymentsRequest { - amount: StringMinorUnit, - card: UnifiedAuthenticationServiceCard, +pub struct BrowserInfo { + pub accept_header: String, + pub screen_width: u32, + pub screen_height: u32, + pub java_enabled: bool, + pub javascript_enabled: bool, + pub language: String, + pub user_agent: String, + pub color_depth: u32, + pub ip: String, + pub tz: i32, + pub time_zone: i8, + pub challenge_window_size: String, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct AuthenticationInfo { + pub authentication_type: Option, + pub authentication_reasons: Option>, + pub consent_received: bool, + pub is_authenticated: bool, + pub locale: Option, + pub supported_card_brands: Option, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct CtpServiceDetails { + pub service_session_ids: Option, + pub merchant_details: Option, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct ServiceSessionIds { + pub client_id: Option, + pub service_id: Option, + pub correlation_id: Option, + pub client_reference_id: Option, + pub merchant_transaction_id: Option, + pub x_src_flow_id: Option, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct MerchantDetails { + pub merchant_id: String, + pub merchant_name: String, + pub mcc: String, + pub country_code: String, + pub name: String, + pub requestor_id: String, + pub requestor_name: String, + pub configuration_id: String, + pub merchant_country: String, + pub merchant_category_code: u32, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct Address { + pub city: String, + pub country: String, + pub line1: Secret, + pub line2: Secret, + pub line3: Option>, + pub post_code: Secret, + pub state: Secret, +} + +#[derive(Default, Debug, Serialize, PartialEq)] +pub struct CustomerDetails { + pub name: Secret, + pub email: Option>, + pub phone_number: Option>, + pub customer_id: String, + #[serde(rename = "type")] + pub customer_type: Option, + pub billing_address: Address, + pub shipping_address: Address, + pub wallet_account_id: Secret, + pub email_hash: Secret, + pub country_code: String, + pub national_identifier: String, } #[derive(Default, Debug, Serialize, Eq, PartialEq)] @@ -49,29 +180,74 @@ pub struct UnifiedAuthenticationServiceCard { complete: bool, } -impl TryFrom<&UnifiedAuthenticationServiceRouterData<&PaymentsAuthorizeRouterData>> - for UnifiedAuthenticationServicePaymentsRequest +impl TryFrom<&UnifiedAuthenticationServiceRouterData<&UasPreAuthenticationRouterData>> + for UnifiedAuthenticationServicePreAuthenticateRequest { type Error = error_stack::Report; fn try_from( - item: &UnifiedAuthenticationServiceRouterData<&PaymentsAuthorizeRouterData>, + item: &UnifiedAuthenticationServiceRouterData<&UasPreAuthenticationRouterData>, ) -> Result { - match item.router_data.request.payment_method_data.clone() { - PaymentMethodData::Card(req_card) => { - let card = UnifiedAuthenticationServiceCard { - number: req_card.card_number, - expiry_month: req_card.card_exp_month, - expiry_year: req_card.card_exp_year, - cvc: req_card.card_cvc, - complete: item.router_data.request.is_auto_capture()?, - }; - Ok(Self { - amount: item.amount.clone(), - card, - }) - } - _ => Err(errors::ConnectorError::NotImplemented("Payment method".to_string()).into()), - } + let auth_type = + UnifiedAuthenticationServiceAuthType::try_from(&item.router_data.connector_auth_type)?; + let authentication_id = item.router_data.authentication_id.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "authentication_id", + }, + )?; + Ok(Self { + authenticate_by: item.router_data.connector.clone(), + session_id: authentication_id.clone(), + source_authentication_id: authentication_id, + authentication_info: None, + service_details: Some(CtpServiceDetails { + service_session_ids: item.router_data.request.service_details.clone().map( + |service_details| ServiceSessionIds { + client_id: None, + service_id: None, + correlation_id: service_details + .service_session_ids + .clone() + .and_then(|service_session_ids| service_session_ids.correlation_id), + client_reference_id: None, + merchant_transaction_id: service_details + .service_session_ids + .clone() + .and_then(|service_session_ids| { + service_session_ids.merchant_transaction_id + }), + x_src_flow_id: service_details + .service_session_ids + .clone() + .and_then(|service_session_ids| service_session_ids.x_src_flow_id), + }, + ), + merchant_details: None, + }), + customer_details: None, + pmt_details: None, + auth_creds: AuthType::HeaderKey { + api_key: auth_type.api_key, + }, + transaction_details: Some(TransactionDetails { + amount: item.amount, + currency: item + .router_data + .request + .transaction_details + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "transaction_details", + })? + .currency, + date: None, + pan_source: None, + protection_type: None, + entry_mode: None, + transaction_type: None, + otp_value: None, + three_ds_data: None, + }), + }) } } @@ -92,144 +268,154 @@ impl TryFrom<&ConnectorAuthType> for UnifiedAuthenticationServiceAuthType { } } } -// PaymentsResponse -//TODO: Append the remaining status flags -#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum UnifiedAuthenticationServicePaymentStatus { - Succeeded, - Failed, - #[default] - Processing, -} - -impl From for common_enums::AttemptStatus { - fn from(item: UnifiedAuthenticationServicePaymentStatus) -> Self { - match item { - UnifiedAuthenticationServicePaymentStatus::Succeeded => Self::Charged, - UnifiedAuthenticationServicePaymentStatus::Failed => Self::Failure, - UnifiedAuthenticationServicePaymentStatus::Processing => Self::Authorizing, - } - } + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum UnifiedAuthenticationServicePreAuthenticateStatus { + ACKSUCCESS, + ACKFAILURE, } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct UnifiedAuthenticationServicePaymentsResponse { - status: UnifiedAuthenticationServicePaymentStatus, - id: String, +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UnifiedAuthenticationServicePreAuthenticateResponse { + status: UnifiedAuthenticationServicePreAuthenticateStatus, } impl TryFrom< ResponseRouterData< F, - UnifiedAuthenticationServicePaymentsResponse, + UnifiedAuthenticationServicePreAuthenticateResponse, T, - PaymentsResponseData, + UasAuthenticationResponseData, >, - > for RouterData + > for RouterData { type Error = error_stack::Report; fn try_from( item: ResponseRouterData< F, - UnifiedAuthenticationServicePaymentsResponse, + UnifiedAuthenticationServicePreAuthenticateResponse, T, - PaymentsResponseData, + UasAuthenticationResponseData, >, ) -> Result { Ok(Self { - status: common_enums::AttemptStatus::from(item.response.status), - response: Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.id), - redirection_data: Box::new(None), - mandate_reference: Box::new(None), - connector_metadata: None, - network_txn_id: None, - connector_response_reference_id: None, - incremental_authorization_allowed: None, - charge_id: None, - }), + response: Ok(UasAuthenticationResponseData::PreAuthentication {}), ..item.data }) } } -//TODO: Fill the struct with respective fields -// REFUND : -// Type definition for RefundRequest -#[derive(Default, Debug, Serialize)] -pub struct UnifiedAuthenticationServiceRefundRequest { - pub amount: StringMinorUnit, +#[derive(Debug, Serialize, PartialEq)] +pub struct UnifiedAuthenticationServicePostAuthenticateRequest { + pub authenticate_by: String, + pub source_authentication_id: String, + pub auth_creds: AuthType, } -impl TryFrom<&UnifiedAuthenticationServiceRouterData<&RefundsRouterData>> - for UnifiedAuthenticationServiceRefundRequest -{ - type Error = error_stack::Report; - fn try_from( - item: &UnifiedAuthenticationServiceRouterData<&RefundsRouterData>, - ) -> Result { - Ok(Self { - amount: item.amount.to_owned(), - }) - } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UnifiedAuthenticationServicePostAuthenticateResponse { + pub authentication_details: AuthenticationDetails, } -// Type definition for Refund Response - -#[allow(dead_code)] -#[derive(Debug, Serialize, Default, Deserialize, Clone)] -pub enum RefundStatus { - Succeeded, - Failed, - #[default] - Processing, +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AuthenticationDetails { + pub eci: Option, + pub token_details: UasTokenDetails, + pub dynamic_data_details: Option, } -impl From for enums::RefundStatus { - fn from(item: RefundStatus) -> Self { - match item { - RefundStatus::Succeeded => Self::Success, - RefundStatus::Failed => Self::Failure, - RefundStatus::Processing => Self::Pending, - //TODO: Review mapping - } - } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UasTokenDetails { + pub payment_token: cards::CardNumber, + pub payment_account_reference: String, + pub token_expiration_month: Secret, + pub token_expiration_year: Secret, } -//TODO: Fill the struct with respective fields -#[derive(Default, Debug, Clone, Serialize, Deserialize)] -pub struct RefundResponse { - id: String, - status: RefundStatus, +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UasDynamicData { + pub dynamic_data_value: Option>, + pub dynamic_data_type: String, + pub ds_trans_id: Option, } -impl TryFrom> for RefundsRouterData { +impl TryFrom<&UasPostAuthenticationRouterData> + for UnifiedAuthenticationServicePostAuthenticateRequest +{ type Error = error_stack::Report; - fn try_from( - item: RefundsResponseRouterData, - ) -> Result { + fn try_from(item: &UasPostAuthenticationRouterData) -> Result { + let auth_type = UnifiedAuthenticationServiceAuthType::try_from(&item.connector_auth_type)?; Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), - }), - ..item.data + authenticate_by: CTP_MASTERCARD.to_owned(), + source_authentication_id: item.authentication_id.clone().ok_or( + errors::ConnectorError::MissingRequiredField { + field_name: "authentication_id", + }, + )?, + auth_creds: AuthType::HeaderKey { + api_key: auth_type.api_key, + }, }) } } -impl TryFrom> for RefundsRouterData { +impl + TryFrom< + ResponseRouterData< + F, + UnifiedAuthenticationServicePostAuthenticateResponse, + T, + UasAuthenticationResponseData, + >, + > for RouterData +{ type Error = error_stack::Report; fn try_from( - item: RefundsResponseRouterData, + item: ResponseRouterData< + F, + UnifiedAuthenticationServicePostAuthenticateResponse, + T, + UasAuthenticationResponseData, + >, ) -> Result { Ok(Self { - response: Ok(RefundsResponseData { - connector_refund_id: item.response.id.to_string(), - refund_status: enums::RefundStatus::from(item.response.status), + response: Ok(UasAuthenticationResponseData::PostAuthentication { + authentication_details: PostAuthenticationDetails { + eci: item.response.authentication_details.eci, + token_details: TokenDetails { + payment_token: item + .response + .authentication_details + .token_details + .payment_token, + payment_account_reference: item + .response + .authentication_details + .token_details + .payment_account_reference, + token_expiration_month: item + .response + .authentication_details + .token_details + .token_expiration_month, + token_expiration_year: item + .response + .authentication_details + .token_details + .token_expiration_year, + }, + dynamic_data_details: item + .response + .authentication_details + .dynamic_data_details + .map(|dynamic_data| DynamicData { + dynamic_data_value: dynamic_data.dynamic_data_value, + dynamic_data_type: dynamic_data.dynamic_data_type, + ds_trans_id: dynamic_data.ds_trans_id, + }), + }, }), ..item.data }) @@ -239,8 +425,5 @@ impl TryFrom> for RefundsRouter //TODO: Fill the struct with respective fields #[derive(Default, Debug, Serialize, Deserialize, PartialEq)] pub struct UnifiedAuthenticationServiceErrorResponse { - pub status_code: u16, - pub code: String, - pub message: String, - pub reason: Option, + pub error: String, } diff --git a/crates/hyperswitch_connectors/src/constants.rs b/crates/hyperswitch_connectors/src/constants.rs index 48a32c180474..9ca4ee9591fb 100644 --- a/crates/hyperswitch_connectors/src/constants.rs +++ b/crates/hyperswitch_connectors/src/constants.rs @@ -26,6 +26,7 @@ pub(crate) mod headers { pub(crate) const X_API_KEY: &str = "X-Api-Key"; pub(crate) const CORRELATION_ID: &str = "Correlation-Id"; pub(crate) const WP_API_VERSION: &str = "WP-Api-Version"; + pub(crate) const SOURCE: &str = "Source"; } /// Unsupported response type error message diff --git a/crates/hyperswitch_connectors/src/default_implementations.rs b/crates/hyperswitch_connectors/src/default_implementations.rs index 041e519acdc6..703b0bb2ac7e 100644 --- a/crates/hyperswitch_connectors/src/default_implementations.rs +++ b/crates/hyperswitch_connectors/src/default_implementations.rs @@ -33,8 +33,13 @@ use hyperswitch_domain_models::{ PreProcessing, Reject, SdkSessionUpdate, }, webhooks::VerifyWebhookSource, + PostAuthenticate, PreAuthenticate, }, router_request_types::{ + unified_authentication_service::{ + UasAuthenticationResponseData, UasPostAuthenticationRequestData, + UasPreAuthenticationRequestData, + }, AcceptDisputeRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, DefendDisputeRequestData, MandateRevokeRequestData, PaymentsApproveData, PaymentsIncrementalAuthorizationData, PaymentsPostProcessingData, @@ -70,6 +75,7 @@ use hyperswitch_interfaces::{ PaymentsPreProcessing, TaxCalculation, }, ConnectorIntegration, ConnectorMandateRevoke, ConnectorRedirectResponse, + UasPostAuthentication, UasPreAuthentication, UnifiedAuthenticationService, }, errors::ConnectorError, }; @@ -140,7 +146,8 @@ default_imp_for_authorize_session_token!( connectors::Worldpay, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_calculate_tax { @@ -209,7 +216,8 @@ default_imp_for_calculate_tax!( connectors::Worldpay, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_session_update { @@ -279,7 +287,8 @@ default_imp_for_session_update!( connectors::Thunes, connectors::Tsys, connectors::Deutschebank, - connectors::Volt + connectors::Volt, + connectors::CtpMastercard ); macro_rules! default_imp_for_post_session_tokens { @@ -349,7 +358,8 @@ default_imp_for_post_session_tokens!( connectors::Deutschebank, connectors::Volt, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); use crate::connectors; @@ -409,7 +419,8 @@ default_imp_for_complete_authorize!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_incremental_authorization { @@ -480,7 +491,8 @@ default_imp_for_incremental_authorization!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_create_customer { @@ -549,7 +561,8 @@ default_imp_for_create_customer!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_connector_redirect_response { @@ -613,7 +626,8 @@ default_imp_for_connector_redirect_response!( connectors::Worldline, connectors::Volt, connectors::Xendit, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_pre_processing_steps{ @@ -680,7 +694,8 @@ default_imp_for_pre_processing_steps!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_post_processing_steps{ @@ -751,7 +766,8 @@ default_imp_for_post_processing_steps!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_approve { @@ -822,7 +838,8 @@ default_imp_for_approve!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_reject { @@ -893,7 +910,8 @@ default_imp_for_reject!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_webhook_source_verification { @@ -964,7 +982,8 @@ default_imp_for_webhook_source_verification!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_accept_dispute { @@ -1036,7 +1055,8 @@ default_imp_for_accept_dispute!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_submit_evidence { @@ -1107,7 +1127,8 @@ default_imp_for_submit_evidence!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_defend_dispute { @@ -1178,7 +1199,8 @@ default_imp_for_defend_dispute!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_file_upload { @@ -1258,7 +1280,8 @@ default_imp_for_file_upload!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_payouts { @@ -1321,7 +1344,8 @@ default_imp_for_payouts!( connectors::Worldpay, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1394,7 +1418,8 @@ default_imp_for_payouts_create!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1467,7 +1492,8 @@ default_imp_for_payouts_retrieve!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1540,7 +1566,8 @@ default_imp_for_payouts_eligibility!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1613,7 +1640,8 @@ default_imp_for_payouts_fulfill!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1686,7 +1714,8 @@ default_imp_for_payouts_cancel!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1759,7 +1788,8 @@ default_imp_for_payouts_quote!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1832,7 +1862,8 @@ default_imp_for_payouts_recipient!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1905,7 +1936,8 @@ default_imp_for_payouts_recipient_account!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "frm")] @@ -1978,7 +2010,8 @@ default_imp_for_frm_sale!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "frm")] @@ -2051,7 +2084,8 @@ default_imp_for_frm_checkout!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "frm")] @@ -2124,7 +2158,8 @@ default_imp_for_frm_transaction!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "frm")] @@ -2197,7 +2232,8 @@ default_imp_for_frm_fulfillment!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); #[cfg(feature = "frm")] @@ -2270,7 +2306,8 @@ default_imp_for_frm_record_return!( connectors::Volt, connectors::Xendit, connectors::Zen, - connectors::Zsl + connectors::Zsl, + connectors::CtpMastercard ); macro_rules! default_imp_for_revoking_mandates { @@ -2340,5 +2377,147 @@ default_imp_for_revoking_mandates!( connectors::Volt, connectors::Xendit, connectors::Zen, + connectors::Zsl, + connectors::CtpMastercard +); + +macro_rules! default_imp_for_uas_pre_authentication { + ($($path:ident::$connector:ident),*) => { + $( impl UnifiedAuthenticationService for $path::$connector {} + impl UasPreAuthentication for $path::$connector {} + impl + ConnectorIntegration< + PreAuthenticate, + UasPreAuthenticationRequestData, + UasAuthenticationResponseData + > for $path::$connector + {} + )* + }; +} + +default_imp_for_uas_pre_authentication!( + connectors::Airwallex, + connectors::Amazonpay, + connectors::Bambora, + connectors::Bamboraapac, + connectors::Billwerk, + connectors::Bluesnap, + connectors::Bitpay, + connectors::Boku, + connectors::Cashtocode, + connectors::Coinbase, + connectors::Cryptopay, + connectors::CtpMastercard, + connectors::Datatrans, + connectors::Deutschebank, + connectors::Digitalvirgo, + connectors::Dlocal, + connectors::Elavon, + connectors::Fiserv, + connectors::Fiservemea, + connectors::Fiuu, + connectors::Forte, + connectors::Globepay, + connectors::Gocardless, + connectors::Helcim, + connectors::Inespay, + connectors::Jpmorgan, + connectors::Nomupay, + connectors::Novalnet, + connectors::Nexinets, + connectors::Nexixpay, + connectors::Payeezy, + connectors::Payu, + connectors::Powertranz, + connectors::Prophetpay, + connectors::Mollie, + connectors::Multisafepay, + connectors::Paybox, + connectors::Placetopay, + connectors::Rapyd, + connectors::Razorpay, + connectors::Redsys, + connectors::Shift4, + connectors::Stax, + connectors::Square, + connectors::Taxjar, + connectors::Thunes, + connectors::Tsys, + connectors::Worldline, + connectors::Worldpay, + connectors::Volt, + connectors::Xendit, + connectors::Zen, + connectors::Zsl +); + +macro_rules! default_imp_for_uas_post_authentication { + ($($path:ident::$connector:ident),*) => { + $( impl UasPostAuthentication for $path::$connector {} + impl + ConnectorIntegration< + PostAuthenticate, + UasPostAuthenticationRequestData, + UasAuthenticationResponseData + > for $path::$connector + {} + )* + }; +} + +default_imp_for_uas_post_authentication!( + connectors::Airwallex, + connectors::Amazonpay, + connectors::Bambora, + connectors::Bamboraapac, + connectors::Billwerk, + connectors::Bitpay, + connectors::Bluesnap, + connectors::Boku, + connectors::Cashtocode, + connectors::Coinbase, + connectors::Cryptopay, + connectors::CtpMastercard, + connectors::Datatrans, + connectors::Deutschebank, + connectors::Digitalvirgo, + connectors::Dlocal, + connectors::Elavon, + connectors::Fiserv, + connectors::Fiservemea, + connectors::Fiuu, + connectors::Forte, + connectors::Globepay, + connectors::Gocardless, + connectors::Helcim, + connectors::Inespay, + connectors::Jpmorgan, + connectors::Nomupay, + connectors::Novalnet, + connectors::Nexinets, + connectors::Nexixpay, + connectors::Payeezy, + connectors::Payu, + connectors::Powertranz, + connectors::Prophetpay, + connectors::Mollie, + connectors::Multisafepay, + connectors::Paybox, + connectors::Placetopay, + connectors::Rapyd, + connectors::Razorpay, + connectors::Redsys, + connectors::Shift4, + connectors::Stax, + connectors::Square, + connectors::Taxjar, + connectors::Thunes, + connectors::Tsys, + connectors::Worldline, + connectors::Worldpay, + connectors::Volt, + connectors::Xendit, + connectors::Zen, connectors::Zsl ); diff --git a/crates/hyperswitch_connectors/src/default_implementations_v2.rs b/crates/hyperswitch_connectors/src/default_implementations_v2.rs index 33c45a542257..4161b36f906e 100644 --- a/crates/hyperswitch_connectors/src/default_implementations_v2.rs +++ b/crates/hyperswitch_connectors/src/default_implementations_v2.rs @@ -216,6 +216,7 @@ default_imp_for_new_connector_integration_payment!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -288,6 +289,7 @@ default_imp_for_new_connector_integration_refund!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -355,6 +357,7 @@ default_imp_for_new_connector_integration_connector_access_token!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -428,6 +431,7 @@ default_imp_for_new_connector_integration_accept_dispute!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -500,6 +504,7 @@ default_imp_for_new_connector_integration_submit_evidence!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -572,6 +577,7 @@ default_imp_for_new_connector_integration_defend_dispute!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -654,6 +660,7 @@ default_imp_for_new_connector_integration_file_upload!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -728,6 +735,7 @@ default_imp_for_new_connector_integration_payouts_create!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -802,6 +810,7 @@ default_imp_for_new_connector_integration_payouts_eligibility!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -876,6 +885,7 @@ default_imp_for_new_connector_integration_payouts_fulfill!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -950,6 +960,7 @@ default_imp_for_new_connector_integration_payouts_cancel!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1024,6 +1035,7 @@ default_imp_for_new_connector_integration_payouts_quote!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1098,6 +1110,7 @@ default_imp_for_new_connector_integration_payouts_recipient!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1172,6 +1185,7 @@ default_imp_for_new_connector_integration_payouts_sync!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1246,6 +1260,7 @@ default_imp_for_new_connector_integration_payouts_recipient_account!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1318,6 +1333,7 @@ default_imp_for_new_connector_integration_webhook_source_verification!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1392,6 +1408,7 @@ default_imp_for_new_connector_integration_frm_sale!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1466,6 +1483,7 @@ default_imp_for_new_connector_integration_frm_checkout!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1540,6 +1558,7 @@ default_imp_for_new_connector_integration_frm_transaction!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1614,6 +1633,7 @@ default_imp_for_new_connector_integration_frm_fulfillment!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1688,6 +1708,7 @@ default_imp_for_new_connector_integration_frm_record_return!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, @@ -1759,6 +1780,7 @@ default_imp_for_new_connector_integration_revoking_mandates!( connectors::Cashtocode, connectors::Coinbase, connectors::Cryptopay, + connectors::CtpMastercard, connectors::Datatrans, connectors::Deutschebank, connectors::Digitalvirgo, diff --git a/crates/hyperswitch_domain_models/src/payment_method_data.rs b/crates/hyperswitch_domain_models/src/payment_method_data.rs index db35766f33ae..169719cb5dad 100644 --- a/crates/hyperswitch_domain_models/src/payment_method_data.rs +++ b/crates/hyperswitch_domain_models/src/payment_method_data.rs @@ -596,6 +596,7 @@ pub struct NetworkTokenData { pub card_issuing_country: Option, pub bank_code: Option, pub nick_name: Option>, + pub eci: Option, } #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize)] diff --git a/crates/hyperswitch_domain_models/src/router_flow_types.rs b/crates/hyperswitch_domain_models/src/router_flow_types.rs index 6cdde87772b0..bdd7691b681a 100644 --- a/crates/hyperswitch_domain_models/src/router_flow_types.rs +++ b/crates/hyperswitch_domain_models/src/router_flow_types.rs @@ -6,6 +6,7 @@ pub mod mandate_revoke; pub mod payments; pub mod payouts; pub mod refunds; +pub mod unified_authentication_service; pub mod webhooks; pub use access_token_auth::*; @@ -15,4 +16,5 @@ pub use fraud_check::*; pub use payments::*; pub use payouts::*; pub use refunds::*; +pub use unified_authentication_service::*; pub use webhooks::*; diff --git a/crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs b/crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs new file mode 100644 index 000000000000..329c18b741ed --- /dev/null +++ b/crates/hyperswitch_domain_models/src/router_flow_types/unified_authentication_service.rs @@ -0,0 +1,5 @@ +#[derive(Debug, Clone)] +pub struct PreAuthenticate; + +#[derive(Debug, Clone)] +pub struct PostAuthenticate; diff --git a/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs b/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs index 6dd899cd5b38..af2a940cb51b 100644 --- a/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs +++ b/crates/hyperswitch_domain_models/src/router_request_types/unified_authentication_service.rs @@ -25,7 +25,7 @@ pub struct TransactionDetails { } #[derive(Clone, Debug)] -pub struct UasPostAuthenticationRequestData; +pub struct UasPostAuthenticationRequestData {} #[derive(Debug, Clone)] pub enum UasAuthenticationResponseData { diff --git a/crates/hyperswitch_domain_models/src/types.rs b/crates/hyperswitch_domain_models/src/types.rs index 201a073dd3e3..2dd38d88b6d9 100644 --- a/crates/hyperswitch_domain_models/src/types.rs +++ b/crates/hyperswitch_domain_models/src/types.rs @@ -5,9 +5,14 @@ use crate::{ router_flow_types::{ AccessTokenAuth, Authorize, AuthorizeSessionToken, CalculateTax, Capture, CompleteAuthorize, CreateConnectorCustomer, Execute, PSync, PaymentMethodToken, - PostSessionTokens, PreProcessing, RSync, Session, SetupMandate, Void, + PostAuthenticate, PostSessionTokens, PreAuthenticate, PreProcessing, RSync, Session, + SetupMandate, Void, }, router_request_types::{ + unified_authentication_service::{ + UasAuthenticationResponseData, UasPostAuthenticationRequestData, + UasPreAuthenticationRequestData, + }, AccessTokenRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, PaymentMethodTokenizationData, PaymentsAuthorizeData, PaymentsCancelData, PaymentsCaptureData, PaymentsPostSessionTokensData, @@ -45,3 +50,9 @@ pub type RefreshTokenRouterData = RouterData; pub type PaymentsSessionRouterData = RouterData; + +pub type UasPostAuthenticationRouterData = + RouterData; + +pub type UasPreAuthenticationRouterData = + RouterData; diff --git a/crates/hyperswitch_interfaces/src/api.rs b/crates/hyperswitch_interfaces/src/api.rs index 617832f191df..bb6b2a4e4f5b 100644 --- a/crates/hyperswitch_interfaces/src/api.rs +++ b/crates/hyperswitch_interfaces/src/api.rs @@ -27,9 +27,17 @@ use hyperswitch_domain_models::{ router_data::{AccessToken, ConnectorAuthType, ErrorResponse, RouterData}, router_data_v2::{ flow_common_types::WebhookSourceVerifyData, AccessTokenFlowData, MandateRevokeFlowData, + UasFlowData, + }, + router_flow_types::{ + mandate_revoke::MandateRevoke, AccessTokenAuth, PostAuthenticate, PreAuthenticate, + VerifyWebhookSource, }, - router_flow_types::{mandate_revoke::MandateRevoke, AccessTokenAuth, VerifyWebhookSource}, router_request_types::{ + unified_authentication_service::{ + UasAuthenticationResponseData, UasPostAuthenticationRequestData, + UasPreAuthenticationRequestData, + }, AccessTokenRequestData, MandateRevokeRequestData, VerifyWebhookSourceRequestData, }, router_response_types::{MandateRevokeResponseData, VerifyWebhookSourceResponseData}, @@ -336,6 +344,60 @@ pub trait ConnectorVerifyWebhookSourceV2: { } +/// trait UnifiedAuthenticationService +pub trait UnifiedAuthenticationService: + ConnectorCommon + UasPreAuthentication + UasPostAuthentication +{ +} + +/// trait UasPreAuthentication +pub trait UasPreAuthentication: + ConnectorIntegration< + PreAuthenticate, + UasPreAuthenticationRequestData, + UasAuthenticationResponseData, +> +{ +} + +/// trait UasPostAuthentication +pub trait UasPostAuthentication: + ConnectorIntegration< + PostAuthenticate, + UasPostAuthenticationRequestData, + UasAuthenticationResponseData, +> +{ +} + +/// trait UnifiedAuthenticationServiceV2 +pub trait UnifiedAuthenticationServiceV2: + ConnectorCommon + UasPreAuthenticationV2 + UasPostAuthenticationV2 +{ +} + +///trait UasPreAuthenticationV2 +pub trait UasPreAuthenticationV2: + ConnectorIntegrationV2< + PreAuthenticate, + UasFlowData, + UasPreAuthenticationRequestData, + UasAuthenticationResponseData, +> +{ +} + +/// trait UasPostAuthenticationV2 +pub trait UasPostAuthenticationV2: + ConnectorIntegrationV2< + PostAuthenticate, + UasFlowData, + UasPostAuthenticationRequestData, + UasAuthenticationResponseData, +> +{ +} + /// trait ConnectorValidation pub trait ConnectorValidation: ConnectorCommon { /// fn validate_capture_method diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs index e7e59d2d0637..4a3b99636dc3 100644 --- a/crates/hyperswitch_interfaces/src/configs.rs +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -3,7 +3,6 @@ use common_enums::ApplicationError; use masking::Secret; use router_derive; use serde::Deserialize; - // struct Connectors #[allow(missing_docs, missing_debug_implementations)] #[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] @@ -28,6 +27,7 @@ pub struct Connectors { pub checkout: ConnectorParams, pub coinbase: ConnectorParams, pub cryptopay: ConnectorParams, + pub ctp_mastercard: NoParams, pub cybersource: ConnectorParams, pub datatrans: ConnectorParams, pub deutschebank: ConnectorParams, @@ -110,6 +110,17 @@ pub struct ConnectorParams { pub secondary_base_url: Option, } +///struct No Param for connectors with no params +#[derive(Debug, Deserialize, Clone, Default)] +pub struct NoParams; + +impl NoParams { + /// function to satisfy connector param validation macro + pub fn validate(&self, _parent_field: &str) -> Result<(), ApplicationError> { + Ok(()) + } +} + /// struct ConnectorParamsWithKeys #[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] #[serde(default)] diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs index 41a1b1ba83c9..dd344edf9da2 100644 --- a/crates/hyperswitch_interfaces/src/types.rs +++ b/crates/hyperswitch_interfaces/src/types.rs @@ -13,9 +13,14 @@ use hyperswitch_domain_models::{ Session, SetupMandate, Void, }, refunds::{Execute, RSync}, + unified_authentication_service::{PostAuthenticate, PreAuthenticate}, webhooks::VerifyWebhookSource, }, router_request_types::{ + unified_authentication_service::{ + UasAuthenticationResponseData, UasPostAuthenticationRequestData, + UasPreAuthenticationRequestData, + }, AcceptDisputeRequestData, AccessTokenRequestData, AuthorizeSessionTokenData, CompleteAuthorizeData, ConnectorCustomerData, DefendDisputeRequestData, MandateRevokeRequestData, PaymentMethodTokenizationData, PaymentsAuthorizeData, @@ -185,3 +190,17 @@ pub type RetrieveFileType = /// Type alias for `ConnectorIntegration` pub type DefendDisputeType = dyn ConnectorIntegration; + +/// Type alias for `ConnectorIntegration` +pub type UasPreAuthenticationType = dyn ConnectorIntegration< + PreAuthenticate, + UasPreAuthenticationRequestData, + UasAuthenticationResponseData, +>; + +/// Type alias for `ConnectorIntegration` +pub type UasPostAuthenticationType = dyn ConnectorIntegration< + PostAuthenticate, + UasPostAuthenticationRequestData, + UasAuthenticationResponseData, +>; diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 3d3c42d07961..0da0e711fbca 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -39,15 +39,16 @@ pub use hyperswitch_connectors::connectors::{ airwallex, airwallex::Airwallex, amazonpay, amazonpay::Amazonpay, bambora, bambora::Bambora, bamboraapac, bamboraapac::Bamboraapac, billwerk, billwerk::Billwerk, bitpay, bitpay::Bitpay, bluesnap, bluesnap::Bluesnap, boku, boku::Boku, cashtocode, cashtocode::Cashtocode, coinbase, - coinbase::Coinbase, cryptopay, cryptopay::Cryptopay, datatrans, datatrans::Datatrans, - deutschebank, deutschebank::Deutschebank, digitalvirgo, digitalvirgo::Digitalvirgo, dlocal, - dlocal::Dlocal, elavon, elavon::Elavon, fiserv, fiserv::Fiserv, fiservemea, - fiservemea::Fiservemea, fiuu, fiuu::Fiuu, forte, forte::Forte, globepay, globepay::Globepay, - gocardless, gocardless::Gocardless, helcim, helcim::Helcim, inespay, inespay::Inespay, - jpmorgan, jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, multisafepay::Multisafepay, - nexinets, nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, nomupay, nomupay::Nomupay, - novalnet, novalnet::Novalnet, paybox, paybox::Paybox, payeezy, payeezy::Payeezy, payu, - payu::Payu, placetopay, placetopay::Placetopay, powertranz, powertranz::Powertranz, prophetpay, + coinbase::Coinbase, cryptopay, cryptopay::Cryptopay, ctp_mastercard, + ctp_mastercard::CtpMastercard, datatrans, datatrans::Datatrans, deutschebank, + deutschebank::Deutschebank, digitalvirgo, digitalvirgo::Digitalvirgo, dlocal, dlocal::Dlocal, + elavon, elavon::Elavon, fiserv, fiserv::Fiserv, fiservemea, fiservemea::Fiservemea, fiuu, + fiuu::Fiuu, forte, forte::Forte, globepay, globepay::Globepay, gocardless, + gocardless::Gocardless, helcim, helcim::Helcim, inespay, inespay::Inespay, jpmorgan, + jpmorgan::Jpmorgan, mollie, mollie::Mollie, multisafepay, multisafepay::Multisafepay, nexinets, + nexinets::Nexinets, nexixpay, nexixpay::Nexixpay, nomupay, nomupay::Nomupay, novalnet, + novalnet::Novalnet, paybox, paybox::Paybox, payeezy, payeezy::Payeezy, payu, payu::Payu, + placetopay, placetopay::Placetopay, powertranz, powertranz::Powertranz, prophetpay, prophetpay::Prophetpay, rapyd, rapyd::Rapyd, razorpay, razorpay::Razorpay, redsys, redsys::Redsys, shift4, shift4::Shift4, square, square::Square, stax, stax::Stax, taxjar, taxjar::Taxjar, thunes, thunes::Thunes, tsys, tsys::Tsys, unified_authentication_service, diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 7aaf9b5e2bb2..6f9fd33ef1c6 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -15,7 +15,7 @@ use crate::{connector::utils::PayoutsData, types::api::payouts, utils::OptionExt use crate::{ connector::utils::{ self, missing_field_err, AddressDetailsData, BrowserInformationData, CardData, - PaymentsAuthorizeRequestData, PhoneDetailsData, RouterData, + NetworkTokenData, PaymentsAuthorizeRequestData, PhoneDetailsData, RouterData, }, consts, core::errors, @@ -618,6 +618,7 @@ pub enum AdyenPaymentMethod<'a> { #[serde(rename = "econtext_stores")] PayEasy(Box), Pix(Box), + NetworkToken(Box), } #[derive(Debug, Clone, Serialize)] @@ -731,7 +732,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for JCSVoucherData { Ok(Self { first_name: item.get_billing_first_name()?, last_name: item.get_optional_billing_last_name(), - shopper_email: item.get_billing_email().or(item.request.get_email())?, + shopper_email: item.get_billing_email()?, telephone_number: item.get_billing_phone_number()?, }) } @@ -1217,6 +1218,19 @@ pub struct AdyenApplePay { } #[serde_with::skip_serializing_none] +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct AdyenNetworkTokenData { + #[serde(rename = "type")] + payment_type: PaymentType, + number: CardNumber, + expiry_month: Secret, + expiry_year: Secret, + holder_name: Option>, + brand: Option, //Mandatory for mandate using network_txns_id + network_payment_reference: Option>, +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DokuBankData { @@ -1598,6 +1612,9 @@ impl TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> for AdyenPay domain::PaymentMethodData::GiftCard(ref gift_card_data) => { AdyenPaymentRequest::try_from((item, gift_card_data.as_ref())) } + domain::PaymentMethodData::NetworkToken(ref token_data) => { + AdyenPaymentRequest::try_from((item, token_data)) + } domain::PaymentMethodData::Crypto(_) | domain::PaymentMethodData::MandatePayment | domain::PaymentMethodData::Reward @@ -1606,7 +1623,6 @@ impl TryFrom<&AdyenRouterData<&types::PaymentsAuthorizeRouterData>> for AdyenPay | domain::PaymentMethodData::Upi(_) | domain::PaymentMethodData::OpenBanking(_) | domain::PaymentMethodData::CardToken(_) - | domain::PaymentMethodData::NetworkToken(_) | domain::PaymentMethodData::CardDetailsForNetworkTransactionId(_) => { Err(errors::ConnectorError::NotImplemented( utils::get_unimplemented_payment_method_error_message("Adyen"), @@ -2567,18 +2583,11 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for DokuBankData { Ok(Self { first_name: item.get_billing_first_name()?, last_name: item.get_optional_billing_last_name(), - shopper_email: item.get_billing_email().or(item.request.get_email())?, + shopper_email: item.get_billing_email()?, }) } } -fn get_optional_shopper_email(item: &types::PaymentsAuthorizeRouterData) -> Option { - match item.get_billing_email() { - Ok(email) => Some(email), - Err(_) => item.request.get_optional_email(), - } -} - impl TryFrom<&domain::payments::CardRedirectData> for AdyenPaymentMethod<'_> { type Error = Error; fn try_from( @@ -2615,7 +2624,6 @@ impl let amount = get_amount_data(item); let auth_type = AdyenAuthType::try_from(&item.router_data.connector_auth_type)?; let shopper_interaction = AdyenShopperInteraction::from(item.router_data); - let shopper_email = get_optional_shopper_email(item.router_data); let (recurring_processing_model, store_payment_method, shopper_reference) = get_recurring_processing_model(item.router_data)?; let browser_info = None; @@ -2697,12 +2705,53 @@ impl } } } - payments::MandateReferenceId::NetworkTokenWithNTI(_) => { - Err(errors::ConnectorError::NotSupported { - message: "Network tokenization for payment method".to_string(), - connector: "Adyen", - })? - } + payments::MandateReferenceId::NetworkTokenWithNTI(network_mandate_id) => { + match item.router_data.request.payment_method_data { + domain::PaymentMethodData::NetworkToken(ref token_data) => { + let card_issuer = token_data.get_card_issuer()?; + let brand = CardBrand::try_from(&card_issuer)?; + let card_holder_name = item.router_data.get_optional_billing_full_name(); + let adyen_network_token = AdyenNetworkTokenData { + payment_type: PaymentType::NetworkToken, + number: token_data.token_number.clone(), + expiry_month: token_data.token_exp_month.clone(), + expiry_year: token_data.get_expiry_year_4_digit(), + holder_name: card_holder_name, + brand: Some(brand), // FIXME: Remove hardcoding + network_payment_reference: Some(Secret::new( + network_mandate_id.network_transaction_id, + )), + }; + Ok(AdyenPaymentMethod::NetworkToken(Box::new( + adyen_network_token, + ))) + } + + domain::PaymentMethodData::Card(_) + | domain::PaymentMethodData::CardRedirect(_) + | domain::PaymentMethodData::Wallet(_) + | domain::PaymentMethodData::PayLater(_) + | domain::PaymentMethodData::BankRedirect(_) + | domain::PaymentMethodData::BankDebit(_) + | domain::PaymentMethodData::BankTransfer(_) + | domain::PaymentMethodData::Crypto(_) + | domain::PaymentMethodData::MandatePayment + | domain::PaymentMethodData::Reward + | domain::PaymentMethodData::RealTimePayment(_) + | domain::PaymentMethodData::Upi(_) + | domain::PaymentMethodData::Voucher(_) + | domain::PaymentMethodData::GiftCard(_) + | domain::PaymentMethodData::OpenBanking(_) + | domain::PaymentMethodData::CardToken(_) + | domain::PaymentMethodData::MobilePayment(_) + | domain::PaymentMethodData::CardDetailsForNetworkTransactionId(_) => { + Err(errors::ConnectorError::NotSupported { + message: "Network tokenization for payment method".to_string(), + connector: "Adyen", + })? + } + } + } // }?; Ok(AdyenPaymentRequest { amount, @@ -2717,7 +2766,7 @@ impl mpi_data: None, telephone_number: None, shopper_name: None, - shopper_email, + shopper_email: None, shopper_locale: None, social_security_number: None, billing_address: None, @@ -2765,7 +2814,7 @@ impl let return_url = item.router_data.request.get_return_url()?; let card_holder_name = item.router_data.get_optional_billing_full_name(); let payment_method = AdyenPaymentMethod::try_from((card_data, card_holder_name))?; - let shopper_email = get_optional_shopper_email(item.router_data); + let shopper_email = item.router_data.get_optional_billing_email(); let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); Ok(AdyenPaymentRequest { @@ -2824,7 +2873,6 @@ impl let return_url = item.router_data.request.get_return_url()?; let payment_method = AdyenPaymentMethod::try_from((bank_debit_data, item.router_data))?; let country_code = get_country_code(item.router_data.get_optional_billing()); - let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -2838,7 +2886,7 @@ impl mpi_data: None, shopper_name: None, shopper_locale: None, - shopper_email, + shopper_email: item.router_data.get_optional_billing_email(), social_security_number: None, telephone_number: None, billing_address: None, @@ -2884,7 +2932,6 @@ impl let billing_address = get_address_info(item.router_data.get_optional_billing()).and_then(Result::ok); let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); - let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, @@ -2898,7 +2945,7 @@ impl additional_data, shopper_name, shopper_locale: None, - shopper_email, + shopper_email: item.router_data.get_optional_billing_email(), social_security_number, mpi_data: None, telephone_number: None, @@ -2938,7 +2985,6 @@ impl let shopper_interaction = AdyenShopperInteraction::from(item.router_data); let payment_method = AdyenPaymentMethod::try_from((bank_transfer_data, item.router_data))?; let return_url = item.router_data.request.get_return_url()?; - let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -2952,7 +2998,7 @@ impl mpi_data: None, shopper_name: None, shopper_locale: None, - shopper_email, + shopper_email: item.router_data.get_optional_billing_email(), social_security_number: None, telephone_number: None, billing_address: None, @@ -2991,7 +3037,6 @@ impl let shopper_interaction = AdyenShopperInteraction::from(item.router_data); let return_url = item.router_data.request.get_router_return_url()?; let payment_method = AdyenPaymentMethod::try_from(gift_card_data)?; - let shopper_email = get_optional_shopper_email(item.router_data); let request = AdyenPaymentRequest { amount, merchant_account: auth_type.merchant_account, @@ -3005,7 +3050,7 @@ impl mpi_data: None, shopper_name: None, shopper_locale: None, - shopper_email, + shopper_email: item.router_data.get_optional_billing_email(), telephone_number: None, billing_address: None, delivery_address: None, @@ -3055,7 +3100,6 @@ impl let line_items = Some(get_line_items(item)); let billing_address = get_address_info(item.router_data.get_optional_billing()).and_then(Result::ok); - let shopper_email = get_optional_shopper_email(item.router_data); Ok(AdyenPaymentRequest { amount, @@ -3070,7 +3114,7 @@ impl mpi_data: None, telephone_number: None, shopper_name: None, - shopper_email, + shopper_email: item.router_data.get_optional_billing_email(), shopper_locale, social_security_number: None, billing_address, @@ -3122,13 +3166,11 @@ fn get_shopper_email( .as_ref() .ok_or(errors::ConnectorError::MissingPaymentMethodType)?; match payment_method_type { - storage_enums::PaymentMethodType::Paypal => { - Ok(Some(item.get_billing_email().or(item.request.get_email())?)) - } - _ => Ok(get_optional_shopper_email(item)), + storage_enums::PaymentMethodType::Paypal => Ok(Some(item.get_billing_email()?)), + _ => Ok(item.get_optional_billing_email()), } } else { - Ok(get_optional_shopper_email(item)) + Ok(item.get_optional_billing_email()) } } @@ -3235,7 +3277,7 @@ impl get_recurring_processing_model(item.router_data)?; let return_url = item.router_data.request.get_return_url()?; let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); - let shopper_email = get_optional_shopper_email(item.router_data); + let shopper_email = item.router_data.get_optional_billing_email(); let billing_address = get_address_info(item.router_data.get_optional_billing()).and_then(Result::ok); let delivery_address = @@ -3303,7 +3345,7 @@ impl let shopper_interaction = AdyenShopperInteraction::from(item.router_data); let return_url = item.router_data.request.get_return_url()?; let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); - let shopper_email = get_optional_shopper_email(item.router_data); + let shopper_email = item.router_data.get_optional_billing_email(); let telephone_number = item .router_data .get_billing_phone() @@ -4881,7 +4923,8 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC })?, }; let bank_data = PayoutBankData { bank: bank_details }; - let address = item.router_data.get_billing_address()?; + let address: &hyperswitch_domain_models::address::AddressDetails = + item.router_data.get_billing_address()?; Ok(Self { amount: Amount { value: item.amount.to_owned(), @@ -4923,7 +4966,8 @@ impl TryFrom<&AdyenRouterData<&types::PayoutsRouterData>> for AdyenPayoutC })? } }; - let address = item.router_data.get_billing_address()?; + let address: &hyperswitch_domain_models::address::AddressDetails = + item.router_data.get_billing_address()?; let payout_wallet = PayoutWalletData { selected_brand: PayoutBrand::Paypal, additional_data, @@ -5350,3 +5394,97 @@ impl ForeignTryFrom<(&Self, AdyenDisputeResponse)> for types::DefendDisputeRoute } } } + +impl TryFrom<(&domain::NetworkTokenData, Option>)> for AdyenPaymentMethod<'_> { + type Error = Error; + fn try_from( + (token_data, card_holder_name): (&domain::NetworkTokenData, Option>), + ) -> Result { + let adyen_network_token = AdyenNetworkTokenData { + payment_type: PaymentType::NetworkToken, + number: token_data.token_number.clone(), + expiry_month: token_data.token_exp_month.clone(), + expiry_year: token_data.get_expiry_year_4_digit(), + holder_name: card_holder_name, + brand: None, // FIXME: Remove hardcoding + network_payment_reference: None, + }; + Ok(AdyenPaymentMethod::NetworkToken(Box::new( + adyen_network_token, + ))) + } +} + +impl + TryFrom<( + &AdyenRouterData<&types::PaymentsAuthorizeRouterData>, + &domain::NetworkTokenData, + )> for AdyenPaymentRequest<'_> +{ + type Error = Error; + fn try_from( + value: ( + &AdyenRouterData<&types::PaymentsAuthorizeRouterData>, + &domain::NetworkTokenData, + ), + ) -> Result { + let (item, token_data) = value; + let amount = get_amount_data(item); + let auth_type = AdyenAuthType::try_from(&item.router_data.connector_auth_type)?; + let shopper_interaction = AdyenShopperInteraction::from(item.router_data); + let shopper_reference = build_shopper_reference( + &item.router_data.customer_id, + item.router_data.merchant_id.clone(), + ); + let (recurring_processing_model, store_payment_method, _) = + get_recurring_processing_model(item.router_data)?; + let browser_info = get_browser_info(item.router_data)?; + let billing_address = + get_address_info(item.router_data.get_optional_billing()).transpose()?; + let country_code = get_country_code(item.router_data.get_optional_billing()); + let additional_data = get_additional_data(item.router_data); + let return_url = item.router_data.request.get_return_url()?; + let card_holder_name = item.router_data.get_optional_billing_full_name(); + let payment_method = AdyenPaymentMethod::try_from((token_data, card_holder_name))?; + let shopper_email = item.router_data.request.email.clone(); + let shopper_name = get_shopper_name(item.router_data.get_optional_billing()); + let mpi_data = AdyenMpiData { + directory_response: "Y".to_string(), + authentication_response: "Y".to_string(), + token_authentication_verification_value: token_data + .token_cryptogram + .clone() + .unwrap_or_default(), + eci: Some("02".to_string()), + }; + + Ok(AdyenPaymentRequest { + amount, + merchant_account: auth_type.merchant_account, + payment_method, + reference: item.router_data.connector_request_reference_id.clone(), + return_url, + shopper_interaction, + recurring_processing_model, + browser_info, + additional_data, + telephone_number: None, + shopper_name, + shopper_email, + shopper_locale: None, + social_security_number: None, + billing_address, + delivery_address: None, + country_code, + line_items: None, + shopper_reference, + store_payment_method, + channel: None, + shopper_statement: item.router_data.request.statement_descriptor.clone(), + shopper_ip: item.router_data.request.get_ip_address_as_optional(), + metadata: item.router_data.request.metadata.clone().map(Into::into), + merchant_order_reference: item.router_data.request.merchant_order_reference_id.clone(), + mpi_data: Some(mpi_data), + }) + } +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 6cca6e9001e9..8e6d74c83753 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1319,6 +1319,7 @@ impl ConnectorAuthTypeAndMetadataValidation<'_> { cryptopay::transformers::CryptopayAuthType::try_from(self.auth_type)?; Ok(()) } + api_enums::Connector::CtpMastercard => Ok(()), api_enums::Connector::Cybersource => { cybersource::transformers::CybersourceAuthType::try_from(self.auth_type)?; cybersource::transformers::CybersourceConnectorMetadataObject::try_from( diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 041fb52091ba..2d9a71a2584d 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -100,7 +100,6 @@ pub async fn retrieve_payment_method_core( business_profile, ) .await?; - Ok((pm_opt.to_owned(), payment_token)) } pm_opt @ Some(pm @ domain::PaymentMethodData::BankDebit(_)) => { @@ -127,6 +126,7 @@ pub async fn retrieve_payment_method_core( pm @ Some(domain::PaymentMethodData::GiftCard(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::OpenBanking(_)) => Ok((pm.to_owned(), None)), pm @ Some(domain::PaymentMethodData::MobilePayment(_)) => Ok((pm.to_owned(), None)), + pm @ Some(domain::PaymentMethodData::NetworkToken(_)) => Ok((pm.to_owned(), None)), pm_opt @ Some(pm @ domain::PaymentMethodData::BankTransfer(_)) => { let payment_token = payment_helpers::store_payment_method_data_in_vault( state, diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index de4524104788..cab515d9bad2 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -436,6 +436,7 @@ pub async fn get_token_from_tokenization_service( card_type: None, card_issuing_country: None, bank_code: None, + eci: None, }; Ok(network_token_data) } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e8a591437de9..58490271079f 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -386,6 +386,17 @@ where mandate_type, ) .await?; + operation + .to_domain()? + .call_unified_authentication_service_if_eligible( + state, + &mut payment_data, + &mut should_continue_transaction, + &connector_details, + &business_profile, + &key_store, + ) + .await?; operation .to_domain()? diff --git a/crates/router/src/core/payments/connector_integration_v2_impls.rs b/crates/router/src/core/payments/connector_integration_v2_impls.rs index c9da7bb1fd95..c96b8f915b59 100644 --- a/crates/router/src/core/payments/connector_integration_v2_impls.rs +++ b/crates/router/src/core/payments/connector_integration_v2_impls.rs @@ -1,3 +1,8 @@ +use hyperswitch_domain_models::router_flow_types::{PostAuthenticate, PreAuthenticate}; +use hyperswitch_interfaces::api::{ + UasPostAuthenticationV2, UasPreAuthenticationV2, UnifiedAuthenticationServiceV2, +}; + #[cfg(feature = "frm")] use crate::types::fraud_check as frm_types; use crate::{ @@ -1050,8 +1055,6 @@ default_imp_for_new_connector_integration_payouts!( connector::Airwallex, connector::Amazonpay, connector::Authorizedotnet, - connector::Bambora, - connector::Bamboraapac, connector::Bankofamerica, connector::Billwerk, connector::Bitpay, @@ -1122,7 +1125,8 @@ default_imp_for_new_connector_integration_payouts!( connector::Worldline, connector::Worldpay, connector::Zen, - connector::Plaid + connector::Plaid, + connector::CtpMastercard ); #[cfg(feature = "payouts")] @@ -1687,7 +1691,8 @@ default_imp_for_new_connector_integration_frm!( connector::Worldline, connector::Worldpay, connector::Zen, - connector::Plaid + connector::Plaid, + connector::CtpMastercard ); #[cfg(feature = "frm")] @@ -2052,8 +2057,6 @@ default_imp_for_new_connector_integration_connector_authentication!( connector::Aci, connector::Adyen, connector::Adyenplatform, - connector::Airwallex, - connector::Amazonpay, connector::Authorizedotnet, connector::Bambora, connector::Bamboraapac, @@ -2137,12 +2140,12 @@ default_imp_for_new_connector_integration_connector_authentication!( macro_rules! default_imp_for_new_connector_integration_uas { ($($path:ident::$connector:ident),*) => { - $( impl api::UnifiedAuthenticationServiceV2 for $path::$connector {} - impl api::UasPreAuthenticationV2 for $path::$connector {} - impl api::UasPostAuthenticationV2 for $path::$connector {} + $( impl UnifiedAuthenticationServiceV2 for $path::$connector {} + impl UasPreAuthenticationV2 for $path::$connector {} + impl UasPostAuthenticationV2 for $path::$connector {} impl services::ConnectorIntegrationV2< - api::PreAuthenticate, + PreAuthenticate, types::UasFlowData, types::UasPreAuthenticationRequestData, types::UasAuthenticationResponseData, @@ -2150,7 +2153,7 @@ macro_rules! default_imp_for_new_connector_integration_uas { {} impl services::ConnectorIntegrationV2< - api::PostAuthenticate, + PostAuthenticate, types::UasFlowData, types::UasPostAuthenticationRequestData, types::UasAuthenticationResponseData, @@ -2161,87 +2164,37 @@ macro_rules! default_imp_for_new_connector_integration_uas { } default_imp_for_new_connector_integration_uas!( + connector::Adyenplatform, connector::Aci, connector::Adyen, - connector::Adyenplatform, - connector::Airwallex, - connector::Amazonpay, connector::Authorizedotnet, - connector::Bambora, - connector::Bamboraapac, connector::Bankofamerica, - connector::Billwerk, - connector::Bitpay, - connector::Bluesnap, - connector::Boku, connector::Braintree, - connector::Cashtocode, connector::Checkout, - connector::Cryptopay, - connector::Coinbase, connector::Cybersource, - connector::Datatrans, - connector::Deutschebank, - connector::Digitalvirgo, - connector::Dlocal, connector::Ebanx, - connector::Elavon, - connector::Fiserv, - connector::Fiservemea, - connector::Forte, - connector::Fiuu, connector::Globalpay, - connector::Globepay, - connector::Gocardless, connector::Gpayments, - connector::Helcim, connector::Iatapay, - connector::Inespay, connector::Itaubank, - connector::Jpmorgan, connector::Klarna, connector::Mifinity, - connector::Mollie, - connector::Multisafepay, connector::Netcetera, - connector::Nexinets, - connector::Nexixpay, connector::Nmi, - connector::Nomupay, connector::Noon, - connector::Novalnet, connector::Nuvei, connector::Opayo, connector::Opennode, - connector::Paybox, - connector::Payeezy, connector::Payme, connector::Payone, connector::Paypal, - connector::Payu, - connector::Placetopay, - connector::Powertranz, - connector::Prophetpay, - connector::Rapyd, - connector::Razorpay, - connector::Redsys, + connector::Plaid, connector::Riskified, connector::Signifyd, - connector::Square, - connector::Stax, connector::Stripe, - connector::Shift4, - connector::Taxjar, - connector::Trustpay, connector::Threedsecureio, - connector::Thunes, - connector::Tsys, - connector::Volt, + connector::Trustpay, connector::Wellsfargo, - connector::Wise, - connector::Worldline, - connector::Worldpay, - connector::Zen, - connector::Zsl, - connector::Plaid + connector::Wellsfargopayout, + connector::Wise ); diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index ba1964435b80..9c9d0a454b3e 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -13,9 +13,13 @@ pub mod setup_mandate_flow; use async_trait::async_trait; use hyperswitch_domain_models::{ - mandates::CustomerAcceptance, router_request_types::PaymentsCaptureData, + mandates::CustomerAcceptance, + router_flow_types::{PostAuthenticate, PreAuthenticate}, + router_request_types::PaymentsCaptureData, +}; +use hyperswitch_interfaces::api::{ + payouts::Payouts, UasPostAuthentication, UasPreAuthentication, UnifiedAuthenticationService, }; -use hyperswitch_interfaces::api::payouts::Payouts; #[cfg(feature = "frm")] use crate::types::fraud_check as frm_types; @@ -504,7 +508,8 @@ default_imp_for_connector_request_id!( connector::Worldline, connector::Worldpay, connector::Zen, - connector::Zsl + connector::Zsl, + connector::CtpMastercard ); macro_rules! default_imp_for_accept_dispute { @@ -1640,7 +1645,8 @@ default_imp_for_fraud_check!( connector::Worldline, connector::Worldpay, connector::Zen, - connector::Zsl + connector::Zsl, + connector::CtpMastercard ); #[cfg(feature = "frm")] @@ -2243,7 +2249,8 @@ default_imp_for_connector_authentication!( connector::Worldline, connector::Worldpay, connector::Zen, - connector::Zsl + connector::Zsl, + connector::CtpMastercard ); macro_rules! default_imp_for_authorize_session_token { @@ -2488,11 +2495,11 @@ default_imp_for_post_session_tokens!( macro_rules! default_imp_for_uas_pre_authentication { ($($path:ident::$connector:ident),*) => { - $( impl api::UnifiedAuthenticationService for $path::$connector {} - impl api::UasPreAuthentication for $path::$connector {} + $( impl UnifiedAuthenticationService for $path::$connector {} + impl UasPreAuthentication for $path::$connector {} impl services::ConnectorIntegration< - api::PreAuthenticate, + PreAuthenticate, types::UasPreAuthenticationRequestData, types::UasAuthenticationResponseData > for $path::$connector @@ -2501,13 +2508,13 @@ macro_rules! default_imp_for_uas_pre_authentication { }; } #[cfg(feature = "dummy_connector")] -impl api::UasPreAuthentication for connector::DummyConnector {} +impl UasPreAuthentication for connector::DummyConnector {} #[cfg(feature = "dummy_connector")] -impl api::UnifiedAuthenticationService for connector::DummyConnector {} +impl UnifiedAuthenticationService for connector::DummyConnector {} #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< - api::PreAuthenticate, + PreAuthenticate, types::UasPreAuthenticationRequestData, types::UasAuthenticationResponseData, > for connector::DummyConnector @@ -2518,95 +2525,44 @@ default_imp_for_uas_pre_authentication!( connector::Adyenplatform, connector::Aci, connector::Adyen, - connector::Airwallex, - connector::Amazonpay, connector::Authorizedotnet, - connector::Bambora, - connector::Bamboraapac, connector::Bankofamerica, - connector::Billwerk, - connector::Bitpay, - connector::Bluesnap, - connector::Boku, connector::Braintree, - connector::Cashtocode, connector::Checkout, - connector::Cryptopay, - connector::Coinbase, connector::Cybersource, - connector::Datatrans, - connector::Deutschebank, - connector::Digitalvirgo, - connector::Dlocal, connector::Ebanx, - connector::Elavon, - connector::Fiserv, - connector::Fiservemea, - connector::Fiuu, - connector::Forte, connector::Globalpay, - connector::Globepay, - connector::Gocardless, connector::Gpayments, - connector::Helcim, connector::Iatapay, connector::Itaubank, - connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Netcetera, - connector::Mollie, - connector::Multisafepay, - connector::Nexinets, - connector::Nexixpay, connector::Nmi, - connector::Nomupay, connector::Noon, - connector::Novalnet, connector::Nuvei, connector::Opayo, connector::Opennode, - connector::Paybox, - connector::Payeezy, connector::Payme, connector::Payone, connector::Paypal, - connector::Payu, - connector::Placetopay, connector::Plaid, - connector::Powertranz, - connector::Prophetpay, - connector::Rapyd, - connector::Razorpay, connector::Riskified, - connector::Shift4, connector::Signifyd, - connector::Square, - connector::Stax, connector::Stripe, connector::Threedsecureio, - connector::Taxjar, connector::Trustpay, - connector::Tsys, - connector::Volt, connector::Wellsfargo, connector::Wellsfargopayout, - connector::Wise, - connector::Worldline, - connector::Worldpay, - connector::Zen, - connector::Zsl, - connector::Inespay, - connector::Redsys, - connector::UnifiedAuthenticationService + connector::Wise ); macro_rules! default_imp_for_uas_post_authentication { ($($path:ident::$connector:ident),*) => { - $( impl api::UasPostAuthentication for $path::$connector {} + $( impl UasPostAuthentication for $path::$connector {} impl services::ConnectorIntegration< - api::PostAuthenticate, + PostAuthenticate, types::UasPostAuthenticationRequestData, types::UasAuthenticationResponseData > for $path::$connector @@ -2615,11 +2571,11 @@ macro_rules! default_imp_for_uas_post_authentication { }; } #[cfg(feature = "dummy_connector")] -impl api::UasPostAuthentication for connector::DummyConnector {} +impl UasPostAuthentication for connector::DummyConnector {} #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< - api::PostAuthenticate, + PostAuthenticate, types::UasPostAuthenticationRequestData, types::UasAuthenticationResponseData, > for connector::DummyConnector @@ -2630,87 +2586,36 @@ default_imp_for_uas_post_authentication!( connector::Adyenplatform, connector::Aci, connector::Adyen, - connector::Airwallex, - connector::Amazonpay, connector::Authorizedotnet, - connector::Bambora, - connector::Bamboraapac, connector::Bankofamerica, - connector::Billwerk, - connector::Bitpay, - connector::Bluesnap, - connector::Boku, connector::Braintree, - connector::Cashtocode, connector::Checkout, - connector::Cryptopay, - connector::Coinbase, connector::Cybersource, - connector::Datatrans, - connector::Deutschebank, - connector::Digitalvirgo, - connector::Dlocal, connector::Ebanx, - connector::Elavon, - connector::Fiserv, - connector::Fiservemea, - connector::Fiuu, - connector::Forte, connector::Globalpay, - connector::Globepay, connector::Gpayments, - connector::Gocardless, - connector::Helcim, connector::Iatapay, connector::Itaubank, - connector::Jpmorgan, connector::Klarna, connector::Mifinity, connector::Netcetera, - connector::Mollie, - connector::Multisafepay, - connector::Nexinets, - connector::Nexixpay, connector::Nmi, - connector::Nomupay, connector::Noon, - connector::Novalnet, connector::Nuvei, connector::Opayo, connector::Opennode, - connector::Paybox, - connector::Payeezy, connector::Payme, connector::Payone, connector::Paypal, - connector::Payu, - connector::Placetopay, connector::Plaid, - connector::Powertranz, - connector::Prophetpay, - connector::Rapyd, - connector::Razorpay, connector::Riskified, - connector::Shift4, connector::Signifyd, - connector::Square, - connector::Stax, connector::Stripe, connector::Threedsecureio, - connector::Taxjar, connector::Trustpay, - connector::Tsys, - connector::Volt, connector::Wellsfargo, connector::Wellsfargopayout, - connector::Wise, - connector::Worldline, - connector::Worldpay, - connector::Zen, - connector::Zsl, - connector::Inespay, - connector::Redsys, - connector::UnifiedAuthenticationService + connector::Wise ); /// Determines whether a capture API call should be made for a payment attempt /// This function evaluates whether an authorized payment should proceed with a capture API call diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index ba14457e023c..84256f1b296d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2088,6 +2088,7 @@ pub async fn retrieve_card_with_permanent_token( card_type: None, card_issuing_country: None, bank_code: None, + eci: None, }; Ok(domain::PaymentMethodData::NetworkToken(network_token_data)) } else { @@ -2648,13 +2649,15 @@ pub(crate) fn validate_payment_method_fields_present( req.payment_method.is_some() && payment_method_data.is_none() && req.payment_token.is_none() - && req.recurring_details.is_none(), + && req.recurring_details.is_none() + && req.ctp_service_details.is_none(), || { Err(errors::ApiErrorResponse::MissingRequiredField { field_name: "payment_method_data", }) }, )?; + utils::when( req.payment_method.is_some() && req.payment_method_type.is_some(), || { @@ -3313,6 +3316,7 @@ pub(crate) fn validate_pm_or_token_given( payment_method_type: &Option, mandate_type: &Option, token: &Option, + ctp_service_details: &Option, ) -> Result<(), errors::ApiErrorResponse> { utils::when( !matches!( @@ -3322,10 +3326,13 @@ pub(crate) fn validate_pm_or_token_given( mandate_type, Some(api::MandateTransactionType::RecurringMandateTransaction) ) && token.is_none() - && (payment_method_data.is_none() || payment_method.is_none()), + && (payment_method_data.is_none() || payment_method.is_none()) + && ctp_service_details.is_none(), || { Err(errors::ApiErrorResponse::InvalidRequestData { - message: "A payment token or payment method data is required".to_string(), + message: + "A payment token or payment method data or ctp service details is required" + .to_string(), }) }, ) 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 09e806755ff0..914ddf251efe 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -171,6 +171,7 @@ impl GetTracker, api::PaymentsRequest> &request.payment_method_type, &mandate_type, &token, + &request.ctp_service_details, )?; } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 58217ef3f41a..aa551ce6fff6 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -16,6 +16,7 @@ use error_stack::{report, ResultExt}; use futures::FutureExt; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use hyperswitch_domain_models::payments::payment_intent::PaymentIntentUpdateFields; +use hyperswitch_domain_models::router_request_types::unified_authentication_service; use masking::{ExposeInterface, PeekInterface}; use router_derive::PaymentOperation; use router_env::{instrument, logger, tracing}; @@ -39,7 +40,7 @@ use crate::{ PaymentData, }, unified_authentication_service::{ - self, + self as uas_utils, types::{ClickToPay, UnifiedAuthenticationService, CTP_MASTERCARD}, }, utils as core_utils, @@ -597,6 +598,7 @@ impl GetTracker, api::PaymentsRequest> &request.payment_method_type, &mandate_type, &token, + &request.ctp_service_details, )?; let (token_data, payment_method_info) = if let Some(token) = token.clone() { @@ -778,6 +780,7 @@ impl GetTracker, api::PaymentsRequest> )), // connector_mandate_request_reference_id )), ); + let payment_data = PaymentData { flow: PhantomData, payment_intent, @@ -818,7 +821,7 @@ impl GetTracker, api::PaymentsRequest> poll_config: None, tax_data: None, session_id: None, - service_details: None, + service_details: request.ctp_service_details.clone(), }; let get_trackers_response = operations::GetTrackerResponse { @@ -883,7 +886,6 @@ impl Domain> for business_profile, )) .await?; - utils::when(payment_method_data.is_none(), || { Err(errors::ApiErrorResponse::PaymentMethodNotFound) })?; @@ -1044,10 +1046,10 @@ impl Domain> for business_profile: &domain::Profile, key_store: &domain::MerchantKeyStore, ) -> CustomResult<(), errors::ApiErrorResponse> { - let is_click_to_pay_enabled = true; // fetch from business profile - if let Some(payment_method) = payment_data.payment_attempt.payment_method { - if payment_method == storage_enums::PaymentMethod::Card && is_click_to_pay_enabled { + if payment_method == storage_enums::PaymentMethod::Card + && business_profile.is_click_to_pay_enabled + { let connector_name = CTP_MASTERCARD; // since the above checks satisfies the connector should be click to pay hence hardcoded the connector name let connector_mca = helpers::get_merchant_connector_account( state, @@ -1091,7 +1093,7 @@ impl Domain> for payment_data.payment_attempt.authentication_id = Some(authentication_id.clone()); - let network_token = ClickToPay::post_authentication( + let response = ClickToPay::post_authentication( state, key_store, business_profile, @@ -1102,10 +1104,43 @@ impl Domain> for ) .await?; - payment_data.payment_method_data = - network_token.map(domain::PaymentMethodData::NetworkToken); + let (network_token, authentication_status) = match response.response.clone() { + Ok(unified_authentication_service::UasAuthenticationResponseData::PostAuthentication { + authentication_details, + }) => { + (Some( + hyperswitch_domain_models::payment_method_data::NetworkTokenData { + token_number: authentication_details.token_details.payment_token, + token_exp_month: authentication_details + .token_details + .token_expiration_month, + token_exp_year: authentication_details + .token_details + .token_expiration_year, + token_cryptogram: authentication_details + .dynamic_data_details + .and_then(|data| data.dynamic_data_value), + card_issuer: None, + card_network: None, + card_type: None, + card_issuing_country: None, + bank_code: None, + nick_name: None, + eci: authentication_details.eci, + }),common_enums::AuthenticationStatus::Success) + }, + Ok(unified_authentication_service::UasAuthenticationResponseData::PreAuthentication {}) => (None, common_enums::AuthenticationStatus::Started), + Err(_) => (None, common_enums::AuthenticationStatus::Failed) + }; + + payment_data.payment_attempt.payment_method = + Some(common_enums::PaymentMethod::Card); + + payment_data.payment_method_data = network_token + .clone() + .map(domain::PaymentMethodData::NetworkToken); - unified_authentication_service::create_new_authentication( + uas_utils::create_new_authentication( state, payment_data.payment_attempt.merchant_id.clone(), connector_name.to_string(), @@ -1113,6 +1148,8 @@ impl Domain> for Some(payment_data.payment_intent.get_id().clone()), connector_transaction_id, &authentication_id, + payment_data.service_details.clone(), + authentication_status, ) .await?; } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 6a676af47b72..a316d6edc372 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1012,6 +1012,7 @@ impl ValidateRequest GetTracker, api::PaymentsRequest> &request.payment_method_type, &mandate_type, &token, + &request.ctp_service_details, )?; } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 92486320cdee..4f24ec98881f 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -2589,6 +2589,7 @@ impl TryFrom> for types::PaymentsAuthoriz None } }); + let amount = payment_data.payment_attempt.get_total_amount(); let customer_name = additional_data diff --git a/crates/router/src/core/unified_authentication_service.rs b/crates/router/src/core/unified_authentication_service.rs index 63375419cf75..5ed1b63d721f 100644 --- a/crates/router/src/core/unified_authentication_service.rs +++ b/crates/router/src/core/unified_authentication_service.rs @@ -2,13 +2,13 @@ pub mod transformers; pub mod types; pub mod utils; +use api_models::payments::CtpServiceDetails; use diesel_models::authentication::{Authentication, AuthenticationNew}; use error_stack::ResultExt; use hyperswitch_domain_models::{ errors::api_error_response::ApiErrorResponse, router_request_types::unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, + UasPostAuthenticationRequestData, UasPreAuthenticationRequestData, }, }; @@ -23,7 +23,6 @@ use crate::{ }, db::domain, routes::SessionState, - types::api, }; #[cfg(feature = "v1")] @@ -42,7 +41,8 @@ impl UnifiedAuthenticationService for ClickToPay { let pre_authentication_data = UasPreAuthenticationRequestData::try_from(payment_data.clone())?; - let pre_auth_router_data: api::unified_authentication_service::UasPreAuthenticationRouterData = utils::construct_uas_router_data( + let pre_auth_router_data: hyperswitch_domain_models::types::UasPreAuthenticationRouterData = + utils::construct_uas_router_data( connector_name.to_string(), payment_method, payment_data.payment_attempt.merchant_id.clone(), @@ -70,9 +70,7 @@ impl UnifiedAuthenticationService for ClickToPay { merchant_connector_account: &MerchantConnectorAccountType, connector_name: &str, payment_method: common_enums::PaymentMethod, - ) -> RouterResult> - { - let post_authentication_data = UasPostAuthenticationRequestData; + ) -> RouterResult { let authentication_id = payment_data .payment_attempt .authentication_id @@ -80,7 +78,9 @@ impl UnifiedAuthenticationService for ClickToPay { .ok_or(ApiErrorResponse::InternalServerError) .attach_printable("Missing authentication id in payment attempt")?; - let post_auth_router_data: api::unified_authentication_service::UasPostAuthenticationRouterData = utils::construct_uas_router_data( + let post_authentication_data = UasPostAuthenticationRequestData {}; + + let post_auth_router_data: hyperswitch_domain_models::types::UasPostAuthenticationRouterData = utils::construct_uas_router_data( connector_name.to_string(), payment_method, payment_data.payment_attempt.merchant_id.clone(), @@ -97,27 +97,7 @@ impl UnifiedAuthenticationService for ClickToPay { ) .await?; - let network_token = match response.response.clone() { - Ok(UasAuthenticationResponseData::PostAuthentication { - authentication_details, - }) => Some( - hyperswitch_domain_models::payment_method_data::NetworkTokenData { - token_number: authentication_details.token_details.payment_token, - token_exp_month: authentication_details.token_details.token_expiration_month, - token_exp_year: authentication_details.token_details.token_expiration_year, - token_cryptogram: None, - card_issuer: None, - card_network: None, - card_type: None, - card_issuing_country: None, - bank_code: None, - nick_name: None, - }, - ), - _ => None, - }; - - Ok(network_token) + Ok(response) } fn confirmation( @@ -130,6 +110,7 @@ impl UnifiedAuthenticationService for ClickToPay { } } +#[allow(clippy::too_many_arguments)] pub async fn create_new_authentication( state: &SessionState, merchant_id: common_utils::id_type::MerchantId, @@ -138,7 +119,16 @@ pub async fn create_new_authentication( payment_id: Option, merchant_connector_id: common_utils::id_type::MerchantConnectorAccountId, authentication_id: &str, + service_details: Option, + authentication_status: common_enums::AuthenticationStatus, ) -> RouterResult { + let service_details_value = service_details + .map(serde_json::to_value) + .transpose() + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to parse service details into json value while inserting to DB", + )?; let new_authorization = AuthenticationNew { authentication_id: authentication_id.to_owned(), merchant_id, @@ -146,7 +136,7 @@ pub async fn create_new_authentication( connector_authentication_id: None, payment_method_id: "".to_string(), authentication_type: None, - authentication_status: common_enums::AuthenticationStatus::Started, + authentication_status, authentication_lifecycle_status: common_enums::AuthenticationLifecycleStatus::Unused, error_message: None, error_code: None, @@ -173,7 +163,7 @@ pub async fn create_new_authentication( ds_trans_id: None, directory_server_id: None, acquirer_country_code: None, - service_details: None, + service_details: service_details_value, }; state .store diff --git a/crates/router/src/core/unified_authentication_service/transformers.rs b/crates/router/src/core/unified_authentication_service/transformers.rs index 0dd59fc81c76..36b4ad7e4203 100644 --- a/crates/router/src/core/unified_authentication_service/transformers.rs +++ b/crates/router/src/core/unified_authentication_service/transformers.rs @@ -14,9 +14,18 @@ impl TryFrom> for UasPreAuthenticationRequestDat fn try_from(payment_data: PaymentData) -> Result { let service_details = CtpServiceDetails { service_session_ids: Some(ServiceSessionIds { - merchant_transaction_id: None, - correlation_id: None, - x_src_flow_id: None, + merchant_transaction_id: payment_data + .service_details + .as_ref() + .and_then(|details| details.merchant_transaction_id.clone()), + correlation_id: payment_data + .service_details + .as_ref() + .and_then(|details| details.correlation_id.clone()), + x_src_flow_id: payment_data + .service_details + .as_ref() + .and_then(|details| details.x_src_flow_id.clone()), }), }; let currency = payment_data.payment_attempt.currency.ok_or( diff --git a/crates/router/src/core/unified_authentication_service/types.rs b/crates/router/src/core/unified_authentication_service/types.rs index 6fc73ec6cd2b..db0251768cf0 100644 --- a/crates/router/src/core/unified_authentication_service/types.rs +++ b/crates/router/src/core/unified_authentication_service/types.rs @@ -41,7 +41,7 @@ pub trait UnifiedAuthenticationService { _merchant_connector_account: &MerchantConnectorAccountType, _connector_name: &str, _payment_method: common_enums::PaymentMethod, - ) -> RouterResult>; + ) -> RouterResult; fn confirmation( _state: &SessionState, diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index f542eece81f0..c39ead5fbce8 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -36,9 +36,6 @@ pub mod payments_v2; pub mod payouts_v2; pub mod refunds_v2; -pub mod unified_authentication_service; -pub mod unified_authentication_service_v2; - use std::{fmt::Debug, str::FromStr}; use api_models::routing::{self as api_routing, RoutableConnectorChoice}; @@ -53,6 +50,7 @@ pub use hyperswitch_interfaces::api::{ ConnectorMandateRevoke, ConnectorMandateRevokeV2, ConnectorVerifyWebhookSource, ConnectorVerifyWebhookSourceV2, CurrencyUnit, }; +use hyperswitch_interfaces::api::{UnifiedAuthenticationService, UnifiedAuthenticationServiceV2}; #[cfg(feature = "frm")] pub use self::fraud_check::*; @@ -61,7 +59,7 @@ pub use self::payouts::*; pub use self::{ admin::*, api_keys::*, authentication::*, configs::*, customers::*, disputes::*, files::*, payment_link::*, payment_methods::*, payments::*, poll::*, refunds::*, refunds_v2::*, - unified_authentication_service::*, webhooks::*, + webhooks::*, }; use super::transformers::ForeignTryFrom; use crate::{ @@ -373,6 +371,9 @@ impl ConnectorData { enums::Connector::Cryptopay => { Ok(ConnectorEnum::Old(Box::new(connector::Cryptopay::new()))) } + enums::Connector::CtpMastercard => { + Ok(ConnectorEnum::Old(Box::new(&connector::CtpMastercard))) + } enums::Connector::Cybersource => { Ok(ConnectorEnum::Old(Box::new(connector::Cybersource::new()))) } diff --git a/crates/router/src/types/api/authentication.rs b/crates/router/src/types/api/authentication.rs index 7d35943dd793..e82ee116709c 100644 --- a/crates/router/src/types/api/authentication.rs +++ b/crates/router/src/types/api/authentication.rs @@ -150,6 +150,12 @@ impl AuthenticationConnectorData { enums::AuthenticationConnectors::Gpayments => { Ok(ConnectorEnum::Old(Box::new(connector::Gpayments::new()))) } + enums::AuthenticationConnectors::CtpMastercard => { + Ok(ConnectorEnum::Old(Box::new(&connector::CtpMastercard))) + } + enums::AuthenticationConnectors::UnifiedAuthenticationService => Ok( + ConnectorEnum::Old(Box::new(connector::UnifiedAuthenticationService::new())), + ), } } } diff --git a/crates/router/src/types/api/unified_authentication_service.rs b/crates/router/src/types/api/unified_authentication_service.rs deleted file mode 100644 index 97f698856fcc..000000000000 --- a/crates/router/src/types/api/unified_authentication_service.rs +++ /dev/null @@ -1,59 +0,0 @@ -use hyperswitch_domain_models::{ - router_data::RouterData, - router_request_types::unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, - }, -}; - -pub use super::unified_authentication_service_v2::{ - UasPostAuthenticationV2, UasPreAuthenticationV2, UnifiedAuthenticationServiceV2, -}; -use crate::services; - -#[derive(Debug, Clone)] -pub struct PreAuthenticate; - -pub trait UnifiedAuthenticationService: - super::ConnectorCommon + UasPreAuthentication + UasPostAuthentication -{ -} - -#[derive(Debug, Clone)] -pub struct PostAuthenticate; - -pub trait UasPreAuthentication: - services::ConnectorIntegration< - PreAuthenticate, - UasPreAuthenticationRequestData, - UasAuthenticationResponseData, -> -{ -} - -pub trait UasPostAuthentication: - services::ConnectorIntegration< - PostAuthenticate, - UasPostAuthenticationRequestData, - UasAuthenticationResponseData, -> -{ -} - -pub type UasPostAuthenticationRouterData = - RouterData; - -pub type UasPostAuthenticationType = dyn services::ConnectorIntegration< - PostAuthenticate, - UasPostAuthenticationRequestData, - UasAuthenticationResponseData, ->; - -pub type UasPreAuthenticationRouterData = - RouterData; - -pub type UasPreAuthenticationType = dyn services::ConnectorIntegration< - PreAuthenticate, - UasPreAuthenticationRequestData, - UasAuthenticationResponseData, ->; diff --git a/crates/router/src/types/api/unified_authentication_service_v2.rs b/crates/router/src/types/api/unified_authentication_service_v2.rs deleted file mode 100644 index 3de12b0bb9e5..000000000000 --- a/crates/router/src/types/api/unified_authentication_service_v2.rs +++ /dev/null @@ -1,35 +0,0 @@ -use hyperswitch_domain_models::{ - router_data_v2::UasFlowData, - router_request_types::unified_authentication_service::{ - UasAuthenticationResponseData, UasPostAuthenticationRequestData, - UasPreAuthenticationRequestData, - }, -}; - -use super::unified_authentication_service::{PostAuthenticate, PreAuthenticate}; -use crate::services; - -pub trait UnifiedAuthenticationServiceV2: - super::ConnectorCommon + UasPreAuthenticationV2 + UasPostAuthenticationV2 -{ -} - -pub trait UasPreAuthenticationV2: - services::ConnectorIntegrationV2< - PreAuthenticate, - UasFlowData, - UasPreAuthenticationRequestData, - UasAuthenticationResponseData, -> -{ -} - -pub trait UasPostAuthenticationV2: - services::ConnectorIntegrationV2< - PostAuthenticate, - UasFlowData, - UasPostAuthenticationRequestData, - UasAuthenticationResponseData, -> -{ -} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 269eb831fc69..d4cd57754362 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -228,6 +228,11 @@ impl ForeignTryFrom for common_enums::RoutableConnectors { api_enums::Connector::Checkout => Self::Checkout, api_enums::Connector::Coinbase => Self::Coinbase, api_enums::Connector::Cryptopay => Self::Cryptopay, + api_enums::Connector::CtpMastercard => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "ctp mastercard is not a routable connector".to_string(), + })? + } api_enums::Connector::Cybersource => Self::Cybersource, api_enums::Connector::Datatrans => Self::Datatrans, api_enums::Connector::Deutschebank => Self::Deutschebank, diff --git a/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js b/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js index d445dbd26cd9..228dfd2cfdda 100644 --- a/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js +++ b/cypress-tests-v2/cypress/e2e/configs/Payment/Commons.js @@ -1199,7 +1199,7 @@ export const connectorDetails = { body: { error: { type: "invalid_request", - message: "A payment token or payment method data is required", + message: "A payment token or payment method data or ctp service details is required", code: "IR_06", }, }, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js index 961762f2ddff..86e41c076391 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Commons.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Commons.js @@ -1392,7 +1392,8 @@ export const connectorDetails = { body: { error: { type: "invalid_request", - message: "A payment token or payment method data is required", + message: + "A payment token or payment method data or ctp service details is required", code: "IR_06", }, }, diff --git a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js index 7a4a47f9c312..2b85b7e5cd99 100644 --- a/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js +++ b/cypress-tests/cypress/e2e/PaymentUtils/Paybox.js @@ -856,7 +856,8 @@ export const connectorDetails = { body: { error: { type: "invalid_request", - message: "A payment token or payment method data is required", + message: + "A payment token or payment method data or ctp service details is required", code: "IR_06", }, }, diff --git a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js index e03c186a82e3..6ca7b398c74c 100644 --- a/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js +++ b/postman/collection-dir/checkout/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js @@ -81,13 +81,13 @@ if (jsonData?.error?.type) { ); } -// Response body should have value "A payment token or payment method data is required" for "message" +// Response body should have value "A payment token or payment method data or ctp service details is required" for "message" if (jsonData?.error?.message) { pm.test( "[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'", function () { pm.expect(jsonData.error.message).to.eql( - "A payment token or payment method data is required", + "A payment token or payment method data or ctp service details is required", ); }, ); diff --git a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js index e03c186a82e3..6ca7b398c74c 100644 --- a/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js +++ b/postman/collection-dir/paypal/Flow Testcases/Variation Cases/Scenario2-Confirming the payment without PMD/Payments - Confirm/event.test.js @@ -81,13 +81,13 @@ if (jsonData?.error?.type) { ); } -// Response body should have value "A payment token or payment method data is required" for "message" +// Response body should have value "A payment token or payment method data or ctp service details is required" for "message" if (jsonData?.error?.message) { pm.test( "[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'", function () { pm.expect(jsonData.error.message).to.eql( - "A payment token or payment method data is required", + "A payment token or payment method data or ctp service details is required", ); }, ); diff --git a/postman/collection-json/checkout.postman_collection.json b/postman/collection-json/checkout.postman_collection.json index f01d5d2fe758..33bfcef8aa90 100644 --- a/postman/collection-json/checkout.postman_collection.json +++ b/postman/collection-json/checkout.postman_collection.json @@ -12186,13 +12186,13 @@ " );", "}", "", - "// Response body should have value \"A payment token or payment method data is required\" for \"message\"", + "// Response body should have value \"A payment token or payment method data or ctp service details is required\" for \"message\"", "if (jsonData?.error?.message) {", " pm.test(", " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"A payment token or payment method data is required\",", + " \"A payment token or payment method data or ctp service details is required\",", " );", " },", " );", diff --git a/postman/collection-json/paypal.postman_collection.json b/postman/collection-json/paypal.postman_collection.json index b4309b2b4a00..50abbd7dd8f2 100644 --- a/postman/collection-json/paypal.postman_collection.json +++ b/postman/collection-json/paypal.postman_collection.json @@ -6137,13 +6137,13 @@ " );", "}", "", - "// Response body should have value \"A payment token or payment method data is required\" for \"message\"", + "// Response body should have value \"A payment token or payment method data or ctp service details is required\" for \"message\"", "if (jsonData?.error?.message) {", " pm.test(", " \"[POST]::/payments - Content check if value for 'error.message' matches 'connector_error'\",", " function () {", " pm.expect(jsonData.error.message).to.eql(", - " \"A payment token or payment method data is required\",", + " \"A payment token or payment method data or ctp service details is required\",", " );", " },", " );", From 3d4fd2f719b38dcbb675de83c0ba384d1573df00 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Mon, 16 Dec 2024 19:24:50 +0530 Subject: [PATCH 10/20] fix(router): change click_to_pay const to snake_case and remove camel_case serde rename for clicktopay metadata (#6852) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference-v2/openapi_spec.json | 36 +++++++++---------- api-reference/openapi_spec.json | 36 +++++++++---------- crates/api_models/src/payments.rs | 1 - .../hyperswitch_domain_models/src/payments.rs | 1 - crates/router/src/consts.rs | 2 +- 5 files changed, 37 insertions(+), 39 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 4725e0552f00..252174d11ff3 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -6235,50 +6235,50 @@ "ClickToPaySessionResponse": { "type": "object", "required": [ - "dpaId", - "dpaName", + "dpa_id", + "dpa_name", "locale", - "cardBrands", - "acquirerBin", - "acquirerMerchantId", - "merchantCategoryCode", - "merchantCountryCode", - "transactionAmount", - "transactionCurrencyCode" + "card_brands", + "acquirer_bin", + "acquirer_merchant_id", + "merchant_category_code", + "merchant_country_code", + "transaction_amount", + "transaction_currency_code" ], "properties": { - "dpaId": { + "dpa_id": { "type": "string" }, - "dpaName": { + "dpa_name": { "type": "string" }, "locale": { "type": "string" }, - "cardBrands": { + "card_brands": { "type": "array", "items": { "type": "string" } }, - "acquirerBin": { + "acquirer_bin": { "type": "string" }, - "acquirerMerchantId": { + "acquirer_merchant_id": { "type": "string" }, - "merchantCategoryCode": { + "merchant_category_code": { "type": "string" }, - "merchantCountryCode": { + "merchant_country_code": { "type": "string" }, - "transactionAmount": { + "transaction_amount": { "type": "string", "example": "38.02" }, - "transactionCurrencyCode": { + "transaction_currency_code": { "$ref": "#/components/schemas/Currency" } } diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 0c8736ce85e3..6b6147aa90f7 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -8679,50 +8679,50 @@ "ClickToPaySessionResponse": { "type": "object", "required": [ - "dpaId", - "dpaName", + "dpa_id", + "dpa_name", "locale", - "cardBrands", - "acquirerBin", - "acquirerMerchantId", - "merchantCategoryCode", - "merchantCountryCode", - "transactionAmount", - "transactionCurrencyCode" + "card_brands", + "acquirer_bin", + "acquirer_merchant_id", + "merchant_category_code", + "merchant_country_code", + "transaction_amount", + "transaction_currency_code" ], "properties": { - "dpaId": { + "dpa_id": { "type": "string" }, - "dpaName": { + "dpa_name": { "type": "string" }, "locale": { "type": "string" }, - "cardBrands": { + "card_brands": { "type": "array", "items": { "type": "string" } }, - "acquirerBin": { + "acquirer_bin": { "type": "string" }, - "acquirerMerchantId": { + "acquirer_merchant_id": { "type": "string" }, - "merchantCategoryCode": { + "merchant_category_code": { "type": "string" }, - "merchantCountryCode": { + "merchant_country_code": { "type": "string" }, - "transactionAmount": { + "transaction_amount": { "type": "string", "example": "38.02" }, - "transactionCurrencyCode": { + "transaction_currency_code": { "$ref": "#/components/schemas/Currency" } } diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 2464cdf62dbc..2f08ba830fc2 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -6984,7 +6984,6 @@ pub struct ExtendedCardInfoResponse { } #[derive(Debug, Clone, Eq, PartialEq, serde::Serialize, ToSchema)] -#[serde(rename_all = "camelCase")] pub struct ClickToPaySessionResponse { pub dpa_id: String, pub dpa_name: String, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 075c94c2eae1..9488a762a666 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -521,7 +521,6 @@ pub struct HeaderPayload { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] pub struct ClickToPayMetaData { pub dpa_id: String, pub dpa_name: String, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index 6d017d7ed04a..823f0a4bf04f 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -209,4 +209,4 @@ pub const VAULT_GET_FINGERPRINT_FLOW_TYPE: &str = "get_fingerprint_vault"; pub const DYNAMIC_ROUTING_MAX_VOLUME: u8 = 100; /// Click To Pay -pub const CLICK_TO_PAY: &str = "CLICK_TO_PAY"; +pub const CLICK_TO_PAY: &str = "click_to_pay"; From 886e2aacd7dcd8a04a33884ceafb1562aa7c365f Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 00:22:18 +0000 Subject: [PATCH 11/20] chore(version): 2024.12.17.0 --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37e49d94784a..11293a2f7e90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,36 @@ All notable changes to HyperSwitch will be documented here. - - - +## 2024.12.17.0 + +### Features + +- **connector:** + - [AIRWALLEX] Add refferer data to whitelist hyperswitch ([#6806](https://github.com/juspay/hyperswitch/pull/6806)) ([`ed276ec`](https://github.com/juspay/hyperswitch/commit/ed276ecc0017f7f98b6f8fa3841e6b8971f609f1)) + - [Adyen ] Add fixes for AdyenPaymentRequest struct ([#6803](https://github.com/juspay/hyperswitch/pull/6803)) ([`c22be0c`](https://github.com/juspay/hyperswitch/commit/c22be0c9274350a531cd74b64eb6b311579dca79)) +- **core:** Add click to pay support in hyperswitch ([#6769](https://github.com/juspay/hyperswitch/pull/6769)) ([`165ead6`](https://github.com/juspay/hyperswitch/commit/165ead61084a48f268829c281e932b278f0a6730)) +- **payments:** Add audit events for PaymentStatus update ([#6520](https://github.com/juspay/hyperswitch/pull/6520)) ([`ae00a10`](https://github.com/juspay/hyperswitch/commit/ae00a103de5bd283695969270a421c7609a699e8)) +- **users:** Incorporate themes in user APIs ([#6772](https://github.com/juspay/hyperswitch/pull/6772)) ([`4b989fe`](https://github.com/juspay/hyperswitch/commit/4b989fe0fb7931479e127fecbaace42d989c0620)) + +### Bug Fixes + +- **router:** + - Handle default case for card_network for co-badged cards ([#6825](https://github.com/juspay/hyperswitch/pull/6825)) ([`f95ee51`](https://github.com/juspay/hyperswitch/commit/f95ee51bb3b879762d493953b4b6e7c2e0359946)) + - Change click_to_pay const to snake_case and remove camel_case serde rename for clicktopay metadata ([#6852](https://github.com/juspay/hyperswitch/pull/6852)) ([`3d4fd2f`](https://github.com/juspay/hyperswitch/commit/3d4fd2f719b38dcbb675de83c0ba384d1573df00)) +- **user_roles:** Migrations for backfilling user_roles entity_id ([#6837](https://github.com/juspay/hyperswitch/pull/6837)) ([`986de77`](https://github.com/juspay/hyperswitch/commit/986de77b4868e48d00161c9d30071d809360e9a6)) + +### Refactors + +- **authz:** Make connector list accessible by operation groups ([#6792](https://github.com/juspay/hyperswitch/pull/6792)) ([`6081283`](https://github.com/juspay/hyperswitch/commit/6081283afc5ab5a6503c8f0f81181cd323b12297)) + +### Miscellaneous Tasks + +- **deps:** Update scylla driver ([#6799](https://github.com/juspay/hyperswitch/pull/6799)) ([`71574a8`](https://github.com/juspay/hyperswitch/commit/71574a85e6aba6bc614e1d7f6775dcef4b481201)) + +**Full Changelog:** [`2024.12.16.0...2024.12.17.0`](https://github.com/juspay/hyperswitch/compare/2024.12.16.0...2024.12.17.0) + +- - - + ## 2024.12.16.0 ### Features From 94ad90f9ed8b2d8a0e4715875f3fdccf2abec15d Mon Sep 17 00:00:00 2001 From: Amisha Prabhat <55580080+Aprabhat19@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:52:31 +0530 Subject: [PATCH 12/20] feat(redis-interface): add redis interface command to set multiple the keys in redis and increment if the key already exists (#6827) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Nishant Joshi --- crates/redis_interface/src/commands.rs | 60 +++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/redis_interface/src/commands.rs b/crates/redis_interface/src/commands.rs index 746b424abc88..19497d6fbb83 100644 --- a/crates/redis_interface/src/commands.rs +++ b/crates/redis_interface/src/commands.rs @@ -14,7 +14,7 @@ use common_utils::{ use error_stack::{report, ResultExt}; use fred::{ interfaces::{HashesInterface, KeysInterface, ListInterface, SetsInterface, StreamsInterface}, - prelude::RedisErrorKind, + prelude::{LuaInterface, RedisErrorKind}, types::{ Expiration, FromRedis, MultipleIDs, MultipleKeys, MultipleOrderedPairs, MultipleStrings, MultipleValues, RedisKey, RedisMap, RedisValue, ScanType, Scanner, SetOptions, XCap, @@ -852,12 +852,31 @@ impl super::RedisConnectionPool { .await .change_context(errors::RedisError::ConsumerGroupClaimFailed) } + + #[instrument(level = "DEBUG", skip(self))] + pub async fn incr_keys_using_script( + &self, + lua_script: &'static str, + key: Vec, + values: V, + ) -> CustomResult<(), errors::RedisError> + where + V: TryInto + Debug + Send + Sync, + V::Error: Into + Send + Sync, + { + self.pool + .eval(lua_script, key, values) + .await + .change_context(errors::RedisError::IncrementHashFieldFailed) + } } #[cfg(test)] mod tests { #![allow(clippy::expect_used, clippy::unwrap_used)] + use std::collections::HashMap; + use crate::{errors::RedisError, RedisConnectionPool, RedisEntryId, RedisSettings}; #[tokio::test] @@ -911,7 +930,6 @@ mod tests { assert!(is_success); } - #[tokio::test] async fn test_delete_non_existing_key_success() { let is_success = tokio::task::spawn_blocking(move || { @@ -930,6 +948,44 @@ mod tests { }) .await .expect("Spawn block failure"); + assert!(is_success); + } + + #[tokio::test] + async fn test_setting_keys_using_scripts() { + let is_success = tokio::task::spawn_blocking(move || { + futures::executor::block_on(async { + // Arrange + let pool = RedisConnectionPool::new(&RedisSettings::default()) + .await + .expect("failed to create redis connection pool"); + let lua_script = r#" + local results = {} + for i = 1, #KEYS do + results[i] = redis.call("INCRBY", KEYS[i], ARGV[i]) + end + return results + "#; + let mut keys_and_values = HashMap::new(); + for i in 0..10 { + keys_and_values.insert(format!("key{}", i), i); + } + + let key = keys_and_values.keys().cloned().collect::>(); + let values = keys_and_values + .values() + .map(|val| val.to_string()) + .collect::>(); + + // Act + let result = pool.incr_keys_using_script(lua_script, key, values).await; + + // Assert Setup + result.is_ok() + }) + }) + .await + .expect("Spawn block failure"); assert!(is_success); } From c883aa59aae4ddbcf8c754052ed60b4514043d47 Mon Sep 17 00:00:00 2001 From: Uzair Khan Date: Tue, 17 Dec 2024 15:53:00 +0530 Subject: [PATCH 13/20] feat(analytics): Analytics Request Validator and config driven forex feature (#6733) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> --- config/config.example.toml | 1 + config/deployments/env_specific.toml | 1 + config/development.toml | 1 + crates/analytics/docs/README.md | 6 + crates/analytics/src/lib.rs | 58 +++++- crates/analytics/src/payment_intents/core.rs | 170 ++++++++++-------- crates/analytics/src/payments/core.rs | 86 +++++---- crates/analytics/src/refunds/core.rs | 43 +++-- crates/api_models/src/analytics.rs | 37 +++- crates/api_models/src/analytics/disputes.rs | 10 +- .../src/analytics/payment_intents.rs | 13 +- crates/api_models/src/analytics/payments.rs | 13 +- crates/api_models/src/analytics/refunds.rs | 10 +- crates/router/src/analytics.rs | 116 ++++++++++-- crates/router/src/analytics_validator.rs | 24 +++ crates/router/src/lib.rs | 1 + 16 files changed, 429 insertions(+), 161 deletions(-) create mode 100644 crates/router/src/analytics_validator.rs diff --git a/config/config.example.toml b/config/config.example.toml index 0fe6b8caa2c7..5c55d57cf77b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -661,6 +661,7 @@ pm_auth_key = "Some_pm_auth_key" # Analytics configuration. [analytics] source = "sqlx" # The Analytics source/strategy to be used +forex_enabled = false # Enable or disable forex conversion for analytics [analytics.clickhouse] username = "" # Clickhouse username diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 848a2305ae46..02d3fe66c09e 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -9,6 +9,7 @@ database_name = "clickhouse_db_name" # Clickhouse database name # Analytics configuration. [analytics] source = "sqlx" # The Analytics source/strategy to be used +forex_enabled = false # Boolean to enable or disable forex conversion [analytics.sqlx] username = "db_user" # Analytics DB Username diff --git a/config/development.toml b/config/development.toml index 077e09f04635..fd84d626d377 100644 --- a/config/development.toml +++ b/config/development.toml @@ -719,6 +719,7 @@ authentication_analytics_topic = "hyperswitch-authentication-events" [analytics] source = "sqlx" +forex_enabled = false [analytics.clickhouse] username = "default" diff --git a/crates/analytics/docs/README.md b/crates/analytics/docs/README.md index eb5c26a6ba3f..e24dc6c5af79 100644 --- a/crates/analytics/docs/README.md +++ b/crates/analytics/docs/README.md @@ -104,6 +104,12 @@ To use Forex services, you need to sign up and get your API keys from the follow - It will be in dashboard, labeled as `access key`. ### Configuring Forex APIs +To enable Forex functionality, update the `config/development.toml` or `config/docker_compose.toml` file: + +```toml +[analytics] +forex_enabled = true # default set to false +``` To configure the Forex APIs, update the `config/development.toml` or `config/docker_compose.toml` file with your API keys: diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 6d694caadf5f..0ad8886b8208 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -969,21 +969,25 @@ impl AnalyticsProvider { tenant: &dyn storage_impl::config::TenantConfig, ) -> Self { match config { - AnalyticsConfig::Sqlx { sqlx } => { + AnalyticsConfig::Sqlx { sqlx, .. } => { Self::Sqlx(SqlxClient::from_conf(sqlx, tenant.get_schema()).await) } - AnalyticsConfig::Clickhouse { clickhouse } => Self::Clickhouse(ClickhouseClient { + AnalyticsConfig::Clickhouse { clickhouse, .. } => Self::Clickhouse(ClickhouseClient { config: Arc::new(clickhouse.clone()), database: tenant.get_clickhouse_database().to_string(), }), - AnalyticsConfig::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh( + AnalyticsConfig::CombinedCkh { + sqlx, clickhouse, .. + } => Self::CombinedCkh( SqlxClient::from_conf(sqlx, tenant.get_schema()).await, ClickhouseClient { config: Arc::new(clickhouse.clone()), database: tenant.get_clickhouse_database().to_string(), }, ), - AnalyticsConfig::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx( + AnalyticsConfig::CombinedSqlx { + sqlx, clickhouse, .. + } => Self::CombinedSqlx( SqlxClient::from_conf(sqlx, tenant.get_schema()).await, ClickhouseClient { config: Arc::new(clickhouse.clone()), @@ -1000,20 +1004,35 @@ impl AnalyticsProvider { pub enum AnalyticsConfig { Sqlx { sqlx: Database, + forex_enabled: bool, }, Clickhouse { clickhouse: ClickhouseConfig, + forex_enabled: bool, }, CombinedCkh { sqlx: Database, clickhouse: ClickhouseConfig, + forex_enabled: bool, }, CombinedSqlx { sqlx: Database, clickhouse: ClickhouseConfig, + forex_enabled: bool, }, } +impl AnalyticsConfig { + pub fn get_forex_enabled(&self) -> bool { + match self { + Self::Sqlx { forex_enabled, .. } + | Self::Clickhouse { forex_enabled, .. } + | Self::CombinedCkh { forex_enabled, .. } + | Self::CombinedSqlx { forex_enabled, .. } => *forex_enabled, + } + } +} + #[async_trait::async_trait] impl SecretsHandler for AnalyticsConfig { async fn convert_to_raw_secret( @@ -1024,7 +1043,7 @@ impl SecretsHandler for AnalyticsConfig { let decrypted_password = match analytics_config { // Todo: Perform kms decryption of clickhouse password Self::Clickhouse { .. } => masking::Secret::new(String::default()), - Self::Sqlx { sqlx } + Self::Sqlx { sqlx, .. } | Self::CombinedCkh { sqlx, .. } | Self::CombinedSqlx { sqlx, .. } => { secret_management_client @@ -1034,26 +1053,46 @@ impl SecretsHandler for AnalyticsConfig { }; Ok(value.transition_state(|conf| match conf { - Self::Sqlx { sqlx } => Self::Sqlx { + Self::Sqlx { + sqlx, + forex_enabled, + } => Self::Sqlx { sqlx: Database { password: decrypted_password, ..sqlx }, + forex_enabled, }, - Self::Clickhouse { clickhouse } => Self::Clickhouse { clickhouse }, - Self::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh { + Self::Clickhouse { + clickhouse, + forex_enabled, + } => Self::Clickhouse { + clickhouse, + forex_enabled, + }, + Self::CombinedCkh { + sqlx, + clickhouse, + forex_enabled, + } => Self::CombinedCkh { sqlx: Database { password: decrypted_password, ..sqlx }, clickhouse, + forex_enabled, }, - Self::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx { + Self::CombinedSqlx { + sqlx, + clickhouse, + forex_enabled, + } => Self::CombinedSqlx { sqlx: Database { password: decrypted_password, ..sqlx }, clickhouse, + forex_enabled, }, })) } @@ -1063,6 +1102,7 @@ impl Default for AnalyticsConfig { fn default() -> Self { Self::Sqlx { sqlx: Database::default(), + forex_enabled: false, } } } diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs index 00a59d82870c..0a512c419ec0 100644 --- a/crates/analytics/src/payment_intents/core.rs +++ b/crates/analytics/src/payment_intents/core.rs @@ -68,7 +68,7 @@ pub async fn get_sankey( #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, - ex_rates: &ExchangeRates, + ex_rates: &Option, auth: &AuthInfo, req: GetPaymentIntentMetricRequest, ) -> AnalyticsResult> { @@ -201,22 +201,25 @@ pub async fn get_metrics( total += total_count; } if let Some(retried_amount) = collected_values.smart_retried_amount { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(retried_amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + id.currency + .and_then(|currency| { + i64::try_from(retried_amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default() + } else { + None + }; collected_values.smart_retried_amount_in_usd = amount_in_usd; total_smart_retried_amount += retried_amount; total_smart_retried_amount_in_usd += amount_in_usd.unwrap_or(0); @@ -224,44 +227,50 @@ pub async fn get_metrics( if let Some(retried_amount) = collected_values.smart_retried_amount_without_smart_retries { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(retried_amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + id.currency + .and_then(|currency| { + i64::try_from(retried_amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default() + } else { + None + }; collected_values.smart_retried_amount_without_smart_retries_in_usd = amount_in_usd; total_smart_retried_amount_without_smart_retries += retried_amount; total_smart_retried_amount_without_smart_retries_in_usd += amount_in_usd.unwrap_or(0); } if let Some(amount) = collected_values.payment_processed_amount { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + id.currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default() + } else { + None + }; collected_values.payment_processed_amount_in_usd = amount_in_usd; total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0); total_payment_processed_amount += amount; @@ -270,22 +279,25 @@ pub async fn get_metrics( total_payment_processed_count += count; } if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + id.currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default() + } else { + None + }; collected_values.payment_processed_amount_without_smart_retries_in_usd = amount_in_usd; total_payment_processed_amount_without_smart_retries_in_usd += @@ -322,14 +334,26 @@ pub async fn get_metrics( total_payment_processed_amount_without_smart_retries: Some( total_payment_processed_amount_without_smart_retries, ), - total_smart_retried_amount_in_usd: Some(total_smart_retried_amount_in_usd), - total_smart_retried_amount_without_smart_retries_in_usd: Some( - total_smart_retried_amount_without_smart_retries_in_usd, - ), - total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd), - total_payment_processed_amount_without_smart_retries_in_usd: Some( - total_payment_processed_amount_without_smart_retries_in_usd, - ), + total_smart_retried_amount_in_usd: if ex_rates.is_some() { + Some(total_smart_retried_amount_in_usd) + } else { + None + }, + total_smart_retried_amount_without_smart_retries_in_usd: if ex_rates.is_some() { + Some(total_smart_retried_amount_without_smart_retries_in_usd) + } else { + None + }, + total_payment_processed_amount_in_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_in_usd) + } else { + None + }, + total_payment_processed_amount_without_smart_retries_in_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_without_smart_retries_in_usd) + } else { + None + }, total_payment_processed_count: Some(total_payment_processed_count), total_payment_processed_count_without_smart_retries: Some( total_payment_processed_count_without_smart_retries, diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs index 86192265d072..7291d2f1fcc7 100644 --- a/crates/analytics/src/payments/core.rs +++ b/crates/analytics/src/payments/core.rs @@ -48,7 +48,7 @@ pub enum TaskType { #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, - ex_rates: &ExchangeRates, + ex_rates: &Option, auth: &AuthInfo, req: GetPaymentMetricRequest, ) -> AnalyticsResult> { @@ -234,22 +234,25 @@ pub async fn get_metrics( .map(|(id, val)| { let mut collected_values = val.collect(); if let Some(amount) = collected_values.payment_processed_amount { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + id.currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default() + } else { + None + }; collected_values.payment_processed_amount_in_usd = amount_in_usd; total_payment_processed_amount += amount; total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0); @@ -258,22 +261,25 @@ pub async fn get_metrics( total_payment_processed_count += count; } if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + id.currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default() + } else { + None + }; collected_values.payment_processed_amount_without_smart_retries_usd = amount_in_usd; total_payment_processed_amount_without_smart_retries += amount; total_payment_processed_amount_without_smart_retries_usd += @@ -298,13 +304,19 @@ pub async fn get_metrics( query_data, meta_data: [PaymentsAnalyticsMetadata { total_payment_processed_amount: Some(total_payment_processed_amount), - total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd), + total_payment_processed_amount_in_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_in_usd) + } else { + None + }, total_payment_processed_amount_without_smart_retries: Some( total_payment_processed_amount_without_smart_retries, ), - total_payment_processed_amount_without_smart_retries_usd: Some( - total_payment_processed_amount_without_smart_retries_usd, - ), + total_payment_processed_amount_without_smart_retries_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_without_smart_retries_usd) + } else { + None + }, total_payment_processed_count: Some(total_payment_processed_count), total_payment_processed_count_without_smart_retries: Some( total_payment_processed_count_without_smart_retries, diff --git a/crates/analytics/src/refunds/core.rs b/crates/analytics/src/refunds/core.rs index 04badd06bd2d..15ae14206ccf 100644 --- a/crates/analytics/src/refunds/core.rs +++ b/crates/analytics/src/refunds/core.rs @@ -47,7 +47,7 @@ pub enum TaskType { pub async fn get_metrics( pool: &AnalyticsProvider, - ex_rates: &ExchangeRates, + ex_rates: &Option, auth: &AuthInfo, req: GetRefundMetricRequest, ) -> AnalyticsResult> { @@ -217,22 +217,25 @@ pub async fn get_metrics( total += total_count; } if let Some(amount) = collected_values.refund_processed_amount { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + id.currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default() + } else { + None + }; collected_values.refund_processed_amount_in_usd = amount_in_usd; total_refund_processed_amount += amount; total_refund_processed_amount_in_usd += amount_in_usd.unwrap_or(0); @@ -261,7 +264,11 @@ pub async fn get_metrics( meta_data: [RefundsAnalyticsMetadata { total_refund_success_rate, total_refund_processed_amount: Some(total_refund_processed_amount), - total_refund_processed_amount_in_usd: Some(total_refund_processed_amount_in_usd), + total_refund_processed_amount_in_usd: if ex_rates.is_some() { + Some(total_refund_processed_amount_in_usd) + } else { + None + }, total_refund_processed_count: Some(total_refund_processed_count), total_refund_reason_count: Some(total_refund_reason_count), total_refund_error_message_count: Some(total_refund_error_message_count), diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 7f58c6b00d78..132272f0e496 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -62,7 +62,42 @@ pub enum Granularity { #[serde(rename = "G_ONEDAY")] OneDay, } - +pub trait ForexMetric { + fn is_forex_metric(&self) -> bool; +} + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AnalyticsRequest { + pub payment_intent: Option, + pub payment_attempt: Option, + pub refund: Option, + pub dispute: Option, +} + +impl AnalyticsRequest { + pub fn requires_forex_functionality(&self) -> bool { + self.payment_attempt + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + || self + .payment_intent + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + || self + .refund + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + || self + .dispute + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + } +} #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentMetricRequest { diff --git a/crates/api_models/src/analytics/disputes.rs b/crates/api_models/src/analytics/disputes.rs index 2509d83e1320..e373704b87c5 100644 --- a/crates/api_models/src/analytics/disputes.rs +++ b/crates/api_models/src/analytics/disputes.rs @@ -3,7 +3,7 @@ use std::{ hash::{Hash, Hasher}, }; -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; use crate::enums::DisputeStage; #[derive( @@ -28,6 +28,14 @@ pub enum DisputeMetrics { SessionizedTotalAmountDisputed, SessionizedTotalDisputeLostAmount, } +impl ForexMetric for DisputeMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::TotalAmountDisputed | Self::TotalDisputeLostAmount + ) + } +} #[derive( Debug, diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs index 365abd71edc2..15c107dfb66c 100644 --- a/crates/api_models/src/analytics/payment_intents.rs +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -5,7 +5,7 @@ use std::{ use common_utils::id_type; -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; use crate::enums::{ AuthenticationType, Connector, Currency, IntentStatus, PaymentMethod, PaymentMethodType, }; @@ -106,6 +106,17 @@ pub enum PaymentIntentMetrics { SessionizedPaymentProcessedAmount, SessionizedPaymentsDistribution, } +impl ForexMetric for PaymentIntentMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::PaymentProcessedAmount + | Self::SmartRetriedAmount + | Self::SessionizedPaymentProcessedAmount + | Self::SessionizedSmartRetriedAmount + ) + } +} #[derive(Debug, Default, serde::Serialize)] pub struct ErrorResult { diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index 8fd24f151248..691e827043f1 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -5,7 +5,7 @@ use std::{ use common_utils::id_type; -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; use crate::enums::{ AttemptStatus, AuthenticationType, CardNetwork, Connector, Currency, PaymentMethod, PaymentMethodType, @@ -119,6 +119,17 @@ pub enum PaymentMetrics { FailureReasons, } +impl ForexMetric for PaymentMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::PaymentProcessedAmount + | Self::AvgTicketSize + | Self::SessionizedPaymentProcessedAmount + | Self::SessionizedAvgTicketSize + ) + } +} #[derive(Debug, Default, serde::Serialize)] pub struct ErrorResult { pub reason: String, diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index 0afca6c1ef64..84954e70910f 100644 --- a/crates/api_models/src/analytics/refunds.rs +++ b/crates/api_models/src/analytics/refunds.rs @@ -30,7 +30,7 @@ pub enum RefundType { RetryRefund, } -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct RefundFilters { #[serde(default)] @@ -137,6 +137,14 @@ pub enum RefundDistributions { #[strum(serialize = "refund_error_message")] SessionizedRefundErrorMessage, } +impl ForexMetric for RefundMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::RefundProcessedAmount | Self::SessionizedRefundProcessedAmount + ) + } +} pub mod metric_behaviour { pub struct RefundSuccessRate; diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index ff2744b824ed..8fe30aa59d4a 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -18,12 +18,12 @@ pub mod routes { search::{ GetGlobalSearchRequest, GetSearchRequest, GetSearchRequestWithIndex, SearchIndex, }, - GenerateReportRequest, GetActivePaymentsMetricRequest, GetApiEventFiltersRequest, - GetApiEventMetricRequest, GetAuthEventMetricRequest, GetDisputeMetricRequest, - GetFrmFilterRequest, GetFrmMetricRequest, GetPaymentFiltersRequest, - GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, GetPaymentMetricRequest, - GetRefundFilterRequest, GetRefundMetricRequest, GetSdkEventFiltersRequest, - GetSdkEventMetricRequest, ReportRequest, + AnalyticsRequest, GenerateReportRequest, GetActivePaymentsMetricRequest, + GetApiEventFiltersRequest, GetApiEventMetricRequest, GetAuthEventMetricRequest, + GetDisputeMetricRequest, GetFrmFilterRequest, GetFrmMetricRequest, + GetPaymentFiltersRequest, GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, + GetPaymentMetricRequest, GetRefundFilterRequest, GetRefundMetricRequest, + GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, }; use common_enums::EntityType; use common_utils::types::TimeRange; @@ -31,11 +31,9 @@ pub mod routes { use futures::{stream::FuturesUnordered, StreamExt}; use crate::{ + analytics_validator::request_validator, consts::opensearch::SEARCH_INDEXES, - core::{ - api_locking, currency::get_forex_exchange_rates, errors::user::UserErrors, - verification::utils, - }, + core::{api_locking, errors::user::UserErrors, verification::utils}, db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload}, routes::AppState, services::{ @@ -405,7 +403,15 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + let validator_response = request_validator( + AnalyticsRequest { + payment_attempt: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -444,7 +450,16 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_attempt: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -491,7 +506,16 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_attempt: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -532,7 +556,16 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_intent: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -571,7 +604,16 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_intent: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -618,7 +660,16 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_intent: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -659,7 +710,16 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + refund: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -698,7 +758,16 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + refund: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -745,7 +814,16 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + refund: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) diff --git a/crates/router/src/analytics_validator.rs b/crates/router/src/analytics_validator.rs new file mode 100644 index 000000000000..d50308cc848c --- /dev/null +++ b/crates/router/src/analytics_validator.rs @@ -0,0 +1,24 @@ +use analytics::errors::AnalyticsError; +use api_models::analytics::AnalyticsRequest; +use common_utils::errors::CustomResult; +use currency_conversion::types::ExchangeRates; +use router_env::logger; + +use crate::core::currency::get_forex_exchange_rates; + +pub async fn request_validator( + req_type: AnalyticsRequest, + state: &crate::routes::SessionState, +) -> CustomResult, AnalyticsError> { + let forex_enabled = state.conf.analytics.get_inner().get_forex_enabled(); + let require_forex_functionality = req_type.requires_forex_functionality(); + + let ex_rates = if forex_enabled && require_forex_functionality { + logger::info!("Fetching forex exchange rates"); + Some(get_forex_exchange_rates(state.clone()).await?) + } else { + None + }; + + Ok(ex_rates) +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 839dd472423a..d43fa0543138 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -16,6 +16,7 @@ pub mod workflows; #[cfg(feature = "olap")] pub mod analytics; +pub mod analytics_validator; pub mod events; pub mod middleware; pub mod services; From a056dc72db23200c473e8aa2ec8ce5579fa4f6c6 Mon Sep 17 00:00:00 2001 From: ShivanshMathurJuspay <104988143+ShivanshMathurJuspay@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:53:30 +0530 Subject: [PATCH 14/20] chore(analytics): SDK table schema changes (#6579) Co-authored-by: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> --- .../docs/clickhouse/scripts/sdk_events.sql | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/analytics/docs/clickhouse/scripts/sdk_events.sql b/crates/analytics/docs/clickhouse/scripts/sdk_events.sql index bfe6401cacc4..c5c70cc9e662 100644 --- a/crates/analytics/docs/clickhouse/scripts/sdk_events.sql +++ b/crates/analytics/docs/clickhouse/scripts/sdk_events.sql @@ -6,7 +6,7 @@ CREATE TABLE sdk_events_queue ( `event_name` LowCardinality(Nullable(String)), `first_event` LowCardinality(Nullable(String)), `latency` Nullable(UInt32), - `timestamp` String, + `timestamp` DateTime64(3), `browser_name` LowCardinality(Nullable(String)), `browser_version` Nullable(String), `platform` LowCardinality(Nullable(String)), @@ -115,7 +115,8 @@ CREATE TABLE sdk_events_audit ( `remote_ip` Nullable(String), `log_type` LowCardinality(Nullable(String)), `event_name` LowCardinality(Nullable(String)), - `first_event` Bool DEFAULT 1, + `first_event` Bool, + `latency` Nullable(UInt32), `browser_name` LowCardinality(Nullable(String)), `browser_version` Nullable(String), `platform` LowCardinality(Nullable(String)), @@ -125,10 +126,10 @@ CREATE TABLE sdk_events_audit ( `value` Nullable(String), `component` LowCardinality(Nullable(String)), `payment_method` LowCardinality(Nullable(String)), - `payment_experience` LowCardinality(Nullable(String)) DEFAULT '', - `created_at` DateTime64(3) DEFAULT now64() CODEC(T64, LZ4), - `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4), - `latency` Nullable(UInt32) DEFAULT 0 + `payment_experience` LowCardinality(Nullable(String)), + `created_at` DateTime DEFAULT now() CODEC(T64, LZ4), + `created_at_precise` DateTime64(3), + `inserted_at` DateTime DEFAULT now() CODEC(T64, LZ4) ) ENGINE = MergeTree PARTITION BY merchant_id ORDER BY (merchant_id, payment_id) @@ -156,7 +157,7 @@ WHERE length(_error) > 0; CREATE MATERIALIZED VIEW sdk_events_audit_mv TO sdk_events_audit ( - `payment_id` Nullable(String), + `payment_id` String, `merchant_id` String, `remote_ip` Nullable(String), `log_type` LowCardinality(Nullable(String)), From 75563721fe3a602eacf394f5b5ccfbb52a70cae1 Mon Sep 17 00:00:00 2001 From: Jeeva Ramachandran <120017870+JeevaRamu0104@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:53:52 +0530 Subject: [PATCH 15/20] ci: add workflow to check wasm-pack build (#6842) --- .github/workflows/wasm-bulild-check.yml | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/wasm-bulild-check.yml diff --git a/.github/workflows/wasm-bulild-check.yml b/.github/workflows/wasm-bulild-check.yml new file mode 100644 index 000000000000..d9b2860a361e --- /dev/null +++ b/.github/workflows/wasm-bulild-check.yml @@ -0,0 +1,34 @@ +name: CI wasm-check + +on: + push: + branches: + - main + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + wasm-ci: + name: Check wasm build + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: stable 2 weeks ago + + - name: Install wasm-pack + uses: taiki-e/install-action@v2 + with: + tool: wasm-pack + checksum: true + + - name: wasm build + shell: bash + run: make euclid-wasm From 588ce408b4b04bdd89f2594239e7efc9e0f66114 Mon Sep 17 00:00:00 2001 From: Swangi Kumari <85639103+swangi-kumari@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:54:05 +0530 Subject: [PATCH 16/20] fix(connector): 5xx error for Volt Payment Sync (#6846) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/connectors/volt/transformers.rs | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs b/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs index 61cf91c87cb8..a0d349eaa984 100644 --- a/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/volt/transformers.rs @@ -235,22 +235,27 @@ impl TryFrom<&ConnectorAuthType> for VoltAuthType { } } -impl From for enums::AttemptStatus { - fn from(item: VoltPaymentStatus) -> Self { - match item { - VoltPaymentStatus::Received | VoltPaymentStatus::Settled => Self::Charged, - VoltPaymentStatus::Completed | VoltPaymentStatus::DelayedAtBank => Self::Pending, - VoltPaymentStatus::NewPayment - | VoltPaymentStatus::BankRedirect - | VoltPaymentStatus::AwaitingCheckoutAuthorisation => Self::AuthenticationPending, - VoltPaymentStatus::RefusedByBank - | VoltPaymentStatus::RefusedByRisk - | VoltPaymentStatus::NotReceived - | VoltPaymentStatus::ErrorAtBank - | VoltPaymentStatus::CancelledByUser - | VoltPaymentStatus::AbandonedByUser - | VoltPaymentStatus::Failed => Self::Failure, +fn get_attempt_status( + (item, current_status): (VoltPaymentStatus, enums::AttemptStatus), +) -> enums::AttemptStatus { + match item { + VoltPaymentStatus::Received | VoltPaymentStatus::Settled => enums::AttemptStatus::Charged, + VoltPaymentStatus::Completed | VoltPaymentStatus::DelayedAtBank => { + enums::AttemptStatus::Pending } + VoltPaymentStatus::NewPayment + | VoltPaymentStatus::BankRedirect + | VoltPaymentStatus::AwaitingCheckoutAuthorisation => { + enums::AttemptStatus::AuthenticationPending + } + VoltPaymentStatus::RefusedByBank + | VoltPaymentStatus::RefusedByRisk + | VoltPaymentStatus::NotReceived + | VoltPaymentStatus::ErrorAtBank + | VoltPaymentStatus::CancelledByUser + | VoltPaymentStatus::AbandonedByUser + | VoltPaymentStatus::Failed => enums::AttemptStatus::Failure, + VoltPaymentStatus::Unknown => current_status, } } @@ -309,6 +314,7 @@ pub enum VoltPaymentStatus { AbandonedByUser, Failed, Settled, + Unknown, } #[derive(Debug, Serialize, Deserialize)] @@ -335,7 +341,8 @@ impl TryFrom Result { match item.response { VoltPaymentsResponseData::PsyncResponse(payment_response) => { - let status = enums::AttemptStatus::from(payment_response.status.clone()); + let status = + get_attempt_status((payment_response.status.clone(), item.data.status)); Ok(Self { status, response: if is_payment_failure(status) { From dfbfce4e4247166e43f1a805e65331b21eab4e09 Mon Sep 17 00:00:00 2001 From: Sanchith Hegde <22217505+SanchithHegde@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:54:27 +0530 Subject: [PATCH 17/20] refactor(customers_v2): address panics and some bugs in customers v2 endpoints (#6836) --- .../customers/customers--create.mdx | 2 +- .../customers/customers--delete.mdx | 2 +- .../customers/customers--list.mdx | 2 +- .../customers/customers--retrieve.mdx | 2 +- .../customers/customers--update.mdx | 4 +- api-reference-v2/openapi_spec.json | 143 ++++++++--- api-reference/openapi_spec.json | 56 ++++- crates/api_models/src/customers.rs | 102 +++----- crates/api_models/src/ephemeral_key.rs | 19 ++ crates/api_models/src/events/customer.rs | 43 +--- crates/api_models/src/payment_methods.rs | 66 +++-- crates/api_models/src/payments.rs | 18 +- crates/common_utils/src/events.rs | 2 +- crates/common_utils/src/id_type.rs | 38 +-- crates/common_utils/src/id_type/customer.rs | 9 + crates/common_utils/src/id_type/global_id.rs | 7 +- .../src/id_type/global_id/customer.rs | 45 ++++ .../src/id_type/global_id/payment.rs | 8 +- crates/diesel_models/src/customers.rs | 4 +- crates/diesel_models/src/kv.rs | 2 +- crates/diesel_models/src/payment_intent.rs | 4 +- crates/diesel_models/src/payment_method.rs | 4 +- crates/diesel_models/src/query/customers.rs | 7 +- crates/diesel_models/src/query/mandate.rs | 19 +- .../diesel_models/src/query/payment_method.rs | 29 ++- .../hyperswitch_domain_models/src/customer.rs | 16 +- .../src/payment_methods.rs | 2 +- .../hyperswitch_domain_models/src/payments.rs | 2 +- crates/openapi/src/openapi.rs | 1 + crates/openapi/src/openapi_v2.rs | 1 + crates/openapi/src/routes/customers.rs | 4 +- .../src/compatibility/stripe/customers.rs | 39 +-- crates/router/src/core/customers.rs | 123 ++++----- crates/router/src/core/mandate.rs | 12 +- crates/router/src/core/payment_methods.rs | 26 +- .../router/src/core/payment_methods/cards.rs | 32 +-- .../payment_methods/network_tokenization.rs | 3 + crates/router/src/core/payments.rs | 1 + crates/router/src/core/payments/helpers.rs | 1 + .../operations/payment_confirm_intent.rs | 2 +- .../operations/payment_create_intent.rs | 2 +- .../core/payments/operations/payment_get.rs | 2 +- .../operations/payment_session_intent.rs | 2 +- .../router/src/core/payments/transformers.rs | 8 +- crates/router/src/core/payouts.rs | 4 +- crates/router/src/db/customers.rs | 48 ++-- crates/router/src/db/kafka_store.rs | 48 +++- crates/router/src/db/mandate.rs | 18 +- crates/router/src/db/payment_method.rs | 237 ++++++++++++++++-- crates/router/src/routes/app.rs | 4 +- crates/router/src/routes/customers.rs | 83 +++--- crates/router/src/routes/ephemeral_key.rs | 7 +- crates/router/src/routes/payment_methods.rs | 4 +- crates/router/src/routes/payments.rs | 2 +- crates/router/src/types/api/customers.rs | 8 +- crates/storage_impl/src/lib.rs | 2 +- 56 files changed, 906 insertions(+), 475 deletions(-) create mode 100644 crates/common_utils/src/id_type/global_id/customer.rs diff --git a/api-reference-v2/api-reference/customers/customers--create.mdx b/api-reference-v2/api-reference/customers/customers--create.mdx index c53a4cd62097..1d517ca26fc8 100644 --- a/api-reference-v2/api-reference/customers/customers--create.mdx +++ b/api-reference-v2/api-reference/customers/customers--create.mdx @@ -1,3 +1,3 @@ --- openapi: post /v2/customers ---- \ No newline at end of file +--- diff --git a/api-reference-v2/api-reference/customers/customers--delete.mdx b/api-reference-v2/api-reference/customers/customers--delete.mdx index eee5cffd5972..5d642f125f34 100644 --- a/api-reference-v2/api-reference/customers/customers--delete.mdx +++ b/api-reference-v2/api-reference/customers/customers--delete.mdx @@ -1,3 +1,3 @@ --- openapi: delete /v2/customers/{id} ---- \ No newline at end of file +--- diff --git a/api-reference-v2/api-reference/customers/customers--list.mdx b/api-reference-v2/api-reference/customers/customers--list.mdx index 432370e119a8..ae0d884e308e 100644 --- a/api-reference-v2/api-reference/customers/customers--list.mdx +++ b/api-reference-v2/api-reference/customers/customers--list.mdx @@ -1,3 +1,3 @@ --- openapi: get /v2/customers/list ---- \ No newline at end of file +--- diff --git a/api-reference-v2/api-reference/customers/customers--retrieve.mdx b/api-reference-v2/api-reference/customers/customers--retrieve.mdx index e89fe53d42a6..16f8a062553c 100644 --- a/api-reference-v2/api-reference/customers/customers--retrieve.mdx +++ b/api-reference-v2/api-reference/customers/customers--retrieve.mdx @@ -1,3 +1,3 @@ --- openapi: get /v2/customers/{id} ---- \ No newline at end of file +--- diff --git a/api-reference-v2/api-reference/customers/customers--update.mdx b/api-reference-v2/api-reference/customers/customers--update.mdx index 65433d8fad47..8d4bed86c97a 100644 --- a/api-reference-v2/api-reference/customers/customers--update.mdx +++ b/api-reference-v2/api-reference/customers/customers--update.mdx @@ -1,3 +1,3 @@ --- -openapi: post /v2/customers/{id} ---- \ No newline at end of file +openapi: put /v2/customers/{id} +--- diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 252174d11ff3..58a81caeed3a 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -1712,7 +1712,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerRequest" + "$ref": "#/components/schemas/CustomerUpdateRequest" }, "examples": { "Update name and email of a customer": { @@ -7253,13 +7253,20 @@ "CustomerDeleteResponse": { "type": "object", "required": [ + "id", "merchant_reference_id", "customer_deleted", "address_deleted", - "payment_methods_deleted", - "id" + "payment_methods_deleted" ], "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 + }, "merchant_reference_id": { "type": "string", "description": "The identifier for the customer object", @@ -7280,10 +7287,6 @@ "type": "boolean", "description": "Whether payment methods deleted or not", "example": false - }, - "id": { - "type": "string", - "description": "Global id" } } }, @@ -7399,9 +7402,9 @@ "customer_id": { "type": "string", "description": "The unique identifier of the customer.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", "maxLength": 64, - "minLength": 1 + "minLength": 32 }, "payment_method_type": { "$ref": "#/components/schemas/PaymentMethod" @@ -7566,16 +7569,24 @@ "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", "nullable": true } - } + }, + "additionalProperties": false }, "CustomerResponse": { "type": "object", "required": [ + "id", "merchant_reference_id", - "created_at", - "id" + "created_at" ], "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the customer", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", + "maxLength": 64, + "minLength": 32 + }, "merchant_reference_id": { "type": "string", "description": "The identifier for the customer object", @@ -7651,12 +7662,86 @@ "example": "pm_djh2837dwduh890123", "nullable": true, "maxLength": 64 + } + } + }, + "CustomerUpdateRequest": { + "type": "object", + "required": [ + "name", + "email" + ], + "properties": { + "merchant_reference_id": { + "type": "string", + "description": "The merchant identifier for the customer object.", + "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "nullable": true, + "maxLength": 64, + "minLength": 1 }, - "id": { + "name": { + "type": "string", + "description": "The customer's name", + "example": "Jon Test", + "maxLength": 255 + }, + "email": { + "type": "string", + "description": "The customer's email address", + "example": "JonTest@test.com", + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "9123456789", + "nullable": true, + "maxLength": 255 + }, + "description": { "type": "string", - "description": "Global id" + "description": "An arbitrary string that you can attach to a customer object.", + "example": "First Customer", + "nullable": true, + "maxLength": 255 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number", + "example": "+65", + "nullable": true, + "maxLength": 255 + }, + "default_billing_address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + }, + "default_shipping_address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", + "nullable": true + }, + "default_payment_method_id": { + "type": "string", + "description": "The unique identifier of the payment method", + "example": "card_rGK4Vi5iSW70MY7J2mIg", + "nullable": true } - } + }, + "additionalProperties": false }, "DecoupledAuthenticationType": { "type": "string", @@ -13122,9 +13207,9 @@ "customer_id": { "type": "string", "description": "The unique identifier of the customer.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", "maxLength": 64, - "minLength": 1 + "minLength": 32 }, "payment_method_data": { "$ref": "#/components/schemas/PaymentMethodCreateData" @@ -13663,9 +13748,9 @@ "customer_id": { "type": "string", "description": "The unique identifier of the customer.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", "maxLength": 64, - "minLength": 1 + "minLength": 32 } }, "additionalProperties": false @@ -13854,6 +13939,7 @@ "type": "object", "required": [ "merchant_id", + "customer_id", "payment_method_id", "payment_method_type", "recurring_enabled" @@ -13867,10 +13953,9 @@ "customer_id": { "type": "string", "description": "The unique identifier of the customer.", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", "maxLength": 64, - "minLength": 1 + "minLength": 32 }, "payment_method_id": { "type": "string", @@ -14436,7 +14521,8 @@ "PaymentsCreateIntentRequest": { "type": "object", "required": [ - "amount_details" + "amount_details", + "customer_id" ], "properties": { "amount_details": { @@ -14491,10 +14577,9 @@ "customer_id": { "type": "string", "description": "The identifier for the customer", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", "maxLength": 64, - "minLength": 1 + "minLength": 32 }, "customer_present": { "allOf": [ @@ -15328,6 +15413,7 @@ "profile_id", "capture_method", "authentication_type", + "customer_id", "customer_present", "setup_future_usage", "apply_mit_exemption", @@ -15399,10 +15485,9 @@ "customer_id": { "type": "string", "description": "The identifier for the customer", - "example": "cus_y3oqhf46pyzuxjbcn2giaqnb44", - "nullable": true, + "example": "12345_cus_01926c58bc6e77c09e809964e72af8c8", "maxLength": 64, - "minLength": 1 + "minLength": 32 }, "customer_present": { "$ref": "#/components/schemas/PresenceOfCustomerDuringPayment" diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 6b6147aa90f7..c31a8bd11ed6 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -2266,7 +2266,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CustomerRequest" + "$ref": "#/components/schemas/CustomerUpdateRequest" }, "examples": { "Update name and email of a customer": { @@ -10036,6 +10036,60 @@ } } }, + "CustomerUpdateRequest": { + "type": "object", + "description": "The identifier for the customer object. If not provided the customer ID will be autogenerated.", + "properties": { + "name": { + "type": "string", + "description": "The customer's name", + "example": "Jon Test", + "nullable": true, + "maxLength": 255 + }, + "email": { + "type": "string", + "description": "The customer's email address", + "example": "JonTest@test.com", + "nullable": true, + "maxLength": 255 + }, + "phone": { + "type": "string", + "description": "The customer's phone number", + "example": "9123456789", + "nullable": true, + "maxLength": 255 + }, + "description": { + "type": "string", + "description": "An arbitrary string that you can attach to a customer object.", + "example": "First Customer", + "nullable": true, + "maxLength": 255 + }, + "phone_country_code": { + "type": "string", + "description": "The country code for the customer phone number", + "example": "+65", + "nullable": true, + "maxLength": 255 + }, + "address": { + "allOf": [ + { + "$ref": "#/components/schemas/AddressDetails" + } + ], + "nullable": true + }, + "metadata": { + "type": "object", + "description": "You can specify up to 50 keys, with key names up to 40 characters long and values up to 500\ncharacters long. Metadata is useful for storing additional, structured information on an\nobject.", + "nullable": true + } + } + }, "DecoupledAuthenticationType": { "type": "string", "enum": [ diff --git a/crates/api_models/src/customers.rs b/crates/api_models/src/customers.rs index 4f6411a42cd9..fe4ba7d96862 100644 --- a/crates/api_models/src/customers.rs +++ b/crates/api_models/src/customers.rs @@ -71,6 +71,7 @@ impl CustomerRequest { /// The customer details #[cfg(all(feature = "v2", feature = "customer_v2"))] #[derive(Debug, Default, Clone, Deserialize, Serialize, ToSchema)] +#[serde(deny_unknown_fields)] pub struct CustomerRequest { /// The merchant identifier for the customer object. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] @@ -170,6 +171,14 @@ impl CustomerResponse { #[cfg(all(feature = "v2", feature = "customer_v2"))] #[derive(Debug, Clone, Serialize, ToSchema)] pub struct CustomerResponse { + /// Unique identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub id: id_type::GlobalCustomerId, /// The identifier for the customer object #[schema(value_type = String, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub merchant_reference_id: Option, @@ -206,8 +215,6 @@ pub struct CustomerResponse { /// The identifier for the default payment method. #[schema(max_length = 64, example = "pm_djh2837dwduh890123")] pub default_payment_method_id: Option, - /// Global id - pub id: String, } #[cfg(all(feature = "v2", feature = "customer_v2"))] @@ -217,55 +224,6 @@ impl CustomerResponse { } } -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CustomerId { - pub customer_id: id_type::CustomerId, -} - -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -impl CustomerId { - pub fn get_merchant_reference_id(&self) -> id_type::CustomerId { - self.customer_id.clone() - } - - pub fn new_customer_id_struct(cust: id_type::CustomerId) -> Self { - Self { customer_id: cust } - } -} - -#[cfg(all(feature = "v2", feature = "customer_v2"))] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct GlobalId { - pub id: String, -} - -#[cfg(all(feature = "v2", feature = "customer_v2"))] -impl GlobalId { - pub fn new(id: String) -> Self { - Self { id } - } -} - -#[cfg(all(feature = "v2", feature = "customer_v2"))] -#[derive(Clone, Debug, Deserialize, Serialize)] -pub struct CustomerId { - pub merchant_reference_id: id_type::CustomerId, -} - -#[cfg(all(feature = "v2", feature = "customer_v2"))] -impl CustomerId { - pub fn get_merchant_reference_id(&self) -> id_type::CustomerId { - self.merchant_reference_id.clone() - } - - pub fn new_customer_id_struct(cust: id_type::CustomerId) -> Self { - Self { - merchant_reference_id: cust, - } - } -} - #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct CustomerDeleteResponse { @@ -286,6 +244,14 @@ pub struct CustomerDeleteResponse { #[cfg(all(feature = "v2", feature = "customer_v2"))] #[derive(Debug, Deserialize, Serialize, ToSchema)] pub struct CustomerDeleteResponse { + /// Unique identifier for the customer + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub id: id_type::GlobalCustomerId, /// The identifier for the customer object #[schema(value_type = String, max_length = 255, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] pub merchant_reference_id: Option, @@ -298,17 +264,12 @@ pub struct CustomerDeleteResponse { /// Whether payment methods deleted or not #[schema(example = false)] pub payment_methods_deleted: bool, - /// Global id - pub id: String, } /// The identifier for the customer object. If not provided the customer ID will be autogenerated. #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[derive(Debug, Default, Clone, Deserialize, Serialize, ToSchema)] pub struct CustomerUpdateRequest { - /// The identifier for the customer object - #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub customer_id: Option, /// The identifier for the Merchant Account #[schema(max_length = 255, example = "y3oqhf46pyzuxjbcn2giaqnb44")] #[serde(skip)] @@ -340,13 +301,6 @@ pub struct CustomerUpdateRequest { #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl CustomerUpdateRequest { - pub fn get_merchant_reference_id(&self) -> Option { - Some( - self.customer_id - .to_owned() - .unwrap_or_else(common_utils::generate_customer_id_of_default_length), - ) - } pub fn get_address(&self) -> Option { self.address.clone() } @@ -354,6 +308,7 @@ impl CustomerUpdateRequest { #[cfg(all(feature = "v2", feature = "customer_v2"))] #[derive(Debug, Default, Clone, Deserialize, Serialize, ToSchema)] +#[serde(deny_unknown_fields)] pub struct CustomerUpdateRequest { /// The merchant identifier for the customer object. #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] @@ -404,15 +359,16 @@ impl CustomerUpdateRequest { } } -#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone)] -pub struct UpdateCustomerId(String); - -impl UpdateCustomerId { - pub fn get_global_id(&self) -> String { - self.0.clone() - } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[derive(Debug, Serialize)] +pub struct CustomerUpdateRequestInternal { + pub customer_id: id_type::CustomerId, + pub request: CustomerUpdateRequest, +} - pub fn new(id: String) -> Self { - Self(id) - } +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[derive(Debug, Serialize)] +pub struct CustomerUpdateRequestInternal { + pub id: id_type::GlobalCustomerId, + pub request: CustomerUpdateRequest, } diff --git a/crates/api_models/src/ephemeral_key.rs b/crates/api_models/src/ephemeral_key.rs index d7ee7bd25171..d06490d6bac2 100644 --- a/crates/api_models/src/ephemeral_key.rs +++ b/crates/api_models/src/ephemeral_key.rs @@ -2,6 +2,25 @@ use common_utils::id_type; use serde; use utoipa::ToSchema; +/// Information required to create an ephemeral key. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] +pub struct EphemeralKeyCreateRequest { + /// Customer ID for which an ephemeral key must be created + #[schema( + min_length = 1, + max_length = 64, + value_type = String, + example = "cus_y3oqhf46pyzuxjbcn2giaqnb44" + )] + pub customer_id: id_type::CustomerId, +} + +impl common_utils::events::ApiEventMetric for EphemeralKeyCreateRequest { + fn get_api_event_type(&self) -> Option { + Some(common_utils::events::ApiEventsType::Miscellaneous) + } +} + /// ephemeral_key for the customer_id mentioned #[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Eq, PartialEq, ToSchema)] pub struct EphemeralKeyCreateResponse { diff --git a/crates/api_models/src/events/customer.rs b/crates/api_models/src/events/customer.rs index d2b30bcf0100..891fb0e9d3c6 100644 --- a/crates/api_models/src/events/customer.rs +++ b/crates/api_models/src/events/customer.rs @@ -1,12 +1,6 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -use crate::customers::CustomerId; -#[cfg(all(feature = "v2", feature = "customer_v2"))] -use crate::customers::GlobalId; -use crate::customers::{ - CustomerDeleteResponse, CustomerRequest, CustomerResponse, CustomerUpdateRequest, -}; +use crate::customers::{CustomerDeleteResponse, CustomerRequest, CustomerResponse}; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] impl ApiEventMetric for CustomerDeleteResponse { @@ -21,7 +15,7 @@ impl ApiEventMetric for CustomerDeleteResponse { impl ApiEventMetric for CustomerDeleteResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Customer { - id: self.id.clone(), + customer_id: Some(self.id.clone()), }) } } @@ -38,9 +32,7 @@ impl ApiEventMetric for CustomerRequest { #[cfg(all(feature = "v2", feature = "customer_v2"))] impl ApiEventMetric for CustomerRequest { fn get_api_event_type(&self) -> Option { - Some(ApiEventsType::Customer { - id: "temp_id".to_string(), - }) + Some(ApiEventsType::Customer { customer_id: None }) } } @@ -57,44 +49,25 @@ impl ApiEventMetric for CustomerResponse { impl ApiEventMetric for CustomerResponse { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Customer { - id: self.id.clone(), + customer_id: Some(self.id.clone()), }) } } #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -impl ApiEventMetric for CustomerId { - fn get_api_event_type(&self) -> Option { - Some(ApiEventsType::Customer { - customer_id: self.get_merchant_reference_id().clone(), - }) - } -} - -#[cfg(all(feature = "v2", feature = "customer_v2"))] -impl ApiEventMetric for GlobalId { +impl ApiEventMetric for crate::customers::CustomerUpdateRequestInternal { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Customer { - id: self.id.clone(), + customer_id: self.customer_id.clone(), }) } } -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] -impl ApiEventMetric for CustomerUpdateRequest { - fn get_api_event_type(&self) -> Option { - self.get_merchant_reference_id() - .clone() - .map(|cid| ApiEventsType::Customer { customer_id: cid }) - } -} - #[cfg(all(feature = "v2", feature = "customer_v2"))] -impl ApiEventMetric for CustomerUpdateRequest { +impl ApiEventMetric for crate::customers::CustomerUpdateRequestInternal { fn get_api_event_type(&self) -> Option { Some(ApiEventsType::Customer { - id: "temo_id".to_string(), + customer_id: Some(self.id.clone()), }) } } -// These needs to be fixed for v2 diff --git a/crates/api_models/src/payment_methods.rs b/crates/api_models/src/payment_methods.rs index 283a0d662d71..c821feaff575 100644 --- a/crates/api_models/src/payment_methods.rs +++ b/crates/api_models/src/payment_methods.rs @@ -121,8 +121,13 @@ pub struct PaymentMethodCreate { pub metadata: Option, /// The unique identifier of the customer. - #[schema(value_type = String, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub customer_id: id_type::CustomerId, + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: id_type::GlobalCustomerId, /// Payment method data to be passed pub payment_method_data: PaymentMethodCreateData, @@ -145,8 +150,13 @@ pub struct PaymentMethodIntentCreate { pub billing: Option, /// The unique identifier of the customer. - #[schema(value_type = String, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub customer_id: id_type::CustomerId, + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: id_type::GlobalCustomerId, } #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] @@ -782,8 +792,13 @@ pub struct PaymentMethodResponse { pub merchant_id: id_type::MerchantId, /// The unique identifier of the customer. - #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub customer_id: id_type::CustomerId, + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: id_type::GlobalCustomerId, /// The unique identifier of the Payment method #[schema(example = "card_rGK4Vi5iSW70MY7J2mIg")] @@ -1780,8 +1795,13 @@ pub struct CustomerPaymentMethod { pub payment_method_id: String, /// The unique identifier of the customer. - #[schema(value_type = String, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub customer_id: id_type::CustomerId, + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: id_type::GlobalCustomerId, /// The type of payment method use for the payment. #[schema(value_type = PaymentMethod,example = "card")] @@ -2253,36 +2273,6 @@ impl From for PaymentMethodMigrationResponse } } -#[cfg(all(feature = "v2", feature = "payment_methods_v2"))] -impl From for PaymentMethodMigrationResponse { - fn from((response, record): PaymentMethodMigrationResponseType) -> Self { - match response { - Ok(res) => Self { - payment_method_id: Some(res.payment_method_response.payment_method_id), - payment_method: res.payment_method_response.payment_method_type, - payment_method_type: res.payment_method_response.payment_method_subtype, - customer_id: Some(res.payment_method_response.customer_id), - migration_status: MigrationStatus::Success, - migration_error: None, - card_number_masked: Some(record.card_number_masked), - line_number: record.line_number, - card_migrated: res.card_migrated, - network_token_migrated: res.network_token_migrated, - connector_mandate_details_migrated: res.connector_mandate_details_migrated, - network_transaction_id_migrated: res.network_transaction_id_migrated, - }, - Err(e) => Self { - customer_id: Some(record.customer_id), - migration_status: MigrationStatus::Failed, - migration_error: Some(e), - card_number_masked: Some(record.card_number_masked), - line_number: record.line_number, - ..Self::default() - }, - } - } -} - impl TryFrom<( PaymentMethodRecord, diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 2f08ba830fc2..0381e347f4f1 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -153,8 +153,13 @@ pub struct PaymentsCreateIntentRequest { pub shipping: Option
, /// The identifier for the customer - #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub customer_id: Option, + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, /// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. #[schema(example = "present", value_type = Option)] @@ -442,8 +447,13 @@ pub struct PaymentsIntentResponse { pub shipping: Option
, /// The identifier for the customer - #[schema(value_type = Option, max_length = 64, min_length = 1, example = "cus_y3oqhf46pyzuxjbcn2giaqnb44")] - pub customer_id: Option, + #[schema( + min_length = 32, + max_length = 64, + example = "12345_cus_01926c58bc6e77c09e809964e72af8c8", + value_type = String + )] + pub customer_id: Option, /// Set to `present` to indicate that the customer is in your checkout flow during this payment, and therefore is able to authenticate. This parameter should be `absent` when merchant's doing merchant initiated payments and customer is not present while doing the payment. #[schema(example = "present", value_type = PresenceOfCustomerDuringPayment)] diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 3b86ea8e88bb..b9af92786f58 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -42,7 +42,7 @@ pub enum ApiEventsType { PaymentMethodCreate, #[cfg(all(feature = "v2", feature = "customer_v2"))] Customer { - id: String, + customer_id: Option, }, #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] Customer { diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index e0aa77ce707e..594078bb4057 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -1,10 +1,10 @@ //! Common ID types //! The id type can be used to create specific id types with custom behaviour -use std::{borrow::Cow, fmt::Debug}; - mod api_key; mod customer; +#[cfg(feature = "v2")] +mod global_id; mod merchant; mod merchant_connector_account; mod organization; @@ -14,11 +14,8 @@ mod refunds; mod routing; mod tenant; -#[cfg(feature = "v2")] -mod global_id; +use std::{borrow::Cow, fmt::Debug}; -pub use api_key::ApiKeyId; -pub use customer::CustomerId; use diesel::{ backend::Backend, deserialize::FromSql, @@ -26,24 +23,29 @@ use diesel::{ serialize::{Output, ToSql}, sql_types, }; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + #[cfg(feature = "v2")] -pub use global_id::{ +pub use self::global_id::{ + customer::GlobalCustomerId, payment::{GlobalAttemptId, GlobalPaymentId}, payment_methods::GlobalPaymentMethodId, refunds::GlobalRefundId, CellId, }; -pub use merchant::MerchantId; -pub use merchant_connector_account::MerchantConnectorAccountId; -pub use organization::OrganizationId; -pub use payment::{PaymentId, PaymentReferenceId}; -pub use profile::ProfileId; -pub use refunds::RefundReferenceId; -pub use routing::RoutingId; -use serde::{Deserialize, Serialize}; -pub use tenant::TenantId; -use thiserror::Error; - +pub use self::{ + api_key::ApiKeyId, + customer::CustomerId, + merchant::MerchantId, + merchant_connector_account::MerchantConnectorAccountId, + organization::OrganizationId, + payment::{PaymentId, PaymentReferenceId}, + profile::ProfileId, + refunds::RefundReferenceId, + routing::RoutingId, + tenant::TenantId, +}; use crate::{fp_utils::when, generate_id_with_default_len}; #[inline] diff --git a/crates/common_utils/src/id_type/customer.rs b/crates/common_utils/src/id_type/customer.rs index 9d02f2013837..54c44020b87d 100644 --- a/crates/common_utils/src/id_type/customer.rs +++ b/crates/common_utils/src/id_type/customer.rs @@ -13,3 +13,12 @@ crate::impl_generate_id_id_type!(CustomerId, "cus"); crate::impl_serializable_secret_id_type!(CustomerId); crate::impl_queryable_id_type!(CustomerId); crate::impl_to_sql_from_sql_id_type!(CustomerId); + +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +impl crate::events::ApiEventMetric for CustomerId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::Customer { + customer_id: self.clone(), + }) + } +} diff --git a/crates/common_utils/src/id_type/global_id.rs b/crates/common_utils/src/id_type/global_id.rs index d783912459e8..1ad1bd960840 100644 --- a/crates/common_utils/src/id_type/global_id.rs +++ b/crates/common_utils/src/id_type/global_id.rs @@ -1,6 +1,7 @@ -pub mod payment; -pub mod payment_methods; -pub mod refunds; +pub(super) mod customer; +pub(super) mod payment; +pub(super) mod payment_methods; +pub(super) mod refunds; use diesel::{backend::Backend, deserialize::FromSql, serialize::ToSql, sql_types}; use error_stack::ResultExt; diff --git a/crates/common_utils/src/id_type/global_id/customer.rs b/crates/common_utils/src/id_type/global_id/customer.rs new file mode 100644 index 000000000000..e0de91d8aed7 --- /dev/null +++ b/crates/common_utils/src/id_type/global_id/customer.rs @@ -0,0 +1,45 @@ +use error_stack::ResultExt; + +use crate::{errors, generate_id_with_default_len, generate_time_ordered_id_without_prefix, types}; + +crate::global_id_type!( + GlobalCustomerId, + "A global id that can be used to identify a customer. + +The format will be `__`. + +Example: `cell1_cus_uu1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p`" +); + +// Database related implementations so that this field can be used directly in the database tables +crate::impl_queryable_id_type!(GlobalCustomerId); +crate::impl_to_sql_from_sql_global_id_type!(GlobalCustomerId); + +impl GlobalCustomerId { + /// Get string representation of the id + pub fn get_string_repr(&self) -> &str { + self.0.get_string_repr() + } + + /// Generate a new GlobalCustomerId from a cell id + pub fn generate(cell_id: &crate::id_type::CellId) -> Self { + let global_id = super::GlobalId::generate(cell_id, super::GlobalEntity::Customer); + Self(global_id) + } +} + +impl TryFrom for crate::id_type::CustomerId { + type Error = error_stack::Report; + + fn try_from(value: GlobalCustomerId) -> Result { + Self::try_from(std::borrow::Cow::from(value.get_string_repr().to_owned())) + } +} + +impl crate::events::ApiEventMetric for GlobalCustomerId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::Customer { + customer_id: Some(self.clone()), + }) + } +} diff --git a/crates/common_utils/src/id_type/global_id/payment.rs b/crates/common_utils/src/id_type/global_id/payment.rs index 5a2da3998bb6..c65fd06f0687 100644 --- a/crates/common_utils/src/id_type/global_id/payment.rs +++ b/crates/common_utils/src/id_type/global_id/payment.rs @@ -4,9 +4,11 @@ use crate::{errors, generate_id_with_default_len, generate_time_ordered_id_witho crate::global_id_type!( GlobalPaymentId, - "A global id that can be used to identify a payment - The format will be `__` - example - cell1_pay_uu1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p" + "A global id that can be used to identify a payment. + +The format will be `__`. + +Example: `cell1_pay_uu1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p`" ); // Database related implementations so that this field can be used directly in the database tables diff --git a/crates/diesel_models/src/customers.rs b/crates/diesel_models/src/customers.rs index 46b5059b42a2..7bd7d8368a71 100644 --- a/crates/diesel_models/src/customers.rs +++ b/crates/diesel_models/src/customers.rs @@ -91,7 +91,7 @@ pub struct CustomerNew { pub default_billing_address: Option, pub default_shipping_address: Option, pub status: DeleteStatus, - pub id: String, + pub id: common_utils::id_type::GlobalCustomerId, } #[cfg(all(feature = "v2", feature = "customer_v2"))] @@ -173,7 +173,7 @@ pub struct Customer { pub default_billing_address: Option, pub default_shipping_address: Option, pub status: DeleteStatus, - pub id: String, + pub id: common_utils::id_type::GlobalCustomerId, } #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] diff --git a/crates/diesel_models/src/kv.rs b/crates/diesel_models/src/kv.rs index 801592712928..94dcf00f755c 100644 --- a/crates/diesel_models/src/kv.rs +++ b/crates/diesel_models/src/kv.rs @@ -179,7 +179,7 @@ impl DBOperation { )), #[cfg(all(feature = "v2", feature = "customer_v2"))] Updateable::CustomerUpdate(cust) => DBResult::Customer(Box::new( - Customer::update_by_id(conn, cust.orig.id.clone(), cust.update_data).await?, + Customer::update_by_id(conn, cust.orig.id, cust.update_data).await?, )), }, }) diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 69743dbf1ca6..892e8867ab07 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -21,7 +21,7 @@ pub struct PaymentIntent { pub amount: MinorUnit, pub currency: storage_enums::Currency, pub amount_captured: Option, - pub customer_id: Option, + pub customer_id: Option, pub description: Option, pub return_url: Option, pub metadata: Option, @@ -251,7 +251,7 @@ pub struct PaymentIntentNew { pub amount: MinorUnit, pub currency: storage_enums::Currency, pub amount_captured: Option, - pub customer_id: Option, + pub customer_id: Option, pub description: Option, pub return_url: Option, pub metadata: Option, diff --git a/crates/diesel_models/src/payment_method.rs b/crates/diesel_models/src/payment_method.rs index 206ba63e17fc..46af78d6d91e 100644 --- a/crates/diesel_models/src/payment_method.rs +++ b/crates/diesel_models/src/payment_method.rs @@ -70,7 +70,7 @@ pub struct PaymentMethod { #[derive(Clone, Debug, Identifiable, Queryable, Selectable, Serialize, Deserialize)] #[diesel(table_name = payment_methods, primary_key(id), check_for_backend(diesel::pg::Pg))] pub struct PaymentMethod { - pub customer_id: common_utils::id_type::CustomerId, + pub customer_id: common_utils::id_type::GlobalCustomerId, pub merchant_id: common_utils::id_type::MerchantId, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, @@ -158,7 +158,7 @@ pub struct PaymentMethodNew { #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay, Serialize, Deserialize)] #[diesel(table_name = payment_methods)] pub struct PaymentMethodNew { - pub customer_id: common_utils::id_type::CustomerId, + pub customer_id: common_utils::id_type::GlobalCustomerId, pub merchant_id: common_utils::id_type::MerchantId, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, diff --git a/crates/diesel_models/src/query/customers.rs b/crates/diesel_models/src/query/customers.rs index cf6e8fa645f4..5caad88c8d64 100644 --- a/crates/diesel_models/src/query/customers.rs +++ b/crates/diesel_models/src/query/customers.rs @@ -33,7 +33,7 @@ impl Customer { #[cfg(all(feature = "v2", feature = "customer_v2"))] pub async fn update_by_id( conn: &PgPooledConn, - id: String, + id: id_type::GlobalCustomerId, customer: CustomerUpdateInternal, ) -> StorageResult { match generics::generic_update_by_id::<::Table, _, _, _>( @@ -54,7 +54,10 @@ impl Customer { } #[cfg(all(feature = "v2", feature = "customer_v2"))] - pub async fn find_by_global_id(conn: &PgPooledConn, id: &str) -> StorageResult { + pub async fn find_by_global_id( + conn: &PgPooledConn, + id: &id_type::GlobalCustomerId, + ) -> StorageResult { generics::generic_find_by_id::<::Table, _, _>(conn, id.to_owned()).await } diff --git a/crates/diesel_models/src/query/mandate.rs b/crates/diesel_models/src/query/mandate.rs index 9f5a402c1772..96e6850f0fa3 100644 --- a/crates/diesel_models/src/query/mandate.rs +++ b/crates/diesel_models/src/query/mandate.rs @@ -63,8 +63,23 @@ impl Mandate { //Fix this function once V2 mandate is schema is being built #[cfg(all(feature = "v2", feature = "customer_v2"))] - pub async fn find_by_global_id(_conn: &PgPooledConn, _id: &str) -> StorageResult> { - todo!() + pub async fn find_by_global_customer_id( + conn: &PgPooledConn, + customer_id: &common_utils::id_type::GlobalCustomerId, + ) -> StorageResult> { + generics::generic_filter::< + ::Table, + _, + <::Table as Table>::PrimaryKey, + _, + >( + conn, + dsl::customer_id.eq(customer_id.to_owned()), + None, + None, + None, + ) + .await } pub async fn update_by_merchant_id_mandate_id( diff --git a/crates/diesel_models/src/query/payment_method.rs b/crates/diesel_models/src/query/payment_method.rs index 321c7a6829c5..d4ad52ae146d 100644 --- a/crates/diesel_models/src/query/payment_method.rs +++ b/crates/diesel_models/src/query/payment_method.rs @@ -126,16 +126,6 @@ impl PaymentMethod { .await } - // Need to fix this function once we start moving to v2 for payment method - #[cfg(all(feature = "v2", feature = "customer_v2"))] - pub async fn find_by_global_id( - _conn: &PgPooledConn, - _id: &str, - _limit: Option, - ) -> StorageResult> { - todo!() - } - pub async fn get_count_by_customer_id_merchant_id_status( conn: &PgPooledConn, customer_id: &common_utils::id_type::CustomerId, @@ -219,9 +209,9 @@ impl PaymentMethod { .await } - pub async fn find_by_customer_id_merchant_id_status( + pub async fn find_by_global_customer_id_merchant_id_status( conn: &PgPooledConn, - customer_id: &common_utils::id_type::CustomerId, + customer_id: &common_utils::id_type::GlobalCustomerId, merchant_id: &common_utils::id_type::MerchantId, status: storage_enums::PaymentMethodStatus, limit: Option, @@ -239,6 +229,21 @@ impl PaymentMethod { .await } + pub async fn find_by_global_customer_id( + conn: &PgPooledConn, + customer_id: &common_utils::id_type::GlobalCustomerId, + limit: Option, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::customer_id.eq(customer_id.to_owned()), + limit, + None, + Some(dsl::last_used_at.desc()), + ) + .await + } + pub async fn update_with_id( self, conn: &PgPooledConn, diff --git a/crates/hyperswitch_domain_models/src/customer.rs b/crates/hyperswitch_domain_models/src/customer.rs index 71cb4ebc2f92..b503326f84c5 100644 --- a/crates/hyperswitch_domain_models/src/customer.rs +++ b/crates/hyperswitch_domain_models/src/customer.rs @@ -63,11 +63,25 @@ pub struct Customer { pub merchant_reference_id: Option, pub default_billing_address: Option, pub default_shipping_address: Option, - pub id: String, + pub id: id_type::GlobalCustomerId, pub version: common_enums::ApiVersion, pub status: DeleteStatus, } +impl Customer { + /// Get the unique identifier of Customer + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] + pub fn get_id(&self) -> &id_type::CustomerId { + &self.customer_id + } + + /// Get the global identifier of Customer + #[cfg(all(feature = "v2", feature = "customer_v2"))] + pub fn get_id(&self) -> &id_type::GlobalCustomerId { + &self.id + } +} + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[async_trait::async_trait] impl super::behaviour::Conversion for Customer { diff --git a/crates/hyperswitch_domain_models/src/payment_methods.rs b/crates/hyperswitch_domain_models/src/payment_methods.rs index 4e7d727839b2..d03762dd64e8 100644 --- a/crates/hyperswitch_domain_models/src/payment_methods.rs +++ b/crates/hyperswitch_domain_models/src/payment_methods.rs @@ -74,7 +74,7 @@ pub struct PaymentMethod { #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] #[derive(Clone, Debug)] pub struct PaymentMethod { - pub customer_id: common_utils::id_type::CustomerId, + pub customer_id: common_utils::id_type::GlobalCustomerId, pub merchant_id: common_utils::id_type::MerchantId, pub created_at: PrimitiveDateTime, pub last_modified: PrimitiveDateTime, diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 9488a762a666..0243c2549204 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -281,7 +281,7 @@ pub struct PaymentIntent { /// The total amount captured for the order. This is the sum of all the captured amounts for the order. pub amount_captured: Option, /// The identifier for the customer. This is the identifier for the customer in the merchant's system. - pub customer_id: Option, + pub customer_id: Option, /// The description of the order. This will be passed to connectors which support description. pub description: Option, /// The return url for the payment. This is the url to which the user will be redirected after the payment is completed. diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 7ad5645e75ac..db0db93109ba 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -235,6 +235,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::BusinessCollectLinkConfig, api_models::admin::BusinessPayoutLinkConfig, api_models::customers::CustomerRequest, + api_models::customers::CustomerUpdateRequest, api_models::customers::CustomerDeleteResponse, api_models::payment_methods::PaymentMethodCreate, api_models::payment_methods::PaymentMethodResponse, diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs index 5a5454e2505e..b5de979b3aef 100644 --- a/crates/openapi/src/openapi_v2.rs +++ b/crates/openapi/src/openapi_v2.rs @@ -187,6 +187,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::admin::BusinessCollectLinkConfig, api_models::admin::BusinessPayoutLinkConfig, api_models::customers::CustomerRequest, + api_models::customers::CustomerUpdateRequest, api_models::customers::CustomerDeleteResponse, api_models::payment_methods::PaymentMethodCreate, api_models::payment_methods::PaymentMethodIntentCreate, diff --git a/crates/openapi/src/routes/customers.rs b/crates/openapi/src/routes/customers.rs index b4c612307524..f23b8b349571 100644 --- a/crates/openapi/src/routes/customers.rs +++ b/crates/openapi/src/routes/customers.rs @@ -51,7 +51,7 @@ pub async fn customers_retrieve() {} post, path = "/customers/{customer_id}", request_body ( - content = CustomerRequest, + content = CustomerUpdateRequest, examples (( "Update name and email of a customer" =( value =json!( { "email": "guest@example.com", @@ -159,7 +159,7 @@ pub async fn customers_retrieve() {} post, path = "/v2/customers/{id}", request_body ( - content = CustomerRequest, + content = CustomerUpdateRequest, examples (( "Update name and email of a customer" =( value =json!( { "email": "guest@example.com", diff --git a/crates/router/src/compatibility/stripe/customers.rs b/crates/router/src/compatibility/stripe/customers.rs index 190e25d71c93..56cf25434c29 100644 --- a/crates/router/src/compatibility/stripe/customers.rs +++ b/crates/router/src/compatibility/stripe/customers.rs @@ -74,7 +74,7 @@ pub async fn customer_retrieve( req: HttpRequest, path: web::Path, ) -> HttpResponse { - let payload = customer_types::CustomerId::new_customer_id_struct(path.into_inner()); + let customer_id = path.into_inner(); let flow = Flow::CustomersRetrieve; @@ -91,9 +91,15 @@ pub async fn customer_retrieve( flow, state.into_inner(), &req, - payload, - |state, auth: auth::AuthenticationData, req, _| { - customers::retrieve_customer(state, auth.merchant_account, None, auth.key_store, req) + customer_id, + |state, auth: auth::AuthenticationData, customer_id, _| { + customers::retrieve_customer( + state, + auth.merchant_account, + None, + auth.key_store, + customer_id, + ) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, @@ -121,10 +127,12 @@ pub async fn customer_update( } }; - let customer_id = path.into_inner(); - let mut cust_update_req: customer_types::CustomerUpdateRequest = payload.into(); - cust_update_req.customer_id = Some(customer_id); - let customer_update_id = customer_types::UpdateCustomerId::new("temp_global_id".to_string()); + let customer_id = path.into_inner().clone(); + let request = customer_types::CustomerUpdateRequest::from(payload); + let request_internal = customer_types::CustomerUpdateRequestInternal { + customer_id, + request, + }; let flow = Flow::CustomersUpdate; @@ -141,14 +149,13 @@ pub async fn customer_update( flow, state.into_inner(), &req, - cust_update_req, - |state, auth: auth::AuthenticationData, req, _| { + request_internal, + |state, auth: auth::AuthenticationData, request_internal, _| { customers::update_customer( state, auth.merchant_account, - req, + request_internal, auth.key_store, - customer_update_id.clone(), ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -168,7 +175,7 @@ pub async fn customer_delete( req: HttpRequest, path: web::Path, ) -> HttpResponse { - let payload = customer_types::CustomerId::new_customer_id_struct(path.into_inner()); + let customer_id = path.into_inner(); let flow = Flow::CustomersDelete; @@ -185,9 +192,9 @@ pub async fn customer_delete( flow, state.into_inner(), &req, - payload, - |state, auth: auth::AuthenticationData, req, _| { - customers::delete_customer(state, auth.merchant_account, req, auth.key_store) + customer_id, + |state, auth: auth::AuthenticationData, customer_id, _| { + customers::delete_customer(state, auth.merchant_account, customer_id, auth.key_store) }, &auth::HeaderAuth(auth::ApiKeyAuth), api_locking::LockAction::NotApplicable, diff --git a/crates/router/src/core/customers.rs b/crates/router/src/core/customers.rs index 3081617f3ea5..059ac4c2f35c 100644 --- a/crates/router/src/core/customers.rs +++ b/crates/router/src/core/customers.rs @@ -1,7 +1,7 @@ use common_utils::{ crypto::Encryptable, errors::ReportSwitchExt, - ext_traits::{AsyncExt, OptionExt}, + ext_traits::AsyncExt, id_type, pii, type_name, types::{ keymanager::{Identifier, KeyManagerState, ToEncryptable}, @@ -264,7 +264,7 @@ impl CustomerCreateBridge for customers::CustomerRequest { .change_context(errors::CustomersErrorResponse::InternalServerError)?; Ok(domain::Customer { - id: common_utils::generate_time_ordered_id("cus"), + id: id_type::GlobalCustomerId::generate(&state.conf.cell_information.id), merchant_reference_id: merchant_reference_id.to_owned(), merchant_id, name: encryptable_customer.name, @@ -457,7 +457,7 @@ pub async fn retrieve_customer( merchant_account: domain::MerchantAccount, _profile_id: Option, key_store: domain::MerchantKeyStore, - req: customers::CustomerId, + customer_id: id_type::CustomerId, ) -> errors::CustomerResponse { let db = state.store.as_ref(); let key_manager_state = &(&state).into(); @@ -465,7 +465,7 @@ pub async fn retrieve_customer( let response = db .find_customer_by_customer_id_merchant_id( key_manager_state, - &req.customer_id, + &customer_id, merchant_account.get_id(), &key_store, merchant_account.storage_scheme, @@ -491,7 +491,7 @@ pub async fn retrieve_customer( state: SessionState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, - req: customers::GlobalId, + id: id_type::GlobalCustomerId, ) -> errors::CustomerResponse { let db = state.store.as_ref(); let key_manager_state = &(&state).into(); @@ -499,7 +499,7 @@ pub async fn retrieve_customer( let response = db .find_customer_by_global_id( key_manager_state, - &req.id, + &id, merchant_account.get_id(), &key_store, merchant_account.storage_scheme, @@ -563,12 +563,12 @@ pub async fn list_customers( pub async fn delete_customer( state: SessionState, merchant_account: domain::MerchantAccount, - req: customers::GlobalId, + id: id_type::GlobalCustomerId, key_store: domain::MerchantKeyStore, ) -> errors::CustomerResponse { let db = &*state.store; let key_manager_state = &(&state).into(); - req.fetch_domain_model_and_update_and_generate_delete_customer_response( + id.fetch_domain_model_and_update_and_generate_delete_customer_response( db, &key_store, &merchant_account, @@ -584,7 +584,7 @@ pub async fn delete_customer( feature = "payment_methods_v2" ))] #[async_trait::async_trait] -impl CustomerDeleteBridge for customers::GlobalId { +impl CustomerDeleteBridge for id_type::GlobalCustomerId { async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( &'a self, db: &'a dyn StorageInterface, @@ -596,7 +596,7 @@ impl CustomerDeleteBridge for customers::GlobalId { let customer_orig = db .find_customer_by_global_id( key_manager_state, - &self.id, + self, merchant_account.get_id(), key_store, merchant_account.storage_scheme, @@ -606,7 +606,7 @@ impl CustomerDeleteBridge for customers::GlobalId { let merchant_reference_id = customer_orig.merchant_reference_id.clone(); - let customer_mandates = db.find_mandate_by_global_id(&self.id).await.switch()?; + let customer_mandates = db.find_mandate_by_global_customer_id(self).await.switch()?; for mandate in customer_mandates.into_iter() { if mandate.mandate_status == enums::MandateStatus::Active { @@ -615,14 +615,19 @@ impl CustomerDeleteBridge for customers::GlobalId { } match db - .find_payment_method_list_by_global_id(key_manager_state, key_store, &self.id, None) + .find_payment_method_list_by_global_customer_id( + key_manager_state, + key_store, + self, + None, + ) .await { // check this in review Ok(customer_payment_methods) => { for pm in customer_payment_methods.into_iter() { - if pm.payment_method_type_v2 == Some(enums::PaymentMethod::Card) { - cards::delete_card_by_locker_id(state, &self.id, merchant_account.get_id()) + if pm.get_payment_method_type() == Some(enums::PaymentMethod::Card) { + cards::delete_card_by_locker_id(state, self, merchant_account.get_id()) .await .switch()?; } @@ -688,7 +693,7 @@ impl CustomerDeleteBridge for customers::GlobalId { db.update_customer_by_global_id( key_manager_state, - self.id.clone(), + self, customer_orig, merchant_account.get_id(), updated_customer, @@ -699,11 +704,11 @@ impl CustomerDeleteBridge for customers::GlobalId { .switch()?; let response = customers::CustomerDeleteResponse { + id: self.clone(), merchant_reference_id, customer_deleted: true, address_deleted: true, payment_methods_deleted: true, - id: self.id.clone(), }; metrics::CUSTOMER_REDACTED.add(1, &[]); Ok(services::ApplicationResponse::Json(response)) @@ -731,19 +736,20 @@ trait CustomerDeleteBridge { pub async fn delete_customer( state: SessionState, merchant_account: domain::MerchantAccount, - req: customers::CustomerId, + customer_id: id_type::CustomerId, key_store: domain::MerchantKeyStore, ) -> errors::CustomerResponse { let db = &*state.store; let key_manager_state = &(&state).into(); - req.fetch_domain_model_and_update_and_generate_delete_customer_response( - db, - &key_store, - &merchant_account, - key_manager_state, - &state, - ) - .await + customer_id + .fetch_domain_model_and_update_and_generate_delete_customer_response( + db, + &key_store, + &merchant_account, + key_manager_state, + &state, + ) + .await } #[cfg(all( @@ -752,7 +758,7 @@ pub async fn delete_customer( not(feature = "payment_methods_v2") ))] #[async_trait::async_trait] -impl CustomerDeleteBridge for customers::CustomerId { +impl CustomerDeleteBridge for id_type::CustomerId { async fn fetch_domain_model_and_update_and_generate_delete_customer_response<'a>( &'a self, db: &'a dyn StorageInterface, @@ -764,7 +770,7 @@ impl CustomerDeleteBridge for customers::CustomerId { let customer_orig = db .find_customer_by_customer_id_merchant_id( key_manager_state, - &self.customer_id, + self, merchant_account.get_id(), key_store, merchant_account.storage_scheme, @@ -773,7 +779,7 @@ impl CustomerDeleteBridge for customers::CustomerId { .switch()?; let customer_mandates = db - .find_mandate_by_merchant_id_customer_id(merchant_account.get_id(), &self.customer_id) + .find_mandate_by_merchant_id_customer_id(merchant_account.get_id(), self) .await .switch()?; @@ -787,7 +793,7 @@ impl CustomerDeleteBridge for customers::CustomerId { .find_payment_method_by_customer_id_merchant_id_list( key_manager_state, key_store, - &self.customer_id, + self, merchant_account.get_id(), None, ) @@ -799,7 +805,7 @@ impl CustomerDeleteBridge for customers::CustomerId { if pm.get_payment_method_type() == Some(enums::PaymentMethod::Card) { cards::delete_card_from_locker( state, - &self.customer_id, + self, merchant_account.get_id(), pm.locker_id.as_ref().unwrap_or(&pm.payment_method_id), ) @@ -810,7 +816,7 @@ impl CustomerDeleteBridge for customers::CustomerId { { network_tokenization::delete_network_token_from_locker_and_token_service( state, - &self.customer_id, + self, merchant_account.get_id(), pm.payment_method_id.clone(), pm.network_token_locker_id, @@ -884,7 +890,7 @@ impl CustomerDeleteBridge for customers::CustomerId { match db .update_address_by_merchant_id_customer_id( key_manager_state, - &self.customer_id, + self, merchant_account.get_id(), update_address, key_store, @@ -927,7 +933,7 @@ impl CustomerDeleteBridge for customers::CustomerId { db.update_customer_by_customer_id_merchant_id( key_manager_state, - self.customer_id.clone(), + self.clone(), merchant_account.get_id().to_owned(), customer_orig, updated_customer, @@ -938,7 +944,7 @@ impl CustomerDeleteBridge for customers::CustomerId { .switch()?; let response = customers::CustomerDeleteResponse { - customer_id: self.customer_id.clone(), + customer_id: self.clone(), customer_deleted: true, address_deleted: true, payment_methods_deleted: true, @@ -952,19 +958,24 @@ impl CustomerDeleteBridge for customers::CustomerId { pub async fn update_customer( state: SessionState, merchant_account: domain::MerchantAccount, - update_customer: customers::CustomerUpdateRequest, + update_customer: customers::CustomerUpdateRequestInternal, key_store: domain::MerchantKeyStore, - id: customers::UpdateCustomerId, ) -> errors::CustomerResponse { let db = state.store.as_ref(); let key_manager_state = &(&state).into(); //Add this in update call if customer can be updated anywhere else - let merchant_reference_id = update_customer.get_merchant_reference_id(); + #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] + let verify_id_for_update_customer = VerifyIdForUpdateCustomer { + merchant_reference_id: &update_customer.customer_id, + merchant_account: &merchant_account, + key_store: &key_store, + key_manager_state, + }; + #[cfg(all(feature = "v2", feature = "customer_v2"))] let verify_id_for_update_customer = VerifyIdForUpdateCustomer { - merchant_reference_id: merchant_reference_id.as_ref(), - id: &id, + id: &update_customer.id, merchant_account: &merchant_account, key_store: &key_store, key_manager_state, @@ -975,6 +986,7 @@ pub async fn update_customer( .await?; let updated_customer = update_customer + .request .create_domain_model_from_request( db, &key_store, @@ -985,7 +997,7 @@ pub async fn update_customer( ) .await?; - update_customer.generate_response(&updated_customer) + update_customer.request.generate_response(&updated_customer) } #[async_trait::async_trait] @@ -1100,9 +1112,19 @@ impl<'a> AddressStructForDbUpdate<'a> { } } +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] +#[derive(Debug)] struct VerifyIdForUpdateCustomer<'a> { - merchant_reference_id: Option<&'a id_type::CustomerId>, - id: &'a customers::UpdateCustomerId, + merchant_reference_id: &'a id_type::CustomerId, + merchant_account: &'a domain::MerchantAccount, + key_store: &'a domain::MerchantKeyStore, + key_manager_state: &'a KeyManagerState, +} + +#[cfg(all(feature = "v2", feature = "customer_v2"))] +#[derive(Debug)] +struct VerifyIdForUpdateCustomer<'a> { + id: &'a id_type::GlobalCustomerId, merchant_account: &'a domain::MerchantAccount, key_store: &'a domain::MerchantKeyStore, key_manager_state: &'a KeyManagerState, @@ -1114,18 +1136,10 @@ impl<'a> VerifyIdForUpdateCustomer<'a> { &self, db: &dyn StorageInterface, ) -> Result> { - let customer_id = self - .merchant_reference_id - .get_required_value("customer_id") - .change_context(errors::CustomersErrorResponse::InternalServerError) - .attach("Missing required field `customer_id`")?; - - let _id = self.id; - let customer = db .find_customer_by_customer_id_merchant_id( self.key_manager_state, - customer_id, + self.merchant_reference_id, self.merchant_account.get_id(), self.key_store, self.merchant_account.storage_scheme, @@ -1143,13 +1157,10 @@ impl<'a> VerifyIdForUpdateCustomer<'a> { &self, db: &dyn StorageInterface, ) -> Result> { - let id = self.id.get_global_id(); - - let _merchant_reference_id = self.merchant_reference_id; let customer = db .find_customer_by_global_id( self.key_manager_state, - &id, + self.id, self.merchant_account.get_id(), self.key_store, self.merchant_account.storage_scheme, @@ -1322,7 +1333,7 @@ impl CustomerUpdateBridge for customers::CustomerUpdateRequest { let response = db .update_customer_by_global_id( key_manager_state, - domain_customer.id.to_owned(), + &domain_customer.id, domain_customer.to_owned(), merchant_account.get_id(), storage::CustomerUpdate::Update { diff --git a/crates/router/src/core/mandate.rs b/crates/router/src/core/mandate.rs index da8015662a7e..15ceb9b1da21 100644 --- a/crates/router/src/core/mandate.rs +++ b/crates/router/src/core/mandate.rs @@ -19,7 +19,6 @@ use crate::{ types::{ self, api::{ - customers, mandates::{self, MandateResponseExt}, ConnectorData, GetToken, }, @@ -224,27 +223,24 @@ pub async fn update_connector_mandate_id( } Ok(services::ApplicationResponse::StatusOk) } - +#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] #[instrument(skip(state))] pub async fn get_customer_mandates( state: SessionState, merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, - req: customers::CustomerId, + customer_id: id_type::CustomerId, ) -> RouterResponse> { let mandates = state .store - .find_mandate_by_merchant_id_customer_id( - merchant_account.get_id(), - &req.get_merchant_reference_id(), - ) + .find_mandate_by_merchant_id_customer_id(merchant_account.get_id(), &customer_id) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable_lazy(|| { format!( "Failed while finding mandate: merchant_id: {:?}, customer_id: {:?}", merchant_account.get_id(), - req.get_merchant_reference_id() + customer_id, ) })?; diff --git a/crates/router/src/core/payment_methods.rs b/crates/router/src/core/payment_methods.rs index 2d9a71a2584d..61dc70ac4799 100644 --- a/crates/router/src/core/payment_methods.rs +++ b/crates/router/src/core/payment_methods.rs @@ -1,4 +1,8 @@ pub mod cards; +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub mod migration; pub mod network_tokenization; pub mod surcharge_decision_configs; @@ -677,7 +681,7 @@ pub(crate) async fn get_payment_method_create_request( payment_method_data: Option<&domain::PaymentMethodData>, payment_method_type: Option, payment_method_subtype: Option, - customer_id: &Option, + customer_id: &Option, billing_name: Option>, ) -> RouterResult { match payment_method_data { @@ -848,7 +852,7 @@ pub async fn create_payment_method( let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); - db.find_customer_by_merchant_reference_id_merchant_id( + db.find_customer_by_global_id( &(state.into()), &customer_id, merchant_account.get_id(), @@ -961,7 +965,7 @@ pub async fn payment_method_intent_create( let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.to_owned(); - db.find_customer_by_merchant_reference_id_merchant_id( + db.find_customer_by_global_id( &(state.into()), &customer_id, merchant_account.get_id(), @@ -1049,7 +1053,7 @@ pub async fn payment_method_intent_confirm( )?; let customer_id = payment_method.customer_id.to_owned(); - db.find_customer_by_merchant_reference_id_merchant_id( + db.find_customer_by_global_id( &(state.into()), &customer_id, merchant_account.get_id(), @@ -1132,7 +1136,7 @@ pub async fn payment_method_intent_confirm( pub async fn create_payment_method_in_db( state: &SessionState, req: &api::PaymentMethodCreate, - customer_id: &id_type::CustomerId, + customer_id: &id_type::GlobalCustomerId, payment_method_id: id_type::GlobalPaymentMethodId, locker_id: Option, merchant_id: &id_type::MerchantId, @@ -1195,7 +1199,7 @@ pub async fn create_payment_method_in_db( pub async fn create_payment_method_for_intent( state: &SessionState, metadata: Option, - customer_id: &id_type::CustomerId, + customer_id: &id_type::GlobalCustomerId, payment_method_id: id_type::GlobalPaymentMethodId, merchant_id: &id_type::MerchantId, key_store: &domain::MerchantKeyStore, @@ -1421,7 +1425,7 @@ pub async fn list_customer_payment_method_util( profile: domain::Profile, key_store: domain::MerchantKeyStore, req: Option, - customer_id: Option, + customer_id: Option, is_payment_associated: bool, ) -> RouterResponse { let limit = req.as_ref().and_then(|pml_req| pml_req.limit); @@ -1481,7 +1485,7 @@ pub async fn list_customer_payment_method( profile: domain::Profile, key_store: domain::MerchantKeyStore, payment_intent: Option, - customer_id: &id_type::CustomerId, + customer_id: &id_type::GlobalCustomerId, limit: Option, is_payment_associated: bool, ) -> RouterResponse { @@ -1491,7 +1495,7 @@ pub async fn list_customer_payment_method( let customer = db .find_customer_by_global_id( key_manager_state, - customer_id.get_string_repr(), + customer_id, merchant_account.get_id(), &key_store, merchant_account.storage_scheme, @@ -1514,7 +1518,7 @@ pub async fn list_customer_payment_method( .transpose()?; let saved_payment_methods = db - .find_payment_method_by_customer_id_merchant_id_status( + .find_payment_method_by_global_customer_id_merchant_id_status( key_manager_state, &key_store, customer_id, @@ -1901,7 +1905,7 @@ pub async fn delete_payment_method( let _customer = db .find_customer_by_global_id( key_manager_state, - payment_method.customer_id.get_string_repr(), + &payment_method.customer_id, merchant_account.get_id(), &key_store, merchant_account.storage_scheme, diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 135759611082..2dba211e24ab 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -58,12 +58,14 @@ use router_env::{instrument, tracing}; use serde_json::json; use strum::IntoEnumIterator; -use super::{ - migration::RecordMigrationStatusBuilder, - surcharge_decision_configs::{ - perform_surcharge_decision_management_for_payment_method_list, - perform_surcharge_decision_management_for_saved_cards, - }, +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] +use super::migration; +use super::surcharge_decision_configs::{ + perform_surcharge_decision_management_for_payment_method_list, + perform_surcharge_decision_management_for_saved_cards, }; #[cfg(all( any(feature = "v2", feature = "v1"), @@ -80,9 +82,7 @@ use crate::{ }, core::{ errors::{self, StorageErrorExt}, - payment_methods::{ - migration, network_tokenization, transformers as payment_methods, vault, - }, + payment_methods::{network_tokenization, transformers as payment_methods, vault}, payments::{ helpers, routing::{self, SessionFlowRoutingInput}, @@ -411,7 +411,7 @@ pub async fn migrate_payment_method( let should_require_connector_mandate_details = req.network_token.is_none(); - let mut migration_status = RecordMigrationStatusBuilder::new(); + let mut migration_status = migration::RecordMigrationStatusBuilder::new(); let resp = match card_number_validation_result { Ok(card_number) => { @@ -508,6 +508,10 @@ pub async fn migrate_payment_method( todo!() } +#[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") +))] pub async fn populate_bin_details_for_masked_card( card_details: &api_models::payment_methods::MigrateCardDetail, db: &dyn db::StorageInterface, @@ -716,7 +720,7 @@ pub async fn skip_locker_call_and_migrate_payment_method( merchant_account: &domain::MerchantAccount, card: api_models::payment_methods::CardDetailFromLocker, should_require_connector_mandate_details: bool, - migration_status: &mut RecordMigrationStatusBuilder, + migration_status: &mut migration::RecordMigrationStatusBuilder, ) -> errors::RouterResponse { let db = &*state.store; let customer_id = req.customer_id.clone().get_required_value("customer_id")?; @@ -1107,7 +1111,7 @@ pub async fn get_client_secret_or_add_payment_method_for_migration( req: api::PaymentMethodCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - migration_status: &mut RecordMigrationStatusBuilder, + migration_status: &mut migration::RecordMigrationStatusBuilder, ) -> errors::RouterResponse { let merchant_id = merchant_account.get_id(); let customer_id = req.customer_id.clone().get_required_value("customer_id")?; @@ -1705,7 +1709,7 @@ pub async fn save_migration_payment_method( req: api::PaymentMethodCreate, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, - migration_status: &mut RecordMigrationStatusBuilder, + migration_status: &mut migration::RecordMigrationStatusBuilder, ) -> errors::RouterResponse { req.validate()?; let db = &*state.store; @@ -2533,7 +2537,7 @@ pub async fn delete_card_from_locker( #[cfg(all(feature = "v2", feature = "customer_v2"))] pub async fn delete_card_by_locker_id( state: &routes::SessionState, - id: &str, + id: &id_type::GlobalCustomerId, merchant_id: &id_type::MerchantId, ) -> errors::RouterResult { todo!() diff --git a/crates/router/src/core/payment_methods/network_tokenization.rs b/crates/router/src/core/payment_methods/network_tokenization.rs index cab515d9bad2..bb730caad2b5 100644 --- a/crates/router/src/core/payment_methods/network_tokenization.rs +++ b/crates/router/src/core/payment_methods/network_tokenization.rs @@ -308,6 +308,7 @@ pub async fn make_card_network_tokenization_request( } } +#[cfg(feature = "v1")] pub async fn get_network_token( state: &routes::SessionState, customer_id: id_type::CustomerId, @@ -375,6 +376,7 @@ pub async fn get_network_token( Ok(token_response) } +#[cfg(feature = "v1")] pub async fn get_token_from_tokenization_service( state: &routes::SessionState, network_token_requestor_ref_id: String, @@ -441,6 +443,7 @@ pub async fn get_token_from_tokenization_service( Ok(network_token_data) } +#[cfg(feature = "v1")] pub async fn do_status_check_for_network_token( state: &routes::SessionState, payment_method_info: &domain::PaymentMethod, diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 58490271079f..e942288078da 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -5850,6 +5850,7 @@ pub fn filter_network_tokenization_supported_connectors( .collect() } +#[cfg(feature = "v1")] pub async fn decide_action_type( state: &SessionState, is_connector_agnostic_mit_enabled: Option, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 84256f1b296d..78cfdda33b49 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2447,6 +2447,7 @@ pub async fn make_pm_data<'a, F: Clone, R, D>( Ok((operation, payment_method, pm_id)) } +#[cfg(feature = "v1")] pub async fn store_in_vault_and_generate_ppmt( state: &SessionState, payment_method_data: &domain::PaymentMethodData, diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index f6c47de37623..374a33026da8 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -276,7 +276,7 @@ impl Domain Domain Domain Domain( // TODO: Take Globalid and convert to connector reference id let customer_id = customer .to_owned() - .map(|customer| customer.id.clone()) - .map(std::borrow::Cow::Owned) - .map(common_utils::id_type::CustomerId::try_from) + .map(|customer| common_utils::id_type::CustomerId::try_from(customer.id.clone())) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( @@ -540,9 +538,7 @@ pub async fn construct_payment_router_data_for_sdk_session<'a>( // TODO: Take Globalid and convert to connector reference id let customer_id = customer .to_owned() - .map(|customer| customer.id.clone()) - .map(std::borrow::Cow::Owned) - .map(common_utils::id_type::CustomerId::try_from) + .map(|customer| common_utils::id_type::CustomerId::try_from(customer.id.clone())) .transpose() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable( diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 94946b7e0e0c..609414c2920c 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -1269,11 +1269,11 @@ pub async fn create_recipient( #[cfg(all(feature = "v2", feature = "customer_v2"))] { - let global_id = "temp_id".to_string(); + let customer_id = customer.get_id().clone(); payout_data.customer_details = Some( db.update_customer_by_global_id( &state.into(), - global_id, + &customer_id, customer, merchant_account.get_id(), updated_customer, diff --git a/crates/router/src/db/customers.rs b/crates/router/src/db/customers.rs index ae23f81e2ead..f1845be53737 100644 --- a/crates/router/src/db/customers.rs +++ b/crates/router/src/db/customers.rs @@ -129,7 +129,7 @@ where async fn update_customer_by_global_id( &self, state: &KeyManagerState, - id: String, + id: &id_type::GlobalCustomerId, customer: customer::Customer, merchant_id: &id_type::MerchantId, customer_update: storage_types::CustomerUpdate, @@ -141,7 +141,7 @@ where async fn find_customer_by_global_id( &self, state: &KeyManagerState, - id: &str, + id: &id_type::GlobalCustomerId, merchant_id: &id_type::MerchantId, key_store: &domain::MerchantKeyStore, storage_scheme: MerchantStorageScheme, @@ -540,7 +540,6 @@ mod storage { ) .await .change_context(errors::StorageError::DecryptionError)?; - //.await match result.name { Some(ref name) if name.peek() == REDACTED => { @@ -608,7 +607,6 @@ mod storage { ) .await .change_context(errors::StorageError::DecryptionError)?; - //.await match result.name { Some(ref name) if name.peek() == REDACTED => { @@ -686,8 +684,10 @@ mod storage { .map_err(|error| report!(errors::StorageError::from(error))) } MerchantStorageScheme::RedisKv => { - let key = PartitionKey::GlobalId { id: &id }; - let field = format!("cust_{}", id); + let key = PartitionKey::GlobalId { + id: id.get_string_repr(), + }; + let field = format!("cust_{}", id.get_string_repr()); let redis_entry = kv::TypedSql { op: kv::DBOperation::Insert { @@ -712,7 +712,7 @@ mod storage { Ok(redis_interface::HsetnxReply::KeyNotSet) => { Err(report!(errors::StorageError::DuplicateValue { entity: "customer", - key: Some(id.to_string()), + key: Some(id.get_string_repr().to_owned()), })) } Ok(redis_interface::HsetnxReply::KeySet) => Ok(storage_customer), @@ -832,7 +832,7 @@ mod storage { async fn find_customer_by_global_id( &self, state: &KeyManagerState, - id: &str, + id: &id_type::GlobalCustomerId, _merchant_id: &id_type::MerchantId, key_store: &domain::MerchantKeyStore, storage_scheme: MerchantStorageScheme, @@ -852,8 +852,10 @@ mod storage { let customer = match storage_scheme { MerchantStorageScheme::PostgresOnly => database_call().await, MerchantStorageScheme::RedisKv => { - let key = PartitionKey::GlobalId { id }; - let field = format!("cust_{}", id); + let key = PartitionKey::GlobalId { + id: id.get_string_repr(), + }; + let field = format!("cust_{}", id.get_string_repr()); Box::pin(db_utils::try_redis_get_else_try_database_get( async { kv_wrapper( @@ -878,13 +880,11 @@ mod storage { ) .await .change_context(errors::StorageError::DecryptionError)?; - //.await - match result.name { - Some(ref name) if name.peek() == REDACTED => { - Err(errors::StorageError::CustomerRedacted)? - } - _ => Ok(result), + if result.status == common_enums::DeleteStatus::Redacted { + Err(report!(errors::StorageError::CustomerRedacted)) + } else { + Ok(result) } } @@ -893,7 +893,7 @@ mod storage { async fn update_customer_by_global_id( &self, state: &KeyManagerState, - id: String, + id: &id_type::GlobalCustomerId, customer: customer::Customer, _merchant_id: &id_type::MerchantId, customer_update: storage_types::CustomerUpdate, @@ -913,8 +913,10 @@ mod storage { .await .map_err(|error| report!(errors::StorageError::from(error))) }; - let key = PartitionKey::GlobalId { id: &id }; - let field = format!("cust_{}", id); + let key = PartitionKey::GlobalId { + id: id.get_string_repr(), + }; + let field = format!("cust_{}", id.get_string_repr()); let storage_scheme = Box::pin(decide_storage_scheme::<_, diesel_models::Customer>( self, storage_scheme, @@ -1284,7 +1286,7 @@ mod storage { async fn update_customer_by_global_id( &self, state: &KeyManagerState, - id: String, + id: &id_type::GlobalCustomerId, customer: customer::Customer, merchant_id: &id_type::MerchantId, customer_update: storage_types::CustomerUpdate, @@ -1308,7 +1310,7 @@ mod storage { async fn find_customer_by_global_id( &self, state: &KeyManagerState, - id: &str, + id: &id_type::GlobalCustomerId, merchant_id: &id_type::MerchantId, key_store: &domain::MerchantKeyStore, _storage_scheme: MerchantStorageScheme, @@ -1526,7 +1528,7 @@ impl CustomerInterface for MockDb { async fn update_customer_by_global_id( &self, _state: &KeyManagerState, - _id: String, + _id: &id_type::GlobalCustomerId, _customer: customer::Customer, _merchant_id: &id_type::MerchantId, _customer_update: storage_types::CustomerUpdate, @@ -1541,7 +1543,7 @@ impl CustomerInterface for MockDb { async fn find_customer_by_global_id( &self, _state: &KeyManagerState, - _id: &str, + _id: &id_type::GlobalCustomerId, _merchant_id: &id_type::MerchantId, _key_store: &domain::MerchantKeyStore, _storage_scheme: MerchantStorageScheme, diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index a7e71284aafd..f49173b35496 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -449,7 +449,7 @@ impl CustomerInterface for KafkaStore { async fn update_customer_by_global_id( &self, state: &KeyManagerState, - id: String, + id: &id_type::GlobalCustomerId, customer: domain::Customer, merchant_id: &id_type::MerchantId, customer_update: storage::CustomerUpdate, @@ -525,7 +525,7 @@ impl CustomerInterface for KafkaStore { async fn find_customer_by_global_id( &self, state: &KeyManagerState, - id: &str, + id: &id_type::GlobalCustomerId, merchant_id: &id_type::MerchantId, key_store: &domain::MerchantKeyStore, storage_scheme: MerchantStorageScheme, @@ -862,11 +862,13 @@ impl MandateInterface for KafkaStore { } #[cfg(all(feature = "v2", feature = "customer_v2"))] - async fn find_mandate_by_global_id( + async fn find_mandate_by_global_customer_id( &self, - id: &str, + id: &id_type::GlobalCustomerId, ) -> CustomResult, errors::StorageError> { - self.diesel_store.find_mandate_by_global_id(id).await + self.diesel_store + .find_mandate_by_global_customer_id(id) + .await } async fn find_mandate_by_merchant_id_customer_id( @@ -1909,18 +1911,22 @@ impl PaymentMethodInterface for KafkaStore { } #[cfg(all(feature = "v2", feature = "customer_v2"))] - async fn find_payment_method_list_by_global_id( + async fn find_payment_method_list_by_global_customer_id( &self, state: &KeyManagerState, key_store: &domain::MerchantKeyStore, - id: &str, + id: &id_type::GlobalCustomerId, limit: Option, - ) -> CustomResult, errors::StorageError> { + ) -> CustomResult, errors::StorageError> { self.diesel_store - .find_payment_method_list_by_global_id(state, key_store, id, limit) + .find_payment_method_list_by_global_customer_id(state, key_store, id, limit) .await } + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] async fn find_payment_method_by_customer_id_merchant_id_status( &self, state: &KeyManagerState, @@ -1944,6 +1950,30 @@ impl PaymentMethodInterface for KafkaStore { .await } + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_payment_method_by_global_customer_id_merchant_id_status( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + status: common_enums::PaymentMethodStatus, + limit: Option, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .find_payment_method_by_global_customer_id_merchant_id_status( + state, + key_store, + customer_id, + merchant_id, + status, + limit, + storage_scheme, + ) + .await + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/router/src/db/mandate.rs b/crates/router/src/db/mandate.rs index 076cb768e611..236c1d526a10 100644 --- a/crates/router/src/db/mandate.rs +++ b/crates/router/src/db/mandate.rs @@ -30,9 +30,9 @@ pub trait MandateInterface { // Fix this function once we move to mandate v2 #[cfg(all(feature = "v2", feature = "customer_v2"))] - async fn find_mandate_by_global_id( + async fn find_mandate_by_global_customer_id( &self, - id: &str, + id: &id_type::GlobalCustomerId, ) -> CustomResult, errors::StorageError>; async fn update_mandate_by_merchant_id_mandate_id( @@ -201,12 +201,12 @@ mod storage { #[cfg(all(feature = "v2", feature = "customer_v2"))] #[instrument(skip_all)] - async fn find_mandate_by_global_id( + async fn find_mandate_by_global_customer_id( &self, - id: &str, + id: &id_type::GlobalCustomerId, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; - storage_types::Mandate::find_by_global_id(&conn, id) + storage_types::Mandate::find_by_global_customer_id(&conn, id) .await .map_err(|error| report!(errors::StorageError::from(error))) } @@ -459,9 +459,9 @@ mod storage { // Need to fix this once we start moving to mandate v2 #[cfg(all(feature = "v2", feature = "customer_v2"))] #[instrument(skip_all)] - async fn find_mandate_by_global_id( + async fn find_mandate_by_global_customer_id( &self, - customer_id: &str, + customer_id: &id_type::GlobalCustomerId, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; storage_types::Mandate::find_by_global_id(&conn, customer_id) @@ -572,9 +572,9 @@ impl MandateInterface for MockDb { // Need to fix this once we move to v2 mandate #[cfg(all(feature = "v2", feature = "customer_v2"))] - async fn find_mandate_by_global_id( + async fn find_mandate_by_global_customer_id( &self, - id: &str, + id: &id_type::GlobalCustomerId, ) -> CustomResult, errors::StorageError> { todo!() } diff --git a/crates/router/src/db/payment_method.rs b/crates/router/src/db/payment_method.rs index 28a3ae7ec095..cc5af40b30ab 100644 --- a/crates/router/src/db/payment_method.rs +++ b/crates/router/src/db/payment_method.rs @@ -62,14 +62,18 @@ pub trait PaymentMethodInterface { // Need to fix this once we start moving to v2 for payment method #[cfg(all(feature = "v2", feature = "customer_v2"))] - async fn find_payment_method_list_by_global_id( + async fn find_payment_method_list_by_global_customer_id( &self, state: &KeyManagerState, key_store: &domain::MerchantKeyStore, - id: &str, + id: &id_type::GlobalCustomerId, limit: Option, - ) -> CustomResult, errors::StorageError>; + ) -> CustomResult, errors::StorageError>; + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] #[allow(clippy::too_many_arguments)] async fn find_payment_method_by_customer_id_merchant_id_status( &self, @@ -82,6 +86,19 @@ pub trait PaymentMethodInterface { storage_scheme: MerchantStorageScheme, ) -> CustomResult, errors::StorageError>; + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[allow(clippy::too_many_arguments)] + async fn find_payment_method_by_global_customer_id_merchant_id_status( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + status: common_enums::PaymentMethodStatus, + limit: Option, + storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError>; + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -698,16 +715,41 @@ mod storage { feature = "customer_v2", feature = "payment_methods_v2" ))] - async fn find_payment_method_list_by_global_id( + async fn find_payment_method_list_by_global_customer_id( &self, - _state: &KeyManagerState, - _key_store: &domain::MerchantKeyStore, - _id: &str, - _limit: Option, - ) -> CustomResult, errors::StorageError> { - todo!() + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + customer_id: &id_type::GlobalCustomerId, + limit: Option, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + let payment_methods = + storage_types::PaymentMethod::find_by_global_customer_id(&conn, customer_id, limit) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + + let pm_futures = payment_methods + .into_iter() + .map(|pm| async { + pm.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .collect::>(); + + let domain_payment_methods = futures::future::try_join_all(pm_futures).await?; + + Ok(domain_payment_methods) } + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] #[instrument(skip_all)] async fn find_payment_method_by_customer_id_merchant_id_status( &self, @@ -785,6 +827,48 @@ mod storage { Ok(domain_payment_methods) } + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[instrument(skip_all)] + async fn find_payment_method_by_global_customer_id_merchant_id_status( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + status: common_enums::PaymentMethodStatus, + limit: Option, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + let payment_methods = + storage_types::PaymentMethod::find_by_global_customer_id_merchant_id_status( + &conn, + customer_id, + merchant_id, + status, + limit, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + + let pm_futures = payment_methods + .into_iter() + .map(|pm| async { + pm.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .collect::>(); + + let domain_payment_methods = futures::future::try_join_all(pm_futures).await?; + + Ok(domain_payment_methods) + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -1112,19 +1196,41 @@ mod storage { // Need to fix this once we move to payment method for customer #[cfg(all(feature = "v2", feature = "customer_v2"))] #[instrument(skip_all)] - async fn find_payment_method_list_by_global_id( + async fn find_payment_method_list_by_global_customer_id( &self, state: &KeyManagerState, key_store: &domain::MerchantKeyStore, - id: &str, + id: &id_type::GlobalCustomerId, limit: Option, - ) -> CustomResult, errors::StorageError> { + ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; - storage_types::PaymentMethod::find_by_global_id(&conn, id, limit) - .await - .map_err(|error| report!(errors::StorageError::from(error))) + let payment_methods = + storage_types::PaymentMethod::find_by_global_customer_id(&conn, customer_id, limit) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + + let pm_futures = payment_methods + .into_iter() + .map(|pm| async { + pm.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .collect::>(); + + let domain_payment_methods = futures::future::try_join_all(pm_futures).await?; + + Ok(domain_payment_methods) } + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] #[instrument(skip_all)] async fn find_payment_method_by_customer_id_merchant_id_status( &self, @@ -1166,6 +1272,48 @@ mod storage { Ok(domain_payment_methods) } + #[cfg(all(feature = "v2", feature = "customer_v2"))] + #[instrument(skip_all)] + async fn find_payment_method_by_global_customer_id_merchant_id_status( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + status: common_enums::PaymentMethodStatus, + limit: Option, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + let payment_methods = + storage_types::PaymentMethod::find_by_global_customer_id_merchant_id_status( + &conn, + customer_id, + merchant_id, + status, + limit, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error)))?; + + let pm_futures = payment_methods + .into_iter() + .map(|pm| async { + pm.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .collect::>(); + + let domain_payment_methods = futures::future::try_join_all(pm_futures).await?; + + Ok(domain_payment_methods) + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") @@ -1426,16 +1574,20 @@ impl PaymentMethodInterface for MockDb { // Need to fix this once we complete v2 payment method #[cfg(all(feature = "v2", feature = "customer_v2"))] - async fn find_payment_method_list_by_global_id( + async fn find_payment_method_list_by_global_customer_id( &self, state: &KeyManagerState, key_store: &domain::MerchantKeyStore, - _id: &str, + _id: &id_type::GlobalCustomerId, _limit: Option, - ) -> CustomResult, errors::StorageError> { + ) -> CustomResult, errors::StorageError> { todo!() } + #[cfg(all( + any(feature = "v1", feature = "v2"), + not(feature = "payment_methods_v2") + ))] async fn find_payment_method_by_customer_id_merchant_id_status( &self, state: &KeyManagerState, @@ -1482,6 +1634,53 @@ impl PaymentMethodInterface for MockDb { } } + #[cfg(all(feature = "v2", feature = "customer_v2"))] + async fn find_payment_method_by_global_customer_id_merchant_id_status( + &self, + state: &KeyManagerState, + key_store: &domain::MerchantKeyStore, + customer_id: &id_type::GlobalCustomerId, + merchant_id: &id_type::MerchantId, + status: common_enums::PaymentMethodStatus, + _limit: Option, + _storage_scheme: MerchantStorageScheme, + ) -> CustomResult, errors::StorageError> { + let payment_methods = self.payment_methods.lock().await; + let payment_methods_found: Vec = payment_methods + .iter() + .filter(|pm| { + pm.customer_id == *customer_id + && pm.merchant_id == *merchant_id + && pm.status == status + }) + .cloned() + .collect(); + + if payment_methods_found.is_empty() { + Err( + errors::StorageError::ValueNotFound("cannot find payment methods".to_string()) + .into(), + ) + } else { + let pm_futures = payment_methods_found + .into_iter() + .map(|pm| async { + pm.convert( + state, + key_store.key.get_inner(), + key_store.merchant_id.clone().into(), + ) + .await + .change_context(errors::StorageError::DecryptionError) + }) + .collect::>(); + + let domain_payment_methods = futures::future::try_join_all(pm_futures).await?; + + Ok(domain_payment_methods) + } + } + #[cfg(all( any(feature = "v1", feature = "v2"), not(feature = "payment_methods_v2") diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 5f04581fbb39..766679491c54 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -973,8 +973,8 @@ impl Customers { .service(web::resource("").route(web::post().to(customers_create))) .service( web::resource("/{id}") - .route(web::post().to(customers_update)) - .route(web::post().to(customers_retrieve)) + .route(web::put().to(customers_update)) + .route(web::get().to(customers_retrieve)) .route(web::delete().to(customers_delete)), ) } diff --git a/crates/router/src/routes/customers.rs b/crates/router/src/routes/customers.rs index 5ff155966a0f..09b6983d2a7c 100644 --- a/crates/router/src/routes/customers.rs +++ b/crates/router/src/routes/customers.rs @@ -1,5 +1,4 @@ use actix_web::{web, HttpRequest, HttpResponse, Responder}; -#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] use common_utils::id_type; use router_env::{instrument, tracing, Flow}; @@ -46,10 +45,7 @@ pub async fn customers_retrieve( ) -> HttpResponse { let flow = Flow::CustomersRetrieve; - let payload = web::Json(customers::CustomerId::new_customer_id_struct( - path.into_inner(), - )) - .into_inner(); + let customer_id = path.into_inner(); let auth = if auth::is_jwt_auth(req.headers()) { Box::new(auth::JWTAuth { @@ -66,14 +62,14 @@ pub async fn customers_retrieve( flow, state, &req, - payload, - |state, auth: auth::AuthenticationData, req, _| { + customer_id, + |state, auth: auth::AuthenticationData, customer_id, _| { retrieve_customer( state, auth.merchant_account, auth.profile_id, auth.key_store, - req, + customer_id, ) }, &*auth, @@ -87,11 +83,11 @@ pub async fn customers_retrieve( pub async fn customers_retrieve( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, ) -> HttpResponse { let flow = Flow::CustomersRetrieve; - let payload = web::Json(customers::GlobalId::new(path.into_inner())).into_inner(); + let id = path.into_inner(); let auth = if auth::is_jwt_auth(req.headers()) { Box::new(auth::JWTAuth { @@ -108,9 +104,9 @@ pub async fn customers_retrieve( flow, state, &req, - payload, - |state, auth: auth::AuthenticationData, req, _| { - retrieve_customer(state, auth.merchant_account, auth.key_store, req) + id, + |state, auth: auth::AuthenticationData, id, _| { + retrieve_customer(state, auth.merchant_account, auth.key_store, id) }, &*auth, api_locking::LockAction::NotApplicable, @@ -159,24 +155,27 @@ pub async fn customers_update( state: web::Data, req: HttpRequest, path: web::Path, - mut json_payload: web::Json, + json_payload: web::Json, ) -> HttpResponse { let flow = Flow::CustomersUpdate; let customer_id = path.into_inner(); - json_payload.customer_id = Some(customer_id); - let customer_update_id = customers::UpdateCustomerId::new("temp_global_id".to_string()); + let request = json_payload.into_inner(); + let request_internal = customers::CustomerUpdateRequestInternal { + customer_id, + request, + }; + Box::pin(api::server_wrap( flow, state, &req, - json_payload.into_inner(), - |state, auth: auth::AuthenticationData, req, _| { + request_internal, + |state, auth: auth::AuthenticationData, request_internal, _| { update_customer( state, auth.merchant_account, - req, + request_internal, auth.key_store, - customer_update_id.clone(), ) }, auth::auth_type( @@ -196,24 +195,25 @@ pub async fn customers_update( pub async fn customers_update( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, json_payload: web::Json, ) -> HttpResponse { let flow = Flow::CustomersUpdate; - let id = path.into_inner().clone(); - let customer_update_id = customers::UpdateCustomerId::new(id); + let id = path.into_inner(); + let request = json_payload.into_inner(); + let request_internal = customers::CustomerUpdateRequestInternal { id, request }; + Box::pin(api::server_wrap( flow, state, &req, - json_payload.into_inner(), - |state, auth: auth::AuthenticationData, req, _| { + request_internal, + |state, auth: auth::AuthenticationData, request_internal, _| { update_customer( state, auth.merchant_account, - req, + request_internal, auth.key_store, - customer_update_id.clone(), ) }, auth::auth_type( @@ -233,18 +233,18 @@ pub async fn customers_update( pub async fn customers_delete( state: web::Data, req: HttpRequest, - path: web::Path, + path: web::Path, ) -> impl Responder { let flow = Flow::CustomersDelete; - let payload = web::Json(customers::GlobalId::new(path.into_inner())).into_inner(); + let id = path.into_inner(); Box::pin(api::server_wrap( flow, state, &req, - payload, - |state, auth: auth::AuthenticationData, req, _| { - delete_customer(state, auth.merchant_account, req, auth.key_store) + id, + |state, auth: auth::AuthenticationData, id, _| { + delete_customer(state, auth.merchant_account, id, auth.key_store) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -266,18 +266,15 @@ pub async fn customers_delete( path: web::Path, ) -> impl Responder { let flow = Flow::CustomersDelete; - let payload = web::Json(customers::CustomerId { - customer_id: path.into_inner(), - }) - .into_inner(); + let customer_id = path.into_inner(); Box::pin(api::server_wrap( flow, state, &req, - payload, - |state, auth: auth::AuthenticationData, req, _| { - delete_customer(state, auth.merchant_account, req, auth.key_store) + customer_id, + |state, auth: auth::AuthenticationData, customer_id, _| { + delete_customer(state, auth.merchant_account, customer_id, auth.key_store) }, auth::auth_type( &auth::HeaderAuth(auth::ApiKeyAuth), @@ -299,21 +296,19 @@ pub async fn get_customer_mandates( path: web::Path, ) -> impl Responder { let flow = Flow::CustomersGetMandates; - let customer_id = customers::CustomerId { - customer_id: path.into_inner(), - }; + let customer_id = path.into_inner(); Box::pin(api::server_wrap( flow, state, &req, customer_id, - |state, auth: auth::AuthenticationData, req, _| { + |state, auth: auth::AuthenticationData, customer_id, _| { crate::core::mandate::get_customer_mandates( state, auth.merchant_account, auth.key_store, - req, + customer_id, ) }, auth::auth_type( diff --git a/crates/router/src/routes/ephemeral_key.rs b/crates/router/src/routes/ephemeral_key.rs index dd11bee24bdf..e6a33c59192e 100644 --- a/crates/router/src/routes/ephemeral_key.rs +++ b/crates/router/src/routes/ephemeral_key.rs @@ -9,7 +9,6 @@ use super::AppState; use crate::{ core::{api_locking, payments::helpers}, services::{api, authentication as auth}, - types::api::customers, }; #[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "customer_v2")))] @@ -17,7 +16,7 @@ use crate::{ pub async fn ephemeral_key_create( state: web::Data, req: HttpRequest, - json_payload: web::Json, + json_payload: web::Json, ) -> HttpResponse { let flow = Flow::EphemeralKeyCreate; let payload = json_payload.into_inner(); @@ -26,10 +25,10 @@ pub async fn ephemeral_key_create( state, &req, payload, - |state, auth: auth::AuthenticationData, req, _| { + |state, auth: auth::AuthenticationData, payload, _| { helpers::make_ephemeral_key( state, - req.get_merchant_reference_id(), + payload.customer_id, auth.merchant_account.get_id().to_owned(), ) }, diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 8ee31ecf943a..c27a25677af2 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -554,13 +554,13 @@ pub async fn list_customer_payment_method_for_payment( #[instrument(skip_all, fields(flow = ?Flow::CustomerPaymentMethodsList))] pub async fn list_customer_payment_method_api( state: web::Data, - customer_id: web::Path<(id_type::CustomerId,)>, + customer_id: web::Path, req: HttpRequest, query_payload: web::Query, ) -> HttpResponse { let flow = Flow::CustomerPaymentMethodsList; let payload = query_payload.into_inner(); - let customer_id = customer_id.into_inner().0.clone(); + let customer_id = customer_id.into_inner(); let ephemeral_or_api_auth = match auth::is_ephemeral_auth(req.headers()) { Ok(auth) => auth, diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 7a8e65e0604f..2893cb8fa044 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -119,7 +119,7 @@ pub async fn payments_create_intent( } }; let global_payment_id = - common_utils::id_type::GlobalPaymentId::generate(&state.conf.cell_information.id.clone()); + common_utils::id_type::GlobalPaymentId::generate(&state.conf.cell_information.id); Box::pin(api::server_wrap( flow, diff --git a/crates/router/src/types/api/customers.rs b/crates/router/src/types/api/customers.rs index 9cfec6f7b5bb..5b37fb2add6e 100644 --- a/crates/router/src/types/api/customers.rs +++ b/crates/router/src/types/api/customers.rs @@ -1,9 +1,7 @@ use api_models::customers; -#[cfg(all(feature = "v2", feature = "customer_v2"))] -pub use api_models::customers::GlobalId; pub use api_models::customers::{ - CustomerDeleteResponse, CustomerId, CustomerListRequest, CustomerRequest, - CustomerUpdateRequest, UpdateCustomerId, + CustomerDeleteResponse, CustomerListRequest, CustomerRequest, CustomerUpdateRequest, + CustomerUpdateRequestInternal, }; #[cfg(all(feature = "v2", feature = "customer_v2"))] use hyperswitch_domain_models::customer; @@ -50,6 +48,7 @@ impl ForeignFrom<(domain::Customer, Option)> for Custo impl ForeignFrom for CustomerResponse { fn foreign_from(cust: domain::Customer) -> Self { customers::CustomerResponse { + id: cust.id, merchant_reference_id: cust.merchant_reference_id, name: cust.name, email: cust.email, @@ -61,7 +60,6 @@ impl ForeignFrom for CustomerResponse { default_billing_address: None, default_shipping_address: None, default_payment_method_id: cust.default_payment_method_id, - id: cust.id, } .into() } diff --git a/crates/storage_impl/src/lib.rs b/crates/storage_impl/src/lib.rs index 1d48606bf006..09bb42567df4 100644 --- a/crates/storage_impl/src/lib.rs +++ b/crates/storage_impl/src/lib.rs @@ -479,7 +479,7 @@ impl UniqueConstraints for diesel_models::Customer { #[cfg(all(feature = "v2", feature = "customer_v2"))] impl UniqueConstraints for diesel_models::Customer { fn unique_constraints(&self) -> Vec { - vec![format!("customer_{}", self.id.clone())] + vec![format!("customer_{}", self.id.get_string_repr())] } fn table_name(&self) -> &str { "Customer" From c154a385597104fcdbed4aa859c52c97a240c39f Mon Sep 17 00:00:00 2001 From: Pa1NarK <69745008+pixincreate@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:54:39 +0530 Subject: [PATCH 18/20] fix(connector): add expiry year conversion for adyen mit transactions (#6851) --- crates/router/src/connector/adyen/transformers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/adyen/transformers.rs b/crates/router/src/connector/adyen/transformers.rs index 6f9fd33ef1c6..d0dad91f7170 100644 --- a/crates/router/src/connector/adyen/transformers.rs +++ b/crates/router/src/connector/adyen/transformers.rs @@ -2671,7 +2671,7 @@ impl .card_exp_month .clone(), expiry_year: card_details_for_network_transaction_id - .card_exp_year + .get_expiry_year_4_digit() .clone(), cvc: None, holder_name: card_holder_name, From e8bfd0e2270300fff3f051143f34ebb782da5366 Mon Sep 17 00:00:00 2001 From: Prajjwal Kumar Date: Tue, 17 Dec 2024 15:55:50 +0530 Subject: [PATCH 19/20] refactor(constraint_graph): handle PML for cases where setup_future_usage is not passed in payments (#6810) --- crates/router/src/core/payment_methods/cards.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index 2dba211e24ab..9d8fbf58f5e7 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -4593,7 +4593,7 @@ pub async fn filter_payment_methods( .map(|future_usage| { future_usage == common_enums::FutureUsage::OnSession }) - .unwrap_or(false) + .unwrap_or(true) }) .and_then(|res| { res.then(|| { From b301d09213a8c1c68d711a3b34227d13e61e52f9 Mon Sep 17 00:00:00 2001 From: Sai Harsha Vardhan <56996463+sai-harsha-vardhan@users.noreply.github.com> Date: Tue, 17 Dec 2024 17:19:56 +0530 Subject: [PATCH 20/20] chore(wasm): add wasm changes for ctp_mastercard connector (#6838) --- crates/connector_configs/src/common_config.rs | 5 ++ crates/connector_configs/src/connector.rs | 5 ++ .../connector_configs/toml/development.toml | 63 ++++++++++++++++++- crates/connector_configs/toml/sandbox.toml | 63 ++++++++++++++++++- 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/crates/connector_configs/src/common_config.rs b/crates/connector_configs/src/common_config.rs index 8f6e03fb3987..599d22ae3f67 100644 --- a/crates/connector_configs/src/common_config.rs +++ b/crates/connector_configs/src/common_config.rs @@ -102,6 +102,11 @@ pub struct ApiModelMetaData { pub source_balance_account: Option, pub brand_id: Option, pub destination_account_number: Option, + pub dpa_id: Option, + pub dpa_name: Option, + pub locale: Option, + pub card_brands: Option>, + pub merchant_category_code: Option, } #[serde_with::skip_serializing_none] diff --git a/crates/connector_configs/src/connector.rs b/crates/connector_configs/src/connector.rs index 7e893a6415e3..1af99f110208 100644 --- a/crates/connector_configs/src/connector.rs +++ b/crates/connector_configs/src/connector.rs @@ -114,6 +114,11 @@ pub struct ConfigMetadata { pub source_balance_account: Option, pub brand_id: Option, pub destination_account_number: Option, + pub dpa_id: Option, + pub dpa_name: Option, + pub locale: Option, + pub card_brands: Option>, + pub merchant_category_code: Option, } #[serde_with::skip_serializing_none] diff --git a/crates/connector_configs/toml/development.toml b/crates/connector_configs/toml/development.toml index 1f2062cdea3a..fcf0dca100cb 100644 --- a/crates/connector_configs/toml/development.toml +++ b/crates/connector_configs/toml/development.toml @@ -4373,4 +4373,65 @@ merchant_secret="Source verification key" [elavon.connector_auth.SignatureKey] api_key="Account Id" key1="User ID" -api_secret="Pin" \ No newline at end of file +api_secret="Pin" + +[ctp_mastercard] +[ctp_mastercard.connector_auth.HeaderKey] +api_key="API Key" + +[ctp_mastercard.metadata.dpa_id] +name="dpa_id" +label="DPA Id" +placeholder="Enter DPA Id" +required=true +type="Text" + +[ctp_mastercard.metadata.dpa_name] +name="dpa_name" +label="DPA Name" +placeholder="Enter DPA Name" +required=true +type="Text" + +[ctp_mastercard.metadata.locale] +name="locale" +label="Locale" +placeholder="Enter locale" +required=true +type="Text" + +[[ctp_mastercard.metadata.card_brands]] +name="card_brands" +label="Card Brands" +placeholder="Enter Card Brands" +required=true +type="MultiSelect" +options=["visa","mastercard"] + +[[ctp_mastercard.metadata.acquirer_bin]] +name="acquirer_bin" +label="Acquire Bin" +placeholder="Enter Acquirer Bin" +required=true +type="Text" + +[[ctp_mastercard.metadata.acquirer_merchant_id]] +name="acquirer_merchant_id" +label="Acquire Merchant Id" +placeholder="Enter Acquirer Merchant Id" +required=true +type="Text" + +[[ctp_mastercard.metadata.merchant_category_code]] +name="merchant_category_code" +label="Merchant Category Code" +placeholder="Enter Merchant Category Code" +required=true +type="Text" + +[[ctp_mastercard.metadata.merchant_country_code]] +name="merchant_country_code" +label="Merchant Country Code" +placeholder="Enter Merchant Country Code" +required=true +type="Text" diff --git a/crates/connector_configs/toml/sandbox.toml b/crates/connector_configs/toml/sandbox.toml index 1e9d274780b7..4d3f6ede1403 100644 --- a/crates/connector_configs/toml/sandbox.toml +++ b/crates/connector_configs/toml/sandbox.toml @@ -4318,4 +4318,65 @@ merchant_secret="Source verification key" [elavon.connector_auth.SignatureKey] api_key="Account Id" key1="User ID" -api_secret="Pin" \ No newline at end of file +api_secret="Pin" + +[ctp_mastercard] +[ctp_mastercard.connector_auth.HeaderKey] +api_key="API Key" + +[ctp_mastercard.metadata.dpa_id] +name="dpa_id" +label="DPA Id" +placeholder="Enter DPA Id" +required=true +type="Text" + +[ctp_mastercard.metadata.dpa_name] +name="dpa_name" +label="DPA Name" +placeholder="Enter DPA Name" +required=true +type="Text" + +[ctp_mastercard.metadata.locale] +name="locale" +label="Locale" +placeholder="Enter locale" +required=true +type="Text" + +[[ctp_mastercard.metadata.card_brands]] +name="card_brands" +label="Card Brands" +placeholder="Enter Card Brands" +required=true +type="MultiSelect" +options=["visa","mastercard"] + +[[ctp_mastercard.metadata.acquirer_bin]] +name="acquirer_bin" +label="Acquire Bin" +placeholder="Enter Acquirer Bin" +required=true +type="Text" + +[[ctp_mastercard.metadata.acquirer_merchant_id]] +name="acquirer_merchant_id" +label="Acquire Merchant Id" +placeholder="Enter Acquirer Merchant Id" +required=true +type="Text" + +[[ctp_mastercard.metadata.merchant_category_code]] +name="merchant_category_code" +label="Merchant Category Code" +placeholder="Enter Merchant Category Code" +required=true +type="Text" + +[[ctp_mastercard.metadata.merchant_country_code]] +name="merchant_country_code" +label="Merchant Country Code" +placeholder="Enter Merchant Country Code" +required=true +type="Text"