diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 525f65ea72..743ddf8254 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -5957,6 +5957,46 @@ ] } }, + "/profile_acquirers": { + "post": { + "tags": [ + "Profile Acquirer" + ], + "summary": "Profile Acquirer - Create", + "description": "Create a new Profile Acquirer for accessing our APIs from your servers.", + "operationId": "Create a Profile Acquirer", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileAcquirerCreate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Profile Acquirer created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProfileAcquirerResponse" + } + } + } + }, + "400": { + "description": "Invalid data" + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, "/three_ds_decision/execute": { "post": { "tags": [ @@ -6307,6 +6347,63 @@ } } }, + "AcquirerConfig": { + "type": "object", + "description": "Acquirer configuration", + "required": [ + "acquirer_assigned_merchant_id", + "merchant_name", + "merchant_country_code", + "network", + "acquirer_bin", + "acquirer_fraud_rate" + ], + "properties": { + "acquirer_assigned_merchant_id": { + "type": "string", + "description": "The merchant id assigned by the acquirer", + "example": "M123456789" + }, + "merchant_name": { + "type": "string", + "description": "merchant name", + "example": "NewAge Retailer" + }, + "merchant_country_code": { + "type": "string", + "description": "Merchant country code assigned by acquirer", + "example": "US" + }, + "network": { + "type": "string", + "description": "Network provider", + "example": "VISA" + }, + "acquirer_bin": { + "type": "string", + "description": "Acquirer bin", + "example": "456789" + }, + "acquirer_ica": { + "type": "string", + "description": "Acquirer ica provided by acquirer", + "example": "401288", + "nullable": true + }, + "acquirer_fraud_rate": { + "type": "string", + "description": "Fraud rate for the particular acquirer configuration", + "example": "0.01" + } + } + }, + "AcquirerConfigMap": { + "type": "object", + "description": "Acquirer configs", + "additionalProperties": { + "$ref": "#/components/schemas/AcquirerConfig" + } + }, "AcquirerData": { "type": "object", "description": "Represents data about the acquirer used in the 3DS decision rule.", @@ -25296,6 +25393,124 @@ "accommodation" ] }, + "ProfileAcquirerCreate": { + "type": "object", + "required": [ + "acquirer_assigned_merchant_id", + "merchant_name", + "merchant_country_code", + "network", + "acquirer_bin", + "acquirer_fraud_rate", + "profile_id" + ], + "properties": { + "acquirer_assigned_merchant_id": { + "type": "string", + "description": "The merchant id assigned by the acquirer", + "example": "M123456789" + }, + "merchant_name": { + "type": "string", + "description": "merchant name", + "example": "NewAge Retailer" + }, + "merchant_country_code": { + "type": "string", + "description": "Merchant country code assigned by acquirer", + "example": "US" + }, + "network": { + "type": "string", + "description": "Network provider", + "example": "VISA" + }, + "acquirer_bin": { + "type": "string", + "description": "Acquirer bin", + "example": "456789" + }, + "acquirer_ica": { + "type": "string", + "description": "Acquirer ica provided by acquirer", + "example": "401288", + "nullable": true + }, + "acquirer_fraud_rate": { + "type": "number", + "format": "double", + "description": "Fraud rate for the particular acquirer configuration", + "example": 0.01 + }, + "profile_id": { + "type": "string", + "description": "Parent profile id to link the acquirer account with", + "example": "pro_ky0yNyOXXlA5hF8JzE5q" + } + } + }, + "ProfileAcquirerResponse": { + "type": "object", + "required": [ + "profile_acquirer_id", + "acquirer_assigned_merchant_id", + "merchant_name", + "merchant_country_code", + "network", + "acquirer_bin", + "acquirer_fraud_rate", + "profile_id" + ], + "properties": { + "profile_acquirer_id": { + "type": "string", + "description": "The unique identifier of the profile acquirer", + "example": "pro_acq_LCRdERuylQvNQ4qh3QE0" + }, + "acquirer_assigned_merchant_id": { + "type": "string", + "description": "The merchant id assigned by the acquirer", + "example": "M123456789" + }, + "merchant_name": { + "type": "string", + "description": "Merchant name", + "example": "NewAge Retailer" + }, + "merchant_country_code": { + "type": "string", + "description": "Merchant country code assigned by acquirer", + "example": "US" + }, + "network": { + "type": "string", + "description": "Network provider", + "example": "VISA" + }, + "acquirer_bin": { + "type": "string", + "description": "Acquirer bin", + "example": "456789" + }, + "acquirer_ica": { + "type": "string", + "description": "Acquirer ica provided by acquirer", + "example": "401288", + "nullable": true + }, + "acquirer_fraud_rate": { + "type": "number", + "format": "double", + "description": "Fraud rate for the particular acquirer configuration", + "example": 0.01 + }, + "profile_id": { + "type": "string", + "description": "Parent profile id to link the acquirer account with", + "example": "pro_ky0yNyOXXlA5hF8JzE5q" + } + } + }, "ProfileCreate": { "type": "object", "properties": { @@ -25818,6 +26033,14 @@ "default": false, "example": false }, + "acquirer_configs": { + "allOf": [ + { + "$ref": "#/components/schemas/AcquirerConfigMap" + } + ], + "nullable": true + }, "is_iframe_redirection_enabled": { "type": "boolean", "description": "Indicates if the redirection has to open in the iframe", diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 5ab7eae75f..07f3e2431f 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2427,6 +2427,10 @@ pub struct ProfileResponse { #[schema(default = false, example = false)] pub is_pre_network_tokenization_enabled: bool, + /// Acquirer configs + #[schema(value_type = Option)] + pub acquirer_configs: Option, + /// Indicates if the redirection has to open in the iframe #[schema(example = false)] pub is_iframe_redirection_enabled: Option, diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index a2258e4bd5..5fdb56e696 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -32,6 +32,7 @@ pub mod payouts; pub mod pm_auth; pub mod poll; pub mod process_tracker; +pub mod profile_acquirer; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] pub mod proxy; #[cfg(feature = "recon")] diff --git a/crates/api_models/src/profile_acquirer.rs b/crates/api_models/src/profile_acquirer.rs new file mode 100644 index 0000000000..86f36db104 --- /dev/null +++ b/crates/api_models/src/profile_acquirer.rs @@ -0,0 +1,66 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::enums; + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ProfileAcquirerCreate { + /// The merchant id assigned by the acquirer + #[schema(value_type= String,example = "M123456789")] + pub acquirer_assigned_merchant_id: String, + /// merchant name + #[schema(value_type= String,example = "NewAge Retailer")] + pub merchant_name: String, + /// Merchant country code assigned by acquirer + #[schema(value_type= String,example = "US")] + pub merchant_country_code: enums::CountryAlpha2, + /// Network provider + #[schema(value_type= String,example = "VISA")] + pub network: common_enums::enums::CardNetwork, + /// Acquirer bin + #[schema(value_type= String,example = "456789")] + pub acquirer_bin: String, + /// Acquirer ica provided by acquirer + #[schema(value_type= Option,example = "401288")] + pub acquirer_ica: Option, + /// Fraud rate for the particular acquirer configuration + #[schema(value_type= f64,example = 0.01)] + pub acquirer_fraud_rate: f64, + /// Parent profile id to link the acquirer account with + #[schema(value_type= String,example = "pro_ky0yNyOXXlA5hF8JzE5q")] + pub profile_id: common_utils::id_type::ProfileId, +} + +#[derive(Debug, Serialize, Deserialize, ToSchema)] +pub struct ProfileAcquirerResponse { + /// The unique identifier of the profile acquirer + #[schema(value_type= String,example = "pro_acq_LCRdERuylQvNQ4qh3QE0")] + pub profile_acquirer_id: common_utils::id_type::ProfileAcquirerId, + /// The merchant id assigned by the acquirer + #[schema(value_type= String,example = "M123456789")] + pub acquirer_assigned_merchant_id: String, + /// Merchant name + #[schema(value_type= String,example = "NewAge Retailer")] + pub merchant_name: String, + /// Merchant country code assigned by acquirer + #[schema(value_type= String,example = "US")] + pub merchant_country_code: enums::CountryAlpha2, + /// Network provider + #[schema(value_type= String,example = "VISA")] + pub network: common_enums::enums::CardNetwork, + /// Acquirer bin + #[schema(value_type= String,example = "456789")] + pub acquirer_bin: String, + /// Acquirer ica provided by acquirer + #[schema(value_type= Option,example = "401288")] + pub acquirer_ica: Option, + /// Fraud rate for the particular acquirer configuration + #[schema(value_type= f64,example = 0.01)] + pub acquirer_fraud_rate: f64, + /// Parent profile id to link the acquirer account with + #[schema(value_type= String,example = "pro_ky0yNyOXXlA5hF8JzE5q")] + pub profile_id: common_utils::id_type::ProfileId, +} + +impl common_utils::events::ApiEventMetric for ProfileAcquirerCreate {} +impl common_utils::events::ApiEventMetric for ProfileAcquirerResponse {} diff --git a/crates/common_types/src/domain.rs b/crates/common_types/src/domain.rs index 0361f5b0e4..ca6a59abd6 100644 --- a/crates/common_types/src/domain.rs +++ b/crates/common_types/src/domain.rs @@ -1,5 +1,7 @@ //! Common types +use std::collections::HashMap; + use common_enums::enums; use common_utils::{impl_to_sql_from_sql_json, types::MinorUnit}; use diesel::{sql_types::Jsonb, AsExpression, FromSqlRow}; @@ -53,3 +55,36 @@ pub struct XenditSplitSubMerchantData { pub for_user_id: String, } impl_to_sql_from_sql_json!(XenditSplitSubMerchantData); + +/// Acquirer configuration +#[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] +pub struct AcquirerConfig { + /// The merchant id assigned by the acquirer + #[schema(value_type= String,example = "M123456789")] + pub acquirer_assigned_merchant_id: String, + /// merchant name + #[schema(value_type= String,example = "NewAge Retailer")] + pub merchant_name: String, + /// Merchant country code assigned by acquirer + #[schema(value_type= String,example = "US")] + pub merchant_country_code: common_enums::CountryAlpha2, + /// Network provider + #[schema(value_type= String,example = "VISA")] + pub network: common_enums::CardNetwork, + /// Acquirer bin + #[schema(value_type= String,example = "456789")] + pub acquirer_bin: String, + /// Acquirer ica provided by acquirer + #[schema(value_type= Option,example = "401288")] + pub acquirer_ica: Option, + /// Fraud rate for the particular acquirer configuration + #[schema(value_type= String,example = "0.01")] + pub acquirer_fraud_rate: f64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, FromSqlRow, AsExpression, ToSchema)] +#[diesel(sql_type = Jsonb)] +/// Acquirer configs +pub struct AcquirerConfigMap(pub HashMap); + +impl_to_sql_from_sql_json!(AcquirerConfigMap); diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 39a1694a7c..b06fea4e1e 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -126,6 +126,9 @@ pub enum ApiEventsType { token_id: Option, }, ProcessTracker, + ProfileAcquirer { + profile_acquirer_id: id_type::ProfileAcquirerId, + }, ThreeDsDecisionRule, } diff --git a/crates/common_utils/src/id_type.rs b/crates/common_utils/src/id_type.rs index 34a3844e76..45b65c9a44 100644 --- a/crates/common_utils/src/id_type.rs +++ b/crates/common_utils/src/id_type.rs @@ -11,6 +11,7 @@ mod merchant_connector_account; mod organization; mod payment; mod profile; +mod profile_acquirer; mod refunds; mod relay; mod routing; @@ -46,6 +47,7 @@ pub use self::{ organization::OrganizationId, payment::{PaymentId, PaymentReferenceId}, profile::ProfileId, + profile_acquirer::ProfileAcquirerId, refunds::RefundReferenceId, relay::RelayId, routing::RoutingId, diff --git a/crates/common_utils/src/id_type/profile_acquirer.rs b/crates/common_utils/src/id_type/profile_acquirer.rs new file mode 100644 index 0000000000..ac5f09646b --- /dev/null +++ b/crates/common_utils/src/id_type/profile_acquirer.rs @@ -0,0 +1,41 @@ +use std::str::FromStr; + +crate::id_type!( + ProfileAcquirerId, + "A type for profile_acquirer_id that can be used for profile acquirer ids" +); +crate::impl_id_type_methods!(ProfileAcquirerId, "profile_acquirer_id"); + +// This is to display the `ProfileAcquirerId` as ProfileAcquirerId(abcd) +crate::impl_debug_id_type!(ProfileAcquirerId); +crate::impl_try_from_cow_str_id_type!(ProfileAcquirerId, "profile_acquirer_id"); + +crate::impl_generate_id_id_type!(ProfileAcquirerId, "pro_acq"); +crate::impl_serializable_secret_id_type!(ProfileAcquirerId); +crate::impl_queryable_id_type!(ProfileAcquirerId); +crate::impl_to_sql_from_sql_id_type!(ProfileAcquirerId); + +impl crate::events::ApiEventMetric for ProfileAcquirerId { + fn get_api_event_type(&self) -> Option { + Some(crate::events::ApiEventsType::ProfileAcquirer { + profile_acquirer_id: self.clone(), + }) + } +} + +impl FromStr for ProfileAcquirerId { + type Err = error_stack::Report; + + fn from_str(s: &str) -> Result { + let cow_string = std::borrow::Cow::Owned(s.to_string()); + Self::try_from(cow_string) + } +} + +// This is implemented so that we can use profile acquirer id directly as attribute in metrics +#[cfg(feature = "metrics")] +impl From for router_env::opentelemetry::Value { + fn from(val: ProfileAcquirerId) -> Self { + Self::from(val.0 .0 .0) + } +} diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 759329ba32..3f491d3e9a 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -253,6 +253,13 @@ pub fn generate_merchant_connector_account_id_of_default_length( id_type::MerchantConnectorAccountId::generate() } +/// Generate a profile_acquirer id with default length, with prefix as `mer_acq` +pub fn generate_profile_acquirer_id_of_default_length() -> id_type::ProfileAcquirerId { + use id_type::GenerateId; + + id_type::ProfileAcquirerId::generate() +} + /// Generate a nanoid with the given prefix and a default length #[inline] pub fn generate_id_with_default_len(prefix: &str) -> String { diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 848f673f50..a93bab30a4 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -74,6 +74,7 @@ pub struct Profile { pub is_iframe_redirection_enabled: Option, pub is_pre_network_tokenization_enabled: Option, pub three_ds_decision_rule_algorithm: Option, + pub acquirer_config_map: Option, } #[cfg(feature = "v1")] @@ -183,6 +184,7 @@ pub struct ProfileUpdateInternal { pub is_iframe_redirection_enabled: Option, pub is_pre_network_tokenization_enabled: Option, pub three_ds_decision_rule_algorithm: Option, + pub acquirer_config_map: Option, } #[cfg(feature = "v1")] @@ -234,6 +236,7 @@ impl ProfileUpdateInternal { is_iframe_redirection_enabled, is_pre_network_tokenization_enabled, three_ds_decision_rule_algorithm, + acquirer_config_map, } = self; Profile { profile_id: source.profile_id, @@ -316,6 +319,7 @@ impl ProfileUpdateInternal { .or(source.is_pre_network_tokenization_enabled), three_ds_decision_rule_algorithm: three_ds_decision_rule_algorithm .or(source.three_ds_decision_rule_algorithm), + acquirer_config_map: acquirer_config_map.or(source.acquirer_config_map), } } } @@ -376,6 +380,7 @@ pub struct Profile { pub id: common_utils::id_type::ProfileId, pub is_iframe_redirection_enabled: Option, pub three_ds_decision_rule_algorithm: Option, + pub acquirer_config_map: Option, pub routing_algorithm_id: Option, pub order_fulfillment_time: Option, pub order_fulfillment_time_origin: Option, @@ -665,6 +670,7 @@ impl ProfileUpdateInternal { external_vault_connector_details: external_vault_connector_details .or(source.external_vault_connector_details), three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, } } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 388fcc2375..4e93ecab94 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -228,6 +228,7 @@ diesel::table! { is_iframe_redirection_enabled -> Nullable, is_pre_network_tokenization_enabled -> Nullable, three_ds_decision_rule_algorithm -> Nullable, + acquirer_config_map -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 5b1201027b..0c6bdadac8 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -223,6 +223,7 @@ diesel::table! { id -> Varchar, is_iframe_redirection_enabled -> Nullable, three_ds_decision_rule_algorithm -> Nullable, + acquirer_config_map -> Nullable, #[max_length = 64] routing_algorithm_id -> Nullable, order_fulfillment_time -> Nullable, diff --git a/crates/hyperswitch_domain_models/src/business_profile.rs b/crates/hyperswitch_domain_models/src/business_profile.rs index 80070aa9e5..6c95af4f14 100644 --- a/crates/hyperswitch_domain_models/src/business_profile.rs +++ b/crates/hyperswitch_domain_models/src/business_profile.rs @@ -76,6 +76,7 @@ pub struct Profile { pub is_iframe_redirection_enabled: Option, pub is_pre_network_tokenization_enabled: bool, pub three_ds_decision_rule_algorithm: Option, + pub acquirer_config_map: Option, } #[cfg(feature = "v1")] @@ -188,6 +189,7 @@ impl From for Profile { is_iframe_redirection_enabled: value.is_iframe_redirection_enabled, is_pre_network_tokenization_enabled: value.is_pre_network_tokenization_enabled, three_ds_decision_rule_algorithm: None, // three_ds_decision_rule_algorithm is not yet created during profile creation + acquirer_config_map: None, } } } @@ -274,6 +276,9 @@ pub enum ProfileUpdate { CardTestingSecretKeyUpdate { card_testing_secret_key: OptionalEncryptableName, }, + AcquirerConfigMapUpdate { + acquirer_config_map: Option, + }, } #[cfg(feature = "v1")] @@ -373,6 +378,7 @@ impl From for ProfileUpdateInternal { is_iframe_redirection_enabled, is_pre_network_tokenization_enabled, three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, } } ProfileUpdate::RoutingAlgorithmUpdate { @@ -425,6 +431,7 @@ impl From for ProfileUpdateInternal { is_iframe_redirection_enabled: None, is_pre_network_tokenization_enabled: None, three_ds_decision_rule_algorithm, + acquirer_config_map: None, }, ProfileUpdate::DynamicRoutingAlgorithmUpdate { dynamic_routing_algorithm, @@ -474,6 +481,7 @@ impl From for ProfileUpdateInternal { is_iframe_redirection_enabled: None, is_pre_network_tokenization_enabled: None, three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, }, ProfileUpdate::ExtendedCardInfoUpdate { is_extended_card_info_enabled, @@ -523,6 +531,7 @@ impl From for ProfileUpdateInternal { is_iframe_redirection_enabled: None, is_pre_network_tokenization_enabled: None, three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, }, ProfileUpdate::ConnectorAgnosticMitUpdate { is_connector_agnostic_mit_enabled, @@ -572,6 +581,7 @@ impl From for ProfileUpdateInternal { is_iframe_redirection_enabled: None, is_pre_network_tokenization_enabled: None, three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, }, ProfileUpdate::NetworkTokenizationUpdate { is_network_tokenization_enabled, @@ -621,6 +631,7 @@ impl From for ProfileUpdateInternal { is_iframe_redirection_enabled: None, is_pre_network_tokenization_enabled: None, three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, }, ProfileUpdate::CardTestingSecretKeyUpdate { card_testing_secret_key, @@ -670,6 +681,57 @@ impl From for ProfileUpdateInternal { is_iframe_redirection_enabled: None, is_pre_network_tokenization_enabled: None, three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, + }, + ProfileUpdate::AcquirerConfigMapUpdate { + acquirer_config_map, + } => Self { + profile_name: None, + modified_at: now, + return_url: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + webhook_details: None, + metadata: None, + routing_algorithm: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + is_recon_enabled: None, + applepay_verified_domains: None, + payment_link_config: None, + session_expiry: None, + authentication_connector_details: None, + payout_link_config: None, + is_extended_card_info_enabled: None, + extended_card_info_config: None, + is_connector_agnostic_mit_enabled: None, + use_billing_as_payment_method_billing: None, + collect_shipping_details_from_wallet_connector: None, + collect_billing_details_from_wallet_connector: None, + outgoing_webhook_custom_http_headers: None, + always_collect_billing_details_from_wallet_connector: None, + always_collect_shipping_details_from_wallet_connector: None, + tax_connector_id: None, + is_tax_connector_enabled: None, + dynamic_routing_algorithm: None, + is_network_tokenization_enabled: None, + is_auto_retries_enabled: None, + max_auto_retries_enabled: None, + always_request_extended_authorization: None, + is_click_to_pay_enabled: None, + authentication_product_ids: None, + card_testing_guard_config: None, + card_testing_secret_key: None, + is_clear_pan_retries_enabled: None, + force_3ds_challenge: None, + is_debit_routing_enabled: None, + merchant_business_country: None, + is_iframe_redirection_enabled: None, + is_pre_network_tokenization_enabled: None, + three_ds_decision_rule_algorithm: None, + acquirer_config_map, }, } } @@ -739,6 +801,7 @@ impl super::behaviour::Conversion for Profile { is_iframe_redirection_enabled: self.is_iframe_redirection_enabled, is_pre_network_tokenization_enabled: Some(self.is_pre_network_tokenization_enabled), three_ds_decision_rule_algorithm: self.three_ds_decision_rule_algorithm, + acquirer_config_map: self.acquirer_config_map, }) } @@ -834,6 +897,7 @@ impl super::behaviour::Conversion for Profile { .is_pre_network_tokenization_enabled .unwrap_or(false), three_ds_decision_rule_algorithm: item.three_ds_decision_rule_algorithm, + acquirer_config_map: item.acquirer_config_map, }) } .await @@ -1825,6 +1889,7 @@ impl super::behaviour::Conversion for Profile { is_external_vault_enabled: self.is_external_vault_enabled, external_vault_connector_details: self.external_vault_connector_details, three_ds_decision_rule_algorithm: None, + acquirer_config_map: None, }) } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index d0585fd98c..4f8b96d5fb 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -203,6 +203,9 @@ Never share your secret api keys. Keep them guarded and secure. // Routes for poll apis routes::poll::retrieve_poll_status, + // Routes for profile acquirer account + routes::profile_acquirer::profile_acquirer_create, + // Routes for 3DS Decision Rule routes::three_ds_decision_rule::three_ds_decision_rule_execute, ), @@ -274,6 +277,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payment_methods::PaymentMethodResponse, api_models::payment_methods::CustomerPaymentMethod, common_types::three_ds_decision_rule_engine::ThreeDSDecisionRule, + common_types::domain::AcquirerConfigMap, + common_types::domain::AcquirerConfig, api_models::payment_methods::PaymentMethodListResponse, api_models::payment_methods::ResponsePaymentMethodsEnabled, api_models::payment_methods::ResponsePaymentMethodTypes, @@ -773,6 +778,8 @@ Never share your secret api keys. Keep them guarded and secure. api_models::open_router::DecisionEngineGatewayWiseExtraScore, api_models::open_router::DecisionEngineSRSubLevelInputConfig, api_models::open_router::DecisionEngineEliminationData, + api_models::profile_acquirer::ProfileAcquirerCreate, + api_models::profile_acquirer::ProfileAcquirerResponse, euclid::frontend::dir::enums::CustomerDevicePlatform, euclid::frontend::dir::enums::CustomerDeviceType, euclid::frontend::dir::enums::CustomerDeviceDisplaySize, diff --git a/crates/openapi/src/routes.rs b/crates/openapi/src/routes.rs index 23d9a79a4d..c91dd74f0f 100644 --- a/crates/openapi/src/routes.rs +++ b/crates/openapi/src/routes.rs @@ -15,6 +15,7 @@ pub mod payments; pub mod payouts; pub mod poll; pub mod profile; +pub mod profile_acquirer; pub mod proxy; pub mod refunds; pub mod relay; diff --git a/crates/openapi/src/routes/profile_acquirer.rs b/crates/openapi/src/routes/profile_acquirer.rs new file mode 100644 index 0000000000..2c3467aeb6 --- /dev/null +++ b/crates/openapi/src/routes/profile_acquirer.rs @@ -0,0 +1,18 @@ +#[cfg(feature = "v1")] +/// Profile Acquirer - Create +/// +/// Create a new Profile Acquirer for accessing our APIs from your servers. +#[utoipa::path( + post, + path = "/profile_acquirers", + request_body = ProfileAcquirerCreate, + responses( + (status = 200, description = "Profile Acquirer created", body = ProfileAcquirerResponse), + (status = 400, description = "Invalid data") + ), + tag = "Profile Acquirer", + operation_id = "Create a Profile Acquirer", + security(("api_key" = [])) +)] +pub async fn profile_acquirer_create() { /* … */ +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index e5e97b5dc0..a46f974279 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -61,6 +61,7 @@ pub mod verification; pub mod verify_connector; pub mod webhooks; +pub mod profile_acquirer; pub mod unified_authentication_service; #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index ebc462d286..04b3e05d1e 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -372,13 +372,13 @@ pub async fn payouts_confirm_core( merchant_context: domain::MerchantContext, req: payouts::PayoutCreateRequest, ) -> RouterResponse { - let mut payout_data = make_payout_data( + let mut payout_data = Box::pin(make_payout_data( &state, &merchant_context, None, &payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())), &state.locale, - ) + )) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); let status = payout_attempt.status; @@ -435,13 +435,13 @@ pub async fn payouts_update_core( req: payouts::PayoutCreateRequest, ) -> RouterResponse { let payout_id = req.payout_id.clone().get_required_value("payout_id")?; - let mut payout_data = make_payout_data( + let mut payout_data = Box::pin(make_payout_data( &state, &merchant_context, None, &payouts::PayoutRequest::PayoutCreateRequest(Box::new(req.to_owned())), &state.locale, - ) + )) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); @@ -509,13 +509,13 @@ pub async fn payouts_retrieve_core( profile_id: Option, req: payouts::PayoutRetrieveRequest, ) -> RouterResponse { - let mut payout_data = make_payout_data( + let mut payout_data = Box::pin(make_payout_data( &state, &merchant_context, profile_id, &payouts::PayoutRequest::PayoutRetrieveRequest(req.to_owned()), &state.locale, - ) + )) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); let status = payout_attempt.status; @@ -550,13 +550,13 @@ pub async fn payouts_cancel_core( merchant_context: domain::MerchantContext, req: payouts::PayoutActionRequest, ) -> RouterResponse { - let mut payout_data = make_payout_data( + let mut payout_data = Box::pin(make_payout_data( &state, &merchant_context, None, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), &state.locale, - ) + )) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); @@ -641,13 +641,13 @@ pub async fn payouts_fulfill_core( merchant_context: domain::MerchantContext, req: payouts::PayoutActionRequest, ) -> RouterResponse { - let mut payout_data = make_payout_data( + let mut payout_data = Box::pin(make_payout_data( &state, &merchant_context, None, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), &state.locale, - ) + )) .await?; let payout_attempt = payout_data.payout_attempt.to_owned(); diff --git a/crates/router/src/core/profile_acquirer.rs b/crates/router/src/core/profile_acquirer.rs new file mode 100644 index 0000000000..747059e540 --- /dev/null +++ b/crates/router/src/core/profile_acquirer.rs @@ -0,0 +1,114 @@ +use api_models::profile_acquirer; +use common_utils::types::keymanager::KeyManagerState; +use error_stack::ResultExt; + +use crate::{ + core::errors::{self, utils::StorageErrorExt, RouterResponse}, + services::api, + types::domain, + SessionState, +}; + +#[cfg(all(feature = "olap", feature = "v1"))] +pub async fn create_profile_acquirer( + state: SessionState, + request: profile_acquirer::ProfileAcquirerCreate, + merchant_context: domain::MerchantContext, +) -> RouterResponse { + let db = state.store.as_ref(); + let profile_acquirer_id = common_utils::generate_profile_acquirer_id_of_default_length(); + let key_manager_state: KeyManagerState = (&state).into(); + let merchant_key_store = merchant_context.get_merchant_key_store(); + + let mut business_profile = db + .find_business_profile_by_profile_id( + &key_manager_state, + merchant_key_store, + &request.profile_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::ProfileNotFound { + id: request.profile_id.get_string_repr().to_owned(), + })?; + + let incoming_acquirer_config = common_types::domain::AcquirerConfig { + acquirer_assigned_merchant_id: request.acquirer_assigned_merchant_id.clone(), + merchant_name: request.merchant_name.clone(), + merchant_country_code: request.merchant_country_code, + network: request.network.clone(), + acquirer_bin: request.acquirer_bin.clone(), + acquirer_ica: request.acquirer_ica.clone(), + acquirer_fraud_rate: request.acquirer_fraud_rate, + }; + + // Check for duplicates before proceeding + + business_profile + .acquirer_config_map + .as_ref() + .map_or(Ok(()), |configs_wrapper| { + match configs_wrapper.0.values().any(|existing_config| existing_config == &incoming_acquirer_config) { + true => Err(error_stack::report!( + errors::ApiErrorResponse::GenericDuplicateError { + message: format!( + "Duplicate acquirer configuration found for profile_id: {}. Conflicting configuration: {:?}", + request.profile_id.get_string_repr(), + incoming_acquirer_config + ), + } + )), + false => Ok(()), + } + })?; + + // Get a mutable reference to the HashMap inside AcquirerConfigMap, + // initializing if it's None or the inner HashMap is not present. + let configs_map = &mut business_profile + .acquirer_config_map + .get_or_insert_with(|| { + common_types::domain::AcquirerConfigMap(std::collections::HashMap::new()) + }) + .0; + + configs_map.insert( + profile_acquirer_id.clone(), + incoming_acquirer_config.clone(), + ); + + let profile_update = domain::ProfileUpdate::AcquirerConfigMapUpdate { + acquirer_config_map: business_profile.acquirer_config_map.clone(), + }; + let updated_business_profile = db + .update_profile_by_profile_id( + &key_manager_state, + merchant_key_store, + business_profile, + profile_update, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to update business profile with new acquirer config")?; + + let updated_acquire_details = updated_business_profile + .acquirer_config_map + .as_ref() + .and_then(|acquirer_configs_wrapper| acquirer_configs_wrapper.0.get(&profile_acquirer_id)) + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get updated acquirer config")?; + + let response = profile_acquirer::ProfileAcquirerResponse { + profile_acquirer_id, + profile_id: request.profile_id.clone(), + acquirer_assigned_merchant_id: updated_acquire_details + .acquirer_assigned_merchant_id + .clone(), + merchant_name: updated_acquire_details.merchant_name.clone(), + merchant_country_code: updated_acquire_details.merchant_country_code, + network: updated_acquire_details.network.clone(), + acquirer_bin: updated_acquire_details.acquirer_bin.clone(), + acquirer_ica: updated_acquire_details.acquirer_ica.clone(), + acquirer_fraud_rate: updated_acquire_details.acquirer_fraud_rate, + }; + + Ok(api::ApplicationResponse::Json(response)) +} diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index a7a0c76e2f..3f95d61bfb 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -822,13 +822,13 @@ async fn payouts_incoming_webhook_flow( payout_id: payouts.payout_id.clone(), }); - let payout_data = payouts::make_payout_data( + let payout_data = Box::pin(payouts::make_payout_data( &state, &merchant_context, None, &action_req, common_utils::consts::DEFAULT_LOCALE, - ) + )) .await?; let updated_payout_attempt = db diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index e34fad3a2b..e7136919ce 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -10,8 +10,7 @@ use common_utils::{ #[cfg(feature = "v2")] use diesel_models::ephemeral_key::{ClientSecretType, ClientSecretTypeNew}; use diesel_models::{ - enums, - enums::ProcessTrackerStatus, + enums::{self, ProcessTrackerStatus}, ephemeral_key::{EphemeralKey, EphemeralKeyNew}, reverse_lookup::{ReverseLookup, ReverseLookupNew}, user_role as user_storage, diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index f37b404de4..6a048c59b0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -132,7 +132,8 @@ pub fn mk_app( { server_app = server_app .service(routes::ProfileNew::server(state.clone())) - .service(routes::Forex::server(state.clone())); + .service(routes::Forex::server(state.clone())) + .service(routes::ProfileAcquirer::server(state.clone())); } server_app = server_app.service(routes::Profile::server(state.clone())); diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 7002a725b7..35221104ae 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -41,6 +41,8 @@ pub mod payouts; pub mod pm_auth; pub mod poll; #[cfg(feature = "olap")] +pub mod profile_acquirer; +#[cfg(feature = "olap")] pub mod profiles; #[cfg(feature = "recon")] pub mod recon; @@ -86,8 +88,8 @@ pub use self::app::{ ApiKeys, AppState, ApplePayCertificatesMigration, Cache, Cards, Configs, ConnectorOnboarding, Customers, Disputes, EphemeralKey, FeatureMatrix, Files, Forex, Gsm, Health, Hypersense, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink, PaymentMethods, Payments, - Poll, ProcessTracker, Profile, ProfileNew, Refunds, Relay, RelayWebhooks, SessionState, - ThreeDsDecisionRule, User, Webhooks, + Poll, ProcessTracker, Profile, ProfileAcquirer, ProfileNew, Refunds, Relay, RelayWebhooks, + SessionState, ThreeDsDecisionRule, User, Webhooks, }; #[cfg(feature = "olap")] pub use self::app::{Blocklist, Organization, Routing, Verify, WebhookEvents}; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 82c3e10105..3d2a6e9170 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -85,6 +85,8 @@ use crate::routes::cards_info::{ use crate::routes::feature_matrix; #[cfg(all(feature = "frm", feature = "oltp"))] use crate::routes::fraud_check as frm_routes; +#[cfg(all(feature = "olap", feature = "v1"))] +use crate::routes::profile_acquirer; #[cfg(all(feature = "recon", feature = "olap"))] use crate::routes::recon as recon_routes; pub use crate::{ @@ -2645,3 +2647,17 @@ impl ProcessTracker { ) } } + +#[cfg(feature = "olap")] +pub struct ProfileAcquirer; + +#[cfg(all(feature = "olap", feature = "v1"))] +impl ProfileAcquirer { + pub fn server(state: AppState) -> Scope { + web::scope("/profile_acquirer") + .app_data(web::Data::new(state)) + .service( + web::resource("").route(web::post().to(profile_acquirer::create_profile_acquirer)), + ) + } +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 91225fa39a..9f64e31dcf 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -44,6 +44,7 @@ pub enum ApiIdentifier { PaymentMethodSession, ProcessTracker, Proxy, + ProfileAcquirer, ThreeDsDecisionRule, GenericTokenization, } @@ -345,6 +346,7 @@ impl From for ApiIdentifier { Flow::RevenueRecoveryRetrieve => Self::ProcessTracker, Flow::Proxy => Self::Proxy, + Flow::ProfileAcquirerCreate => Self::ProfileAcquirer, Flow::ThreeDsDecisionRuleExecute => Self::ThreeDsDecisionRule, Flow::TokenizationCreate | Flow::TokenizationRetrieve => Self::GenericTokenization, } diff --git a/crates/router/src/routes/profile_acquirer.rs b/crates/router/src/routes/profile_acquirer.rs new file mode 100644 index 0000000000..4e8a5eeffa --- /dev/null +++ b/crates/router/src/routes/profile_acquirer.rs @@ -0,0 +1,50 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::profile_acquirer::ProfileAcquirerCreate; +use router_env::{instrument, tracing, Flow}; + +use super::app::AppState; +use crate::{ + core::api_locking, + services::{api, authentication as auth, authorization::permissions::Permission}, + types::domain, +}; + +#[cfg(all(feature = "olap", feature = "v1"))] +#[instrument(skip_all, fields(flow = ?Flow::ProfileAcquirerCreate))] +pub async fn create_profile_acquirer( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::ProfileAcquirerCreate; + let payload = json_payload.into_inner(); + + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state: super::SessionState, auth_data, req, _| { + let merchant_context = domain::MerchantContext::NormalMerchant(Box::new( + domain::Context(auth_data.merchant_account, auth_data.key_store), + )); + crate::core::profile_acquirer::create_profile_acquirer( + state, + req, + merchant_context.clone(), + ) + }, + auth::auth_type( + &auth::HeaderAuth(auth::ApiKeyAuth { + is_connected_allowed: false, + is_platform_allowed: true, + }), + &auth::JWTAuth { + permission: Permission::ProfileAccountWrite, + }, + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index cbca021f1c..41069a784b 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -197,6 +197,7 @@ impl ForeignTryFrom for ProfileResponse { is_debit_routing_enabled: Some(item.is_debit_routing_enabled), merchant_business_country: item.merchant_business_country, is_pre_network_tokenization_enabled: item.is_pre_network_tokenization_enabled, + acquirer_configs: item.acquirer_config_map, is_iframe_redirection_enabled: item.is_iframe_redirection_enabled, }) } diff --git a/crates/router/src/workflows/attach_payout_account_workflow.rs b/crates/router/src/workflows/attach_payout_account_workflow.rs index 910afe3166..9704ce06d3 100644 --- a/crates/router/src/workflows/attach_payout_account_workflow.rs +++ b/crates/router/src/workflows/attach_payout_account_workflow.rs @@ -53,9 +53,14 @@ impl ProcessTrackerWorkflow for AttachPayoutAccountWorkflow { merchant_account.clone(), key_store.clone(), ))); - let mut payout_data = - payouts::make_payout_data(state, &merchant_context, None, &request, DEFAULT_LOCALE) - .await?; + let mut payout_data = Box::pin(payouts::make_payout_data( + state, + &merchant_context, + None, + &request, + DEFAULT_LOCALE, + )) + .await?; payouts::payouts_core(state, &merchant_context, &mut payout_data, None, None).await?; diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs index 56265c788f..d525763529 100644 --- a/crates/router/src/workflows/outgoing_webhook_retry.rs +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -542,13 +542,13 @@ async fn get_outgoing_webhook_content_and_event_type( payout_models::PayoutActionRequest { payout_id }, ); - let payout_data = payouts::make_payout_data( + let payout_data = Box::pin(payouts::make_payout_data( &state, &merchant_context, None, &request, DEFAULT_LOCALE, - ) + )) .await?; let router_response = diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 12fd6108b6..27ab6dd7c1 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -594,6 +594,7 @@ pub enum Flow { CloneConnector, ///Proxy Flow Proxy, + ProfileAcquirerCreate, /// ThreeDs Decision Rule Execute flow ThreeDsDecisionRuleExecute, } diff --git a/migrations/2025-06-05-122346_add-acquirer-config-in-business-profile/down.sql b/migrations/2025-06-05-122346_add-acquirer-config-in-business-profile/down.sql new file mode 100644 index 0000000000..01490869cf --- /dev/null +++ b/migrations/2025-06-05-122346_add-acquirer-config-in-business-profile/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE business_profile DROP COLUMN IF EXISTS acquirer_config_map; \ No newline at end of file diff --git a/migrations/2025-06-05-122346_add-acquirer-config-in-business-profile/up.sql b/migrations/2025-06-05-122346_add-acquirer-config-in-business-profile/up.sql new file mode 100644 index 0000000000..ac326b3ca4 --- /dev/null +++ b/migrations/2025-06-05-122346_add-acquirer-config-in-business-profile/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS acquirer_config_map JSONB; \ No newline at end of file