diff --git a/.github/workflows/cypress-tests-runner.yml b/.github/workflows/cypress-tests-runner.yml index 3c999cc2be..815030af2d 100644 --- a/.github/workflows/cypress-tests-runner.yml +++ b/.github/workflows/cypress-tests-runner.yml @@ -162,6 +162,11 @@ jobs: toolchain: stable 2 weeks ago components: clippy + - name: Install Protoc + uses: arduino/setup-protoc@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install sccache if: ${{ env.RUN_TESTS == 'true' }} uses: taiki-e/install-action@v2 diff --git a/Cargo.lock b/Cargo.lock index 8d135c11bc..848ccc6648 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -354,7 +354,7 @@ dependencies = [ "common_utils", "currency_conversion", "diesel_models", - "error-stack", + "error-stack 0.4.1", "futures 0.3.31", "hyperswitch_interfaces", "masking", @@ -459,7 +459,7 @@ dependencies = [ "common_enums", "common_types", "common_utils", - "error-stack", + "error-stack 0.4.1", "euclid", "masking", "mime", @@ -1214,10 +1214,13 @@ checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" dependencies = [ "axum-core 0.5.2", "bytes 1.10.1", + "form_urlencoded", "futures-util", "http 1.3.1", "http-body 1.0.1", "http-body-util", + "hyper 1.6.0", + "hyper-util", "itoa", "matchit 0.8.4", "memchr", @@ -1226,10 +1229,15 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", "sync_wrapper 1.0.1", + "tokio 1.45.1", "tower 0.5.2", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1269,6 +1277,7 @@ dependencies = [ "sync_wrapper 1.0.1", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -1609,7 +1618,7 @@ name = "cards" version = "0.1.0" dependencies = [ "common_utils", - "error-stack", + "error-stack 0.4.1", "masking", "regex", "router_env", @@ -1857,7 +1866,7 @@ dependencies = [ "bytes 1.10.1", "common_enums", "diesel", - "error-stack", + "error-stack 0.4.1", "fake", "futures 0.3.31", "globset", @@ -2597,7 +2606,7 @@ dependencies = [ "common_types", "common_utils", "diesel", - "error-stack", + "error-stack 0.4.1", "masking", "router_derive", "router_env", @@ -2683,7 +2692,7 @@ dependencies = [ "config", "diesel", "diesel_models", - "error-stack", + "error-stack 0.4.1", "external_services", "hyperswitch_interfaces", "masking", @@ -2889,6 +2898,16 @@ dependencies = [ "serde", ] +[[package]] +name = "error-stack" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" +dependencies = [ + "anyhow", + "rustc_version 0.4.1", +] + [[package]] name = "etcetera" version = "0.8.0" @@ -2974,7 +2993,7 @@ dependencies = [ name = "events" version = "0.1.0" dependencies = [ - "error-stack", + "error-stack 0.4.1", "masking", "router_env", "serde", @@ -2998,7 +3017,7 @@ dependencies = [ "base64 0.22.1", "common_utils", "dyn-clone", - "error-stack", + "error-stack 0.4.1", "hex", "http 0.2.12", "http-body-util", @@ -3013,6 +3032,7 @@ dependencies = [ "quick-xml", "reqwest 0.11.27", "router_env", + "rust-grpc-client", "serde", "thiserror 1.0.69", "tokio 1.45.1", @@ -3325,6 +3345,18 @@ dependencies = [ "slab", ] +[[package]] +name = "g2h" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "312ad594dd0e3c26f860a52c8b703ab509c546931920f277f1afa9b7127fd755" +dependencies = [ + "heck 0.5.0", + "prost-build", + "quote", + "tonic-build", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -3471,6 +3503,24 @@ dependencies = [ "subtle", ] +[[package]] +name = "grpc-api-types" +version = "0.1.0" +source = "git+https://github.com/juspay/connector-service?rev=afaef3427f815befe253da152e5f37d3858bbc9f#afaef3427f815befe253da152e5f37d3858bbc9f" +dependencies = [ + "axum 0.8.4", + "error-stack 0.5.0", + "g2h", + "heck 0.5.0", + "http 1.3.1", + "prost", + "prost-build", + "prost-types", + "serde", + "tonic 0.13.1", + "tonic-build", +] + [[package]] name = "h2" version = "0.3.26" @@ -3951,7 +4001,7 @@ dependencies = [ "common_utils", "crc", "encoding_rs", - "error-stack", + "error-stack 0.4.1", "hex", "html-escape", "http 0.2.12", @@ -4015,7 +4065,7 @@ dependencies = [ "common_types", "common_utils", "diesel_models", - "error-stack", + "error-stack 0.4.1", "futures 0.3.31", "http 0.2.12", "masking", @@ -4042,7 +4092,7 @@ dependencies = [ "common_enums", "common_utils", "dyn-clone", - "error-stack", + "error-stack 0.4.1", "http 0.2.12", "hyperswitch_domain_models", "masking", @@ -5595,7 +5645,7 @@ dependencies = [ "common_utils", "csv", "dyn-clone", - "error-stack", + "error-stack 0.4.1", "hyperswitch_domain_models", "hyperswitch_interfaces", "masking", @@ -5852,7 +5902,7 @@ dependencies = [ "bytes 1.10.1", "common_enums", "common_utils", - "error-stack", + "error-stack 0.4.1", "http 0.2.12", "masking", "mime", @@ -5992,9 +6042,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2ecbe40f08db5c006b5764a2645f7f3f141ce756412ac9e1dd6087e6d32995" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" dependencies = [ "bytes 1.10.1", "prost-derive", @@ -6002,11 +6052,10 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8650aabb6c35b860610e9cff5dc1af886c9e25073b7b1712a68972af4281302" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "bytes 1.10.1", "heck 0.5.0", "itertools 0.13.0", "log", @@ -6023,9 +6072,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf0c195eebb4af52c752bec4f52f645da98b6e92077a04110c7f349477ae5ac" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" dependencies = [ "anyhow", "itertools 0.13.0", @@ -6036,9 +6085,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.13.2" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60caa6738c7369b940c3d49246a8d1749323674c65cb13010134f5c9bad5b519" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" dependencies = [ "prost", ] @@ -6360,7 +6409,7 @@ name = "redis_interface" version = "0.1.0" dependencies = [ "common_utils", - "error-stack", + "error-stack 0.4.1", "fred", "futures 0.3.31", "serde", @@ -6686,7 +6735,7 @@ dependencies = [ "diesel", "diesel_models", "dyn-clone", - "error-stack", + "error-stack 0.4.1", "euclid", "events", "external_services", @@ -6723,6 +6772,7 @@ dependencies = [ "ring 0.17.14", "router_derive", "router_env", + "rust-grpc-client", "rust-i18n", "rust_decimal", "rustc-hash 1.1.0", @@ -6761,7 +6811,7 @@ version = "0.1.0" dependencies = [ "common_utils", "diesel", - "error-stack", + "error-stack 0.4.1", "indexmap 2.9.0", "proc-macro2", "quote", @@ -6779,7 +6829,7 @@ version = "0.1.0" dependencies = [ "cargo_metadata", "config", - "error-stack", + "error-stack 0.4.1", "gethostname", "opentelemetry", "opentelemetry-aws", @@ -6827,6 +6877,14 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust-grpc-client" +version = "0.1.0" +source = "git+https://github.com/juspay/connector-service?rev=afaef3427f815befe253da152e5f37d3858bbc9f#afaef3427f815befe253da152e5f37d3858bbc9f" +dependencies = [ + "grpc-api-types", +] + [[package]] name = "rust-i18n" version = "3.1.1" @@ -7196,7 +7254,7 @@ dependencies = [ "common_types", "common_utils", "diesel_models", - "error-stack", + "error-stack 0.4.1", "external_services", "futures 0.3.31", "hyperswitch_domain_models", @@ -8025,7 +8083,7 @@ dependencies = [ "diesel", "diesel_models", "dyn-clone", - "error-stack", + "error-stack 0.4.1", "futures 0.3.31", "hyperswitch_domain_models", "masking", diff --git a/config/config.example.toml b/config/config.example.toml index 888db62bbc..98ef7cf628 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1095,6 +1095,11 @@ billing_connectors_which_require_payment_sync = "stripebilling, recurly" # List enabled = true # Enable or disable Open Router url = "http://localhost:8080" # Open Router URL +[grpc_client.unified_connector_service] +host = "localhost" # Unified Connector Service Client Host +port = 8000 # Unified Connector Service Client Port +connection_timeout = 10 # Connection Timeout Duration in Seconds + [billing_connectors_invoice_sync] billing_connectors_which_requires_invoice_sync_call = "recurly" # List of billing connectors which has invoice sync api call diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 7e33ffcf40..cab8777aa9 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -376,3 +376,8 @@ base_url = "http://localhost:8080" # base url to call hyperswitch vault service [clone_connector_allowlist] merchant_ids = "merchant_ids" # Comma-separated list of allowed merchant IDs connector_names = "connector_names" # Comma-separated list of allowed connector names + +[grpc_client.unified_connector_service] +host = "localhost" # Unified Connector Service Client Host +port = 8000 # Unified Connector Service Client Port +connection_timeout = 10 # Connection Timeout Duration in Seconds \ No newline at end of file diff --git a/config/development.toml b/config/development.toml index c9d6e70dd7..db64e73099 100644 --- a/config/development.toml +++ b/config/development.toml @@ -1190,6 +1190,11 @@ click_to_pay = {connector_list = "adyen, cybersource"} enabled = false url = "http://localhost:8080" +[grpc_client.unified_connector_service] +host = "localhost" +port = 8000 +connection_timeout = 10 + [revenue_recovery] monitoring_threshold_in_seconds = 2592000 retry_algorithm_type = "cascading" diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index bdbaa1eae6..c8cf1e8c57 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -152,6 +152,9 @@ pub const APPLEPAY_VALIDATION_URL: &str = /// Request ID pub const X_REQUEST_ID: &str = "x-request-id"; +/// Merchant ID Header +pub const X_MERCHANT_ID: &str = "x-merchant-id"; + /// Default Tenant ID for the `Global` tenant pub const DEFAULT_GLOBAL_TENANT_ID: &str = "global"; diff --git a/crates/external_services/Cargo.toml b/crates/external_services/Cargo.toml index 4002e117c6..906f8e9b59 100644 --- a/crates/external_services/Cargo.toml +++ b/crates/external_services/Cargo.toml @@ -15,13 +15,9 @@ hashicorp-vault = ["dep:vaultrs"] v1 = ["hyperswitch_interfaces/v1", "common_utils/v1"] dynamic_routing = [ "dep:prost", - "dep:tonic", - "dep:tonic-reflection", - "dep:tonic-types", "dep:api_models", "tokio/macros", "tokio/rt-multi-thread", - "dep:tonic-build", "dep:router_env", "dep:hyper-util", "dep:http-body-util", @@ -48,15 +44,16 @@ thiserror = "1.0.69" vaultrs = { version = "0.7.4", optional = true } prost = { version = "0.13", optional = true } tokio = "1.45.1" -tonic = { version = "0.13.1", optional = true } -tonic-reflection = { version = "0.13.1", optional = true } -tonic-types = { version = "0.13.1", optional = true } +tonic = "0.13.1" +tonic-reflection = "0.13.1" +tonic-types = "0.13.1" hyper-util = { version = "0.1.12", optional = true } http-body-util = { version = "0.1.3", optional = true } reqwest = { version = "0.11.27", features = ["rustls-tls"] } http = "0.2.12" url = { version = "2.5.4", features = ["serde"] } quick-xml = { version = "0.31.0", features = ["serialize"] } +unified-connector-service-client = { git = "https://github.com/juspay/connector-service", rev = "afaef3427f815befe253da152e5f37d3858bbc9f", package = "rust-grpc-client" } # First party crates @@ -71,7 +68,7 @@ api_models = { version = "0.1.0", path = "../api_models", optional = true } [build-dependencies] -tonic-build = { version = "0.13.1", optional = true } +tonic-build = "0.13.1" router_env = { version = "0.1.0", path = "../router_env", default-features = false, optional = true } [lints] diff --git a/crates/external_services/src/grpc_client.rs b/crates/external_services/src/grpc_client.rs index 45b98c5115..5a9bd6143d 100644 --- a/crates/external_services/src/grpc_client.rs +++ b/crates/external_services/src/grpc_client.rs @@ -4,6 +4,8 @@ pub mod dynamic_routing; /// gRPC based Heath Check Client interface implementation #[cfg(feature = "dynamic_routing")] pub mod health_check_client; +/// gRPC based Unified Connector Service Client interface implementation +pub mod unified_connector_service; use std::{fmt::Debug, sync::Arc}; #[cfg(feature = "dynamic_routing")] @@ -20,6 +22,10 @@ use serde; #[cfg(feature = "dynamic_routing")] use tonic::body::Body; +use crate::grpc_client::unified_connector_service::{ + UnifiedConnectorServiceClient, UnifiedConnectorServiceClientConfig, +}; + #[cfg(feature = "dynamic_routing")] /// Hyper based Client type for maintaining connection pool for all gRPC services pub type Client = hyper_util::client::legacy::Client; @@ -33,6 +39,8 @@ pub struct GrpcClients { /// Health Check client for all gRPC services #[cfg(feature = "dynamic_routing")] pub health_client: HealthCheckClient, + /// Unified Connector Service client + pub unified_connector_service_client: Option, } /// Type that contains the configs required to construct a gRPC client with its respective services. #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, Default)] @@ -40,6 +48,8 @@ pub struct GrpcClientSettings { #[cfg(feature = "dynamic_routing")] /// Configs for Dynamic Routing Client pub dynamic_routing_client: DynamicRoutingClientConfig, + /// Configs for Unified Connector Service client + pub unified_connector_service: Option, } impl GrpcClientSettings { @@ -68,11 +78,15 @@ impl GrpcClientSettings { .await .expect("Failed to build gRPC connections"); + let unified_connector_service_client = + UnifiedConnectorServiceClient::build_connections(self).await; + Arc::new(GrpcClients { #[cfg(feature = "dynamic_routing")] dynamic_routing: dynamic_routing_connection, #[cfg(feature = "dynamic_routing")] health_client, + unified_connector_service_client, }) } } diff --git a/crates/external_services/src/grpc_client/unified_connector_service.rs b/crates/external_services/src/grpc_client/unified_connector_service.rs new file mode 100644 index 0000000000..0ad9602985 --- /dev/null +++ b/crates/external_services/src/grpc_client/unified_connector_service.rs @@ -0,0 +1,253 @@ +use common_utils::{consts as common_utils_consts, errors::CustomResult}; +use error_stack::ResultExt; +use masking::{PeekInterface, Secret}; +use router_env::logger; +use tokio::time::{timeout, Duration}; +use tonic::{ + metadata::{MetadataMap, MetadataValue}, + transport::Uri, +}; +use unified_connector_service_client::payments::{ + self as payments_grpc, payment_service_client::PaymentServiceClient, + PaymentServiceAuthorizeResponse, +}; + +use crate::{ + consts, + grpc_client::{GrpcClientSettings, GrpcHeaders}, +}; + +/// Unified Connector Service error variants +#[derive(Debug, Clone, thiserror::Error)] +pub enum UnifiedConnectorServiceError { + /// Error occurred while communicating with the gRPC server. + #[error("Error from gRPC Server : {0}")] + ConnectionError(String), + + /// Failed to encode the request to the unified connector service. + #[error("Failed to encode unified connector service request")] + RequestEncodingFailed, + + /// Request encoding failed due to a specific reason. + #[error("Request encoding failed : {0}")] + RequestEncodingFailedWithReason(String), + + /// Failed to deserialize the response from the connector. + #[error("Failed to deserialize connector response")] + ResponseDeserializationFailed, + + /// The connector name provided is invalid or unrecognized. + #[error("An invalid connector name was provided")] + InvalidConnectorName, + + /// Connector name is missing + #[error("Connector name is missing")] + MissingConnectorName, + + /// A required field was missing in the request. + #[error("Missing required field: {field_name}")] + MissingRequiredField { + /// Missing Field + field_name: &'static str, + }, + + /// Multiple required fields were missing in the request. + #[error("Missing required fields: {field_names:?}")] + MissingRequiredFields { + /// Missing Fields + field_names: Vec<&'static str>, + }, + + /// The requested step or feature is not yet implemented. + #[error("This step has not been implemented for: {0}")] + NotImplemented(String), + + /// Parsing of some value or input failed. + #[error("Parsing failed")] + ParsingFailed, + + /// Data format provided is invalid + #[error("Invalid Data format")] + InvalidDataFormat { + /// Field Name for which data is invalid + field_name: &'static str, + }, + + /// Failed to obtain authentication type + #[error("Failed to obtain authentication type")] + FailedToObtainAuthType, + + /// Failed to inject metadata into request headers + #[error("Failed to inject metadata into request headers: {0}")] + HeaderInjectionFailed(String), + + /// Failed to perform Payment Authorize from gRPC Server + #[error("Failed to perform Payment Authorize from gRPC Server")] + PaymentAuthorizeFailure, +} + +/// Result type for Dynamic Routing +pub type UnifiedConnectorServiceResult = CustomResult; +/// Contains the Unified Connector Service client +#[derive(Debug, Clone)] +pub struct UnifiedConnectorServiceClient { + /// The Unified Connector Service Client + pub client: PaymentServiceClient, +} + +/// Contains the Unified Connector Service Client config +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct UnifiedConnectorServiceClientConfig { + /// Host for the gRPC Client + pub host: String, + + /// Port of the gRPC Client + pub port: u16, + + /// Contains the connection timeout duration in seconds + pub connection_timeout: u64, +} + +/// Contains the Connector Auth Type and related authentication data. +#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)] +pub struct ConnectorAuthMetadata { + /// Name of the connector (e.g., "stripe", "paypal"). + pub connector_name: String, + + /// Type of authentication used (e.g., "HeaderKey", "BodyKey", "SignatureKey"). + pub auth_type: String, + + /// Optional API key used for authentication. + pub api_key: Option>, + + /// Optional additional key used by some authentication types. + pub key1: Option>, + + /// Optional API secret used for signature or secure authentication. + pub api_secret: Option>, + + /// Id of the merchant. + pub merchant_id: Secret, +} + +impl UnifiedConnectorServiceClient { + /// Builds the connection to the gRPC service + pub async fn build_connections(config: &GrpcClientSettings) -> Option { + match &config.unified_connector_service { + Some(unified_connector_service_client_config) => { + let uri_str = format!( + "https://{}:{}", + unified_connector_service_client_config.host, + unified_connector_service_client_config.port + ); + + let uri: Uri = match uri_str.parse() { + Ok(parsed_uri) => parsed_uri, + Err(err) => { + logger::error!(error = ?err, "Failed to parse URI for Unified Connector Service"); + return None; + } + }; + + let connect_result = timeout( + Duration::from_secs(unified_connector_service_client_config.connection_timeout), + PaymentServiceClient::connect(uri), + ) + .await; + + match connect_result { + Ok(Ok(client)) => Some(Self { client }), + Ok(Err(err)) => { + logger::error!(error = ?err, "Failed to connect to Unified Connector Service"); + None + } + Err(err) => { + logger::error!(error = ?err, "Connection to Unified Connector Service timed out"); + None + } + } + } + None => None, + } + } + + /// Performs Payment Authorize + pub async fn payment_authorize( + &self, + payment_authorize_request: payments_grpc::PaymentServiceAuthorizeRequest, + connector_auth_metadata: ConnectorAuthMetadata, + grpc_headers: GrpcHeaders, + ) -> UnifiedConnectorServiceResult> { + let mut request = tonic::Request::new(payment_authorize_request); + + let metadata = + build_unified_connector_service_grpc_headers(connector_auth_metadata, grpc_headers)?; + *request.metadata_mut() = metadata; + + self.client + .clone() + .authorize(request) + .await + .change_context(UnifiedConnectorServiceError::PaymentAuthorizeFailure) + .inspect_err(|error| logger::error!(?error)) + } +} + +/// Build the gRPC Headers for Unified Connector Service Request +pub fn build_unified_connector_service_grpc_headers( + meta: ConnectorAuthMetadata, + grpc_headers: GrpcHeaders, +) -> Result { + let mut metadata = MetadataMap::new(); + let parse = + |key: &str, value: &str| -> Result, UnifiedConnectorServiceError> { + value.parse::>().map_err(|error| { + logger::error!(?error); + UnifiedConnectorServiceError::HeaderInjectionFailed(key.to_string()) + }) + }; + + metadata.append( + consts::UCS_HEADER_CONNECTOR, + parse("connector", &meta.connector_name)?, + ); + metadata.append( + consts::UCS_HEADER_AUTH_TYPE, + parse("auth_type", &meta.auth_type)?, + ); + + if let Some(api_key) = meta.api_key { + metadata.append( + consts::UCS_HEADER_API_KEY, + parse("api_key", api_key.peek())?, + ); + } + if let Some(key1) = meta.key1 { + metadata.append(consts::UCS_HEADER_KEY1, parse("key1", key1.peek())?); + } + if let Some(api_secret) = meta.api_secret { + metadata.append( + consts::UCS_HEADER_API_SECRET, + parse("api_secret", api_secret.peek())?, + ); + } + + metadata.append( + common_utils_consts::X_MERCHANT_ID, + parse(common_utils_consts::X_MERCHANT_ID, meta.merchant_id.peek())?, + ); + + grpc_headers.tenant_id + .parse() + .map(|tenant_id| { + metadata.append( + common_utils_consts::TENANT_HEADER, + tenant_id) + }) + .inspect_err( + |err| logger::warn!(header_parse_error=?err,"invalid {} received",common_utils_consts::TENANT_HEADER), + ) + .ok(); + + Ok(metadata) +} diff --git a/crates/external_services/src/lib.rs b/crates/external_services/src/lib.rs index f3b1b330e1..3d9f1bcf93 100644 --- a/crates/external_services/src/lib.rs +++ b/crates/external_services/src/lib.rs @@ -29,11 +29,26 @@ pub mod managers; pub mod crm; /// Crate specific constants -#[cfg(feature = "aws_kms")] pub mod consts { /// General purpose base64 engine + #[cfg(feature = "aws_kms")] pub(crate) const BASE64_ENGINE: base64::engine::GeneralPurpose = base64::engine::general_purpose::STANDARD; + + /// Header key used to specify the connector name in UCS requests. + pub(crate) const UCS_HEADER_CONNECTOR: &str = "x-connector"; + + /// Header key used to indicate the authentication type being used. + pub(crate) const UCS_HEADER_AUTH_TYPE: &str = "x-auth"; + + /// Header key for sending the API key used for authentication. + pub(crate) const UCS_HEADER_API_KEY: &str = "x-api-key"; + + /// Header key for sending an additional secret key used in some auth types. + pub(crate) const UCS_HEADER_KEY1: &str = "x-key1"; + + /// Header key for sending the API secret in signature-based authentication. + pub(crate) const UCS_HEADER_API_SECRET: &str = "x-api-secret"; } /// Metrics for interactions with external systems. diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 95bd2566dd..585c49ffe1 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -88,6 +88,7 @@ reqwest = { version = "0.11.27", features = ["json", "rustls-tls", "gzip", "mult ring = "0.17.14" rust_decimal = { version = "1.37.1", features = ["serde-with-float", "serde-with-str"] } rust-i18n = { git = "https://github.com/kashif-m/rust-i18n", rev = "f2d8096aaaff7a87a847c35a5394c269f75e077a" } +unified-connector-service-client = { git = "https://github.com/juspay/connector-service", rev = "afaef3427f815befe253da152e5f37d3858bbc9f", package = "rust-grpc-client" } rustc-hash = "1.1.0" rustls = "0.22" rustls-pemfile = "2" diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index db50e81fb1..f9d6daa53e 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -307,3 +307,15 @@ pub const PSD2_COUNTRIES: [Country; 27] = [ Country::Spain, Country::Sweden, ]; + +// Rollout percentage config prefix +pub const UCS_ROLLOUT_PERCENT_CONFIG_PREFIX: &str = "UCS_ROLLOUT_CONFIG"; + +/// Header value indicating that signature-key-based authentication is used. +pub const UCS_AUTH_SIGNATURE_KEY: &str = "signature-key"; + +/// Header value indicating that body-key-based authentication is used. +pub const UCS_AUTH_BODY_KEY: &str = "body-key"; + +/// Header value indicating that header-key-based authentication is used. +pub const UCS_AUTH_HEADER_KEY: &str = "header-key"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 5fb11fd796..2334eddfd6 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -64,6 +64,7 @@ pub mod webhooks; pub mod profile_acquirer; pub mod unified_authentication_service; +pub mod unified_connector_service; #[cfg(feature = "v2")] pub mod proxy; diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 99debc44fd..95bf199400 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -12,10 +12,7 @@ use crate::{ }, errors, services, types::{ - api::{ - self, - fraud_check::{self as frm_api, FraudCheckConnectorData}, - }, + api::fraud_check::{self as frm_api, FraudCheckConnectorData}, domain, fraud_check::{FraudCheckCheckoutData, FraudCheckResponseData, FrmCheckoutRouterData}, storage::enums as storage_enums, @@ -167,16 +164,6 @@ impl ConstructFlowSpecificData( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs index bf24097add..7823e8db13 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -11,7 +11,7 @@ use crate::{ }, errors, services, types::{ - api::{self, RecordReturn}, + api::RecordReturn, domain, fraud_check::{ FraudCheckRecordReturnData, FraudCheckResponseData, FrmRecordReturnRouterData, @@ -135,16 +135,6 @@ impl ConstructFlowSpecificData( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/fraud_check/flows/sale_flow.rs b/crates/router/src/core/fraud_check/flows/sale_flow.rs index eef75ee2a4..a76afc386c 100644 --- a/crates/router/src/core/fraud_check/flows/sale_flow.rs +++ b/crates/router/src/core/fraud_check/flows/sale_flow.rs @@ -11,7 +11,7 @@ use crate::{ }, errors, services, types::{ - api::{self, fraud_check as frm_api}, + api::fraud_check as frm_api, domain, fraud_check::{FraudCheckResponseData, FraudCheckSaleData, FrmSaleRouterData}, storage::enums as storage_enums, @@ -143,16 +143,6 @@ impl ConstructFlowSpecificData( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/fraud_check/flows/transaction_flow.rs b/crates/router/src/core/fraud_check/flows/transaction_flow.rs index 0f3c3f0a8b..9504870a3b 100644 --- a/crates/router/src/core/fraud_check/flows/transaction_flow.rs +++ b/crates/router/src/core/fraud_check/flows/transaction_flow.rs @@ -10,7 +10,7 @@ use crate::{ }, errors, services, types::{ - api::{self, fraud_check as frm_api}, + api::fraud_check as frm_api, domain, fraud_check::{ FraudCheckResponseData, FraudCheckTransactionData, FrmTransactionRouterData, @@ -152,16 +152,6 @@ impl Ok(router_data) } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 1d5e2a7b4a..32b6aa1bfb 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -25,6 +25,8 @@ use std::{ #[cfg(feature = "v2")] pub mod payment_methods; +use std::future; + #[cfg(feature = "olap")] use api_models::admin::MerchantConnectorInfo; use api_models::{ @@ -91,6 +93,7 @@ use self::{ }; use super::{ errors::StorageErrorExt, payment_methods::surcharge_decision_configs, routing::TransactionData, + unified_connector_service::should_call_unified_connector_service, }; #[cfg(feature = "v1")] use crate::core::debit_routing; @@ -707,7 +710,22 @@ where None }; - let (router_data, mca) = call_connector_service( + let (merchant_connector_account, router_data, tokenization_action) = + call_connector_service_prerequisites( + state, + merchant_context, + connector.connector_data.clone(), + &operation, + &mut payment_data, + &customer, + &validate_result, + &business_profile, + false, + None, + ) + .await?; + + let (router_data, mca) = decide_unified_connector_service_call( state, req_state.clone(), merchant_context, @@ -725,9 +743,10 @@ where None, &business_profile, false, - false, - None, ::should_return_raw_response(&req), + merchant_connector_account, + router_data, + tokenization_action, ) .await?; @@ -831,7 +850,23 @@ where } else { None }; - let (router_data, mca) = call_connector_service( + + let (merchant_connector_account, router_data, tokenization_action) = + call_connector_service_prerequisites( + state, + merchant_context, + connector_data.clone(), + &operation, + &mut payment_data, + &customer, + &validate_result, + &business_profile, + false, + routing_decision, + ) + .await?; + + let (router_data, mca) = decide_unified_connector_service_call( state, req_state.clone(), merchant_context, @@ -849,9 +884,10 @@ where None, &business_profile, false, - false, - routing_decision, ::should_return_raw_response(&req), + merchant_connector_account, + router_data, + tokenization_action, ) .await?; @@ -3441,9 +3477,10 @@ pub async fn call_connector_service( frm_suggestion: Option, business_profile: &domain::Profile, is_retry_payment: bool, - should_retry_with_pan: bool, - routing_decision: Option, return_raw_connector_response: Option, + merchant_connector_account: helpers::MerchantConnectorAccountType, + mut router_data: RouterData, + tokenization_action: TokenizationAction, ) -> RouterResult<( RouterData, helpers::MerchantConnectorAccountType, @@ -3460,143 +3497,6 @@ where dyn api::Connector: services::api::ConnectorIntegration, { - let stime_connector = Instant::now(); - - let merchant_connector_account = construct_profile_id_and_get_mca( - state, - merchant_context, - payment_data, - &connector.connector_name.to_string(), - connector.merchant_connector_id.as_ref(), - false, - ) - .await?; - - let customer_acceptance = payment_data - .get_payment_attempt() - .customer_acceptance - .clone(); - - if is_pre_network_tokenization_enabled( - state, - business_profile, - customer_acceptance, - connector.connector_name, - ) { - let payment_method_data = payment_data.get_payment_method_data(); - let customer_id = payment_data.get_payment_intent().customer_id.clone(); - if let (Some(domain::PaymentMethodData::Card(card_data)), Some(customer_id)) = - (payment_method_data, customer_id) - { - let vault_operation = - get_vault_operation_for_pre_network_tokenization(state, customer_id, card_data) - .await; - match vault_operation { - payments::VaultOperation::SaveCardAndNetworkTokenData( - card_and_network_token_data, - ) => { - payment_data.set_vault_operation( - payments::VaultOperation::SaveCardAndNetworkTokenData(Box::new( - *card_and_network_token_data.clone(), - )), - ); - - payment_data.set_payment_method_data(Some( - domain::PaymentMethodData::NetworkToken( - card_and_network_token_data - .network_token - .network_token_data - .clone(), - ), - )); - } - payments::VaultOperation::SaveCardData(card_data_for_vault) => payment_data - .set_vault_operation(payments::VaultOperation::SaveCardData( - card_data_for_vault.clone(), - )), - payments::VaultOperation::ExistingVaultData(_) => (), - } - } - } - - #[cfg(feature = "v1")] - if payment_data - .get_payment_attempt() - .merchant_connector_id - .is_none() - { - payment_data.set_merchant_connector_id_in_attempt(merchant_connector_account.get_mca_id()); - } - - operation - .to_domain()? - .populate_payment_data( - state, - payment_data, - merchant_context, - business_profile, - &connector, - ) - .await?; - - let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( - state, - operation, - payment_data, - validate_result, - &merchant_connector_account, - merchant_context.get_merchant_key_store(), - customer, - business_profile, - should_retry_with_pan, - ) - .await?; - *payment_data = pd; - - // This is used to apply any kind of routing decision to the required data, - // before the call to `connector` is made. - routing_decision.map(|decision| decision.apply_routing_decision(payment_data)); - - // Validating the blocklist guard and generate the fingerprint - blocklist_guard(state, merchant_context, operation, payment_data).await?; - - #[cfg(feature = "v1")] - let merchant_recipient_data = if let Some(true) = payment_data - .get_payment_intent() - .is_payment_processor_token_flow - { - None - } else { - payment_data - .get_merchant_recipient_data( - state, - merchant_context, - &merchant_connector_account, - &connector, - ) - .await? - }; - - // TODO: handle how we read `is_processor_token_flow` in v2 and then call `get_merchant_recipient_data` - #[cfg(feature = "v2")] - let merchant_recipient_data = None; - - let mut router_data = payment_data - .construct_router_data( - state, - connector.connector.id(), - merchant_context, - customer, - &merchant_connector_account, - merchant_recipient_data, - None, - ) - .await?; - - let connector_request_reference_id = router_data.connector_request_reference_id.clone(); - payment_data - .set_connector_request_reference_id_in_payment_attempt(connector_request_reference_id); - let add_access_token_result = router_data .add_access_token( state, @@ -3752,13 +3652,283 @@ where Ok(router_data) }?; - let etime_connector = Instant::now(); - let duration_connector = etime_connector.saturating_duration_since(stime_connector); - tracing::info!(duration = format!("Duration taken: {}", duration_connector.as_millis())); - Ok((router_data, merchant_connector_account)) } +#[cfg(feature = "v1")] +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn call_connector_service_prerequisites( + state: &SessionState, + merchant_context: &domain::MerchantContext, + connector: api::ConnectorData, + operation: &BoxedOperation<'_, F, ApiRequest, D>, + payment_data: &mut D, + customer: &Option, + validate_result: &operations::ValidateResult, + business_profile: &domain::Profile, + should_retry_with_pan: bool, + routing_decision: Option, +) -> RouterResult<( + helpers::MerchantConnectorAccountType, + RouterData, + TokenizationAction, +)> +where + F: Send + Clone + Sync, + RouterDReq: Send + Sync, + + // To create connector flow specific interface data + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + D: ConstructFlowSpecificData, + RouterData: Feature + Send, + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, +{ + let merchant_connector_account = construct_profile_id_and_get_mca( + state, + merchant_context, + payment_data, + &connector.connector_name.to_string(), + connector.merchant_connector_id.as_ref(), + false, + ) + .await?; + + let customer_acceptance = payment_data + .get_payment_attempt() + .customer_acceptance + .clone(); + + if is_pre_network_tokenization_enabled( + state, + business_profile, + customer_acceptance, + connector.connector_name, + ) { + let payment_method_data = payment_data.get_payment_method_data(); + let customer_id = payment_data.get_payment_intent().customer_id.clone(); + if let (Some(domain::PaymentMethodData::Card(card_data)), Some(customer_id)) = + (payment_method_data, customer_id) + { + let vault_operation = + get_vault_operation_for_pre_network_tokenization(state, customer_id, card_data) + .await; + match vault_operation { + payments::VaultOperation::SaveCardAndNetworkTokenData( + card_and_network_token_data, + ) => { + payment_data.set_vault_operation( + payments::VaultOperation::SaveCardAndNetworkTokenData(Box::new( + *card_and_network_token_data.clone(), + )), + ); + + payment_data.set_payment_method_data(Some( + domain::PaymentMethodData::NetworkToken( + card_and_network_token_data + .network_token + .network_token_data + .clone(), + ), + )); + } + payments::VaultOperation::SaveCardData(card_data_for_vault) => payment_data + .set_vault_operation(payments::VaultOperation::SaveCardData( + card_data_for_vault.clone(), + )), + payments::VaultOperation::ExistingVaultData(_) => (), + } + } + } + + if payment_data + .get_payment_attempt() + .merchant_connector_id + .is_none() + { + payment_data.set_merchant_connector_id_in_attempt(merchant_connector_account.get_mca_id()); + } + + operation + .to_domain()? + .populate_payment_data( + state, + payment_data, + merchant_context, + business_profile, + &connector, + ) + .await?; + + let (pd, tokenization_action) = get_connector_tokenization_action_when_confirm_true( + state, + operation, + payment_data, + validate_result, + &merchant_connector_account, + merchant_context.get_merchant_key_store(), + customer, + business_profile, + should_retry_with_pan, + ) + .await?; + *payment_data = pd; + + // This is used to apply any kind of routing decision to the required data, + // before the call to `connector` is made. + routing_decision.map(|decision| decision.apply_routing_decision(payment_data)); + + // Validating the blocklist guard and generate the fingerprint + blocklist_guard(state, merchant_context, operation, payment_data).await?; + + let merchant_recipient_data = payment_data + .get_merchant_recipient_data( + state, + merchant_context, + &merchant_connector_account, + &connector, + ) + .await?; + + let router_data = payment_data + .construct_router_data( + state, + connector.connector.id(), + merchant_context, + customer, + &merchant_connector_account, + merchant_recipient_data, + None, + ) + .await?; + + let connector_request_reference_id = router_data.connector_request_reference_id.clone(); + payment_data + .set_connector_request_reference_id_in_payment_attempt(connector_request_reference_id); + + Ok((merchant_connector_account, router_data, tokenization_action)) +} + +#[cfg(feature = "v1")] +#[allow(clippy::too_many_arguments)] +#[instrument(skip_all)] +pub async fn decide_unified_connector_service_call( + state: &SessionState, + req_state: ReqState, + merchant_context: &domain::MerchantContext, + connector: api::ConnectorData, + operation: &BoxedOperation<'_, F, ApiRequest, D>, + payment_data: &mut D, + customer: &Option, + call_connector_action: CallConnectorAction, + validate_result: &operations::ValidateResult, + schedule_time: Option, + header_payload: HeaderPayload, + frm_suggestion: Option, + business_profile: &domain::Profile, + is_retry_payment: bool, + all_keys_required: Option, + merchant_connector_account: helpers::MerchantConnectorAccountType, + mut router_data: RouterData, + tokenization_action: TokenizationAction, +) -> RouterResult<( + RouterData, + helpers::MerchantConnectorAccountType, +)> +where + F: Send + Clone + Sync, + RouterDReq: Send + Sync, + + // To create connector flow specific interface data + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, + D: ConstructFlowSpecificData, + RouterData: Feature + Send, + // To construct connector flow specific api + dyn api::Connector: + services::api::ConnectorIntegration, +{ + record_time_taken_with(|| async { + if should_call_unified_connector_service(state, merchant_context, &router_data).await? { + if should_add_task_to_process_tracker(payment_data) { + operation + .to_domain()? + .add_task_to_process_tracker( + state, + payment_data.get_payment_attempt(), + validate_result.requeue, + schedule_time, + ) + .await + .map_err(|error| logger::error!(process_tracker_error=?error)) + .ok(); + } + + (_, *payment_data) = operation + .to_update_tracker()? + .update_trackers( + state, + req_state, + payment_data.clone(), + customer.clone(), + merchant_context.get_merchant_account().storage_scheme, + None, + merchant_context.get_merchant_key_store(), + frm_suggestion, + header_payload.clone(), + ) + .await?; + + router_data + .call_unified_connector_service( + state, + merchant_connector_account.clone(), + merchant_context, + ) + .await?; + + Ok((router_data, merchant_connector_account)) + } else { + call_connector_service( + state, + req_state, + merchant_context, + connector, + operation, + payment_data, + customer, + call_connector_action, + validate_result, + schedule_time, + header_payload, + frm_suggestion, + business_profile, + is_retry_payment, + all_keys_required, + merchant_connector_account, + router_data, + tokenization_action, + ) + .await + } + }) + .await +} + +async fn record_time_taken_with(f: F) -> RouterResult +where + F: FnOnce() -> Fut, + Fut: future::Future>, +{ + let stime = Instant::now(); + let result = f().await; + let etime = Instant::now(); + let duration = etime.saturating_duration_since(stime); + tracing::info!(duration = format!("Duration taken: {}", duration.as_millis())); + result +} + #[cfg(feature = "v2")] #[allow(clippy::too_many_arguments)] #[instrument(skip_all)] diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 5cc7cfa777..a93e2f391f 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -60,11 +60,13 @@ pub trait ConstructFlowSpecificData { async fn get_merchant_recipient_data<'a>( &self, - state: &SessionState, - merchant_context: &domain::MerchantContext, - merchant_connector_account: &helpers::MerchantConnectorAccountType, - connector: &api::ConnectorData, - ) -> RouterResult>; + _state: &SessionState, + _merchant_context: &domain::MerchantContext, + _merchant_connector_account: &helpers::MerchantConnectorAccountType, + _connector: &api::ConnectorData, + ) -> RouterResult> { + Ok(None) + } } #[allow(clippy::too_many_arguments)] @@ -207,6 +209,20 @@ pub trait Feature { { Ok(should_continue_payment) } + + async fn call_unified_connector_service<'a>( + &mut self, + _state: &SessionState, + _merchant_connector_account: helpers::MerchantConnectorAccountType, + _merchant_context: &domain::MerchantContext, + ) -> RouterResult<()> + where + F: Clone, + Self: Sized, + dyn api::Connector: services::ConnectorIntegration, + { + Ok(()) + } } /// Determines whether a capture API call should be made for a payment attempt diff --git a/crates/router/src/core/payments/flows/approve_flow.rs b/crates/router/src/core/payments/flows/approve_flow.rs index e2f671d914..296b081db6 100644 --- a/crates/router/src/core/payments/flows/approve_flow.rs +++ b/crates/router/src/core/payments/flows/approve_flow.rs @@ -56,16 +56,6 @@ impl )) .await } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs index 4df6ae9fe9..1532fffa3f 100644 --- a/crates/router/src/core/payments/flows/authorize_flow.rs +++ b/crates/router/src/core/payments/flows/authorize_flow.rs @@ -1,10 +1,12 @@ use async_trait::async_trait; use common_enums as enums; use common_types::payments as common_payments_types; +use error_stack::ResultExt; use hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse; #[cfg(feature = "v2")] use hyperswitch_domain_models::payments::PaymentConfirmData; use masking::ExposeInterface; +use unified_connector_service_client::payments as payments_grpc; // use router_env::tracing::Instrument; use super::{ConstructFlowSpecificData, Feature}; @@ -15,6 +17,10 @@ use crate::{ payments::{ self, access_token, customers, helpers, tokenization, transformers, PaymentData, }, + unified_connector_service::{ + build_unified_connector_service_auth_metadata, + handle_unified_connector_service_response_for_payment_authorize, + }, }, logger, routes::{metrics, SessionState}, @@ -71,24 +77,23 @@ impl merchant_connector_account: &helpers::MerchantConnectorAccountType, connector: &api::ConnectorData, ) -> RouterResult> { - let payment_method = &self + let is_open_banking = &self .payment_attempt .get_payment_method() - .get_required_value("PaymentMethod")?; + .get_required_value("PaymentMethod")? + .eq(&enums::PaymentMethod::OpenBanking); - let data = if *payment_method == enums::PaymentMethod::OpenBanking { + if *is_open_banking { payments::get_merchant_bank_data_for_open_banking_connectors( merchant_connector_account, merchant_context, connector, state, ) - .await? + .await } else { - None - }; - - Ok(data) + Ok(None) + } } } @@ -140,24 +145,28 @@ impl merchant_connector_account: &helpers::MerchantConnectorAccountType, connector: &api::ConnectorData, ) -> RouterResult> { - let payment_method = &self - .payment_attempt - .get_payment_method() - .get_required_value("PaymentMethod")?; + match &self.payment_intent.is_payment_processor_token_flow { + Some(true) => Ok(None), + Some(false) | None => { + let is_open_banking = &self + .payment_attempt + .get_payment_method() + .get_required_value("PaymentMethod")? + .eq(&enums::PaymentMethod::OpenBanking); - let data = if *payment_method == enums::PaymentMethod::OpenBanking { - payments::get_merchant_bank_data_for_open_banking_connectors( - merchant_connector_account, - merchant_context, - connector, - state, - ) - .await? - } else { - None - }; - - Ok(data) + Ok(if *is_open_banking { + payments::get_merchant_bank_data_for_open_banking_connectors( + merchant_connector_account, + merchant_context, + connector, + state, + ) + .await? + } else { + None + }) + } + } } } @@ -509,6 +518,56 @@ impl Feature for types::PaymentsAu Ok(should_continue_further) } } + + async fn call_unified_connector_service<'a>( + &mut self, + state: &SessionState, + merchant_connector_account: helpers::MerchantConnectorAccountType, + merchant_context: &domain::MerchantContext, + ) -> RouterResult<()> { + let client = state + .grpc_client + .unified_connector_service_client + .clone() + .ok_or(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to fetch Unified Connector Service client")?; + + let payment_authorize_request = + payments_grpc::PaymentServiceAuthorizeRequest::foreign_try_from(self) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to construct Payment Authorize Request")?; + + let connector_auth_metadata = build_unified_connector_service_auth_metadata( + merchant_connector_account, + merchant_context, + ) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to construct request metadata")?; + + let response = client + .payment_authorize( + payment_authorize_request, + connector_auth_metadata, + state.get_grpc_headers(), + ) + .await + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to authorize payment")?; + + let payment_authorize_response = response.into_inner(); + + let (status, router_data_response) = + handle_unified_connector_service_response_for_payment_authorize( + payment_authorize_response, + ) + .change_context(ApiErrorResponse::InternalServerError) + .attach_printable("Failed to deserialize UCS response")?; + + self.status = status; + self.response = router_data_response; + + Ok(()) + } } pub trait RouterDataAuthorize { diff --git a/crates/router/src/core/payments/flows/cancel_flow.rs b/crates/router/src/core/payments/flows/cancel_flow.rs index 046213dbe5..7f025f81c8 100644 --- a/crates/router/src/core/payments/flows/cancel_flow.rs +++ b/crates/router/src/core/payments/flows/cancel_flow.rs @@ -55,16 +55,6 @@ impl ConstructFlowSpecificData( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/capture_flow.rs b/crates/router/src/core/payments/flows/capture_flow.rs index 09d60db9a4..3f2dafc0cb 100644 --- a/crates/router/src/core/payments/flows/capture_flow.rs +++ b/crates/router/src/core/payments/flows/capture_flow.rs @@ -42,16 +42,6 @@ impl )) .await } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[cfg(feature = "v2")] @@ -84,16 +74,6 @@ impl )) .await } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs index d734f195ce..1d7a7772fb 100644 --- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs +++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs @@ -72,16 +72,6 @@ impl > { todo!() } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/incremental_authorization_flow.rs b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs index c7dee2ad9e..ef04b0354d 100644 --- a/crates/router/src/core/payments/flows/incremental_authorization_flow.rs +++ b/crates/router/src/core/payments/flows/incremental_authorization_flow.rs @@ -59,16 +59,6 @@ impl ) -> RouterResult { todo!() } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/post_session_tokens_flow.rs b/crates/router/src/core/payments/flows/post_session_tokens_flow.rs index 118dd17a10..b356b69513 100644 --- a/crates/router/src/core/payments/flows/post_session_tokens_flow.rs +++ b/crates/router/src/core/payments/flows/post_session_tokens_flow.rs @@ -59,16 +59,6 @@ impl ) -> RouterResult { todo!() } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/psync_flow.rs b/crates/router/src/core/payments/flows/psync_flow.rs index 87536525be..f2ffbe24d1 100644 --- a/crates/router/src/core/payments/flows/psync_flow.rs +++ b/crates/router/src/core/payments/flows/psync_flow.rs @@ -46,16 +46,6 @@ impl ConstructFlowSpecificData( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[cfg(feature = "v2")] @@ -87,16 +77,6 @@ impl ConstructFlowSpecificData( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/reject_flow.rs b/crates/router/src/core/payments/flows/reject_flow.rs index efb34be54b..2c7a9878ed 100644 --- a/crates/router/src/core/payments/flows/reject_flow.rs +++ b/crates/router/src/core/payments/flows/reject_flow.rs @@ -55,16 +55,6 @@ impl ConstructFlowSpecificData RouterResult { todo!() } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/session_flow.rs b/crates/router/src/core/payments/flows/session_flow.rs index ae0450f28f..06441f2466 100644 --- a/crates/router/src/core/payments/flows/session_flow.rs +++ b/crates/router/src/core/payments/flows/session_flow.rs @@ -56,16 +56,6 @@ impl )) .await } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &routes::SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[cfg(feature = "v1")] @@ -99,16 +89,6 @@ impl )) .await } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &routes::SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/session_update_flow.rs b/crates/router/src/core/payments/flows/session_update_flow.rs index cd42f855c1..24b51ce1ca 100644 --- a/crates/router/src/core/payments/flows/session_update_flow.rs +++ b/crates/router/src/core/payments/flows/session_update_flow.rs @@ -59,16 +59,6 @@ impl ) -> RouterResult { todo!() } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs index f99d13bd31..eb43bff64f 100644 --- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs +++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs @@ -50,16 +50,6 @@ impl )) .await } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[cfg(feature = "v2")] @@ -95,16 +85,6 @@ impl ) .await } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/flows/update_metadata_flow.rs b/crates/router/src/core/payments/flows/update_metadata_flow.rs index f654b7ead1..a57b6acb75 100644 --- a/crates/router/src/core/payments/flows/update_metadata_flow.rs +++ b/crates/router/src/core/payments/flows/update_metadata_flow.rs @@ -58,16 +58,6 @@ impl ) -> RouterResult { todo!() } - - async fn get_merchant_recipient_data<'a>( - &self, - _state: &SessionState, - _merchant_context: &domain::MerchantContext, - _merchant_connector_account: &helpers::MerchantConnectorAccountType, - _connector: &api::ConnectorData, - ) -> RouterResult> { - Ok(None) - } } #[async_trait] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 9065dcd2ce..d6ce585829 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -50,6 +50,7 @@ use openssl::{ pkey::PKey, symm::{decrypt_aead, Cipher}, }; +use rand::Rng; #[cfg(feature = "v2")] use redis_interface::errors::RedisError; use router_env::{instrument, logger, tracing}; @@ -2036,6 +2037,38 @@ pub fn decide_payment_method_retrieval_action( } } +pub async fn should_execute_based_on_rollout( + state: &SessionState, + config_key: &str, +) -> RouterResult { + let db = state.store.as_ref(); + + match db.find_config_by_key(config_key).await { + Ok(rollout_config) => match rollout_config.config.parse::() { + Ok(rollout_percent) => { + if !(0.0..=1.0).contains(&rollout_percent) { + logger::warn!( + rollout_percent, + "Rollout percent out of bounds. Must be between 0.0 and 1.0" + ); + return Ok(false); + } + + let sampled_value: f64 = rand::thread_rng().gen_range(0.0..1.0); + Ok(sampled_value < rollout_percent) + } + Err(err) => { + logger::error!(error = ?err, "Failed to parse rollout percent"); + Ok(false) + } + }, + Err(err) => { + logger::error!(error = ?err, "Failed to fetch rollout config from DB"); + Ok(false) + } + } +} + pub fn determine_standard_vault_action( is_network_tokenization_enabled: bool, mandate_id: Option, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index f7d8b3e888..8fe9c565e0 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -369,7 +369,22 @@ where ) .await?; - let (router_data, _mca) = payments::call_connector_service( + let (merchant_connector_account, router_data, tokenization_action) = + payments::call_connector_service_prerequisites( + state, + merchant_context, + connector.clone(), + operation, + payment_data, + customer, + validate_result, + business_profile, + should_retry_with_pan, + routing_decision, + ) + .await?; + + let (router_data, _mca) = payments::decide_unified_connector_service_call( state, req_state, merchant_context, @@ -384,9 +399,10 @@ where frm_suggestion, business_profile, true, - should_retry_with_pan, - routing_decision, None, + merchant_connector_account, + router_data, + tokenization_action, ) .await?; diff --git a/crates/router/src/core/unified_connector_service.rs b/crates/router/src/core/unified_connector_service.rs new file mode 100644 index 0000000000..d9c00fbadb --- /dev/null +++ b/crates/router/src/core/unified_connector_service.rs @@ -0,0 +1,252 @@ +use api_models::admin::ConnectorAuthType; +use common_enums::{AttemptStatus, PaymentMethodType}; +use common_utils::{errors::CustomResult, ext_traits::ValueExt}; +use error_stack::ResultExt; +use external_services::grpc_client::unified_connector_service::{ + ConnectorAuthMetadata, UnifiedConnectorServiceError, +}; +use hyperswitch_connectors::utils::CardData; +use hyperswitch_domain_models::{ + merchant_context::MerchantContext, + router_data::{ErrorResponse, RouterData}, + router_response_types::{PaymentsResponseData, RedirectForm}, +}; +use masking::{ExposeInterface, PeekInterface, Secret}; +use unified_connector_service_client::payments::{ + self as payments_grpc, payment_method::PaymentMethod, CardDetails, CardPaymentMethodType, + PaymentServiceAuthorizeResponse, +}; + +use crate::{ + consts, + core::{ + errors::RouterResult, + payments::helpers::{should_execute_based_on_rollout, MerchantConnectorAccountType}, + utils::get_flow_name, + }, + routes::SessionState, + types::transformers::ForeignTryFrom, +}; + +mod transformers; + +pub async fn should_call_unified_connector_service( + state: &SessionState, + merchant_context: &MerchantContext, + router_data: &RouterData, +) -> RouterResult { + let merchant_id = merchant_context + .get_merchant_account() + .get_id() + .get_string_repr(); + + let connector_name = router_data.connector.clone(); + let payment_method = router_data.payment_method.to_string(); + let flow_name = get_flow_name::()?; + + let config_key = format!( + "{}_{}_{}_{}_{}", + consts::UCS_ROLLOUT_PERCENT_CONFIG_PREFIX, + merchant_id, + connector_name, + payment_method, + flow_name + ); + + let should_execute = should_execute_based_on_rollout(state, &config_key).await?; + Ok(should_execute && state.grpc_client.unified_connector_service_client.is_some()) +} + +pub fn build_unified_connector_service_payment_method( + payment_method_data: hyperswitch_domain_models::payment_method_data::PaymentMethodData, + payment_method_type: PaymentMethodType, +) -> CustomResult { + match payment_method_data { + hyperswitch_domain_models::payment_method_data::PaymentMethodData::Card(card) => { + let card_exp_month = card + .get_card_expiry_month_2_digit() + .attach_printable("Failed to extract 2-digit expiry month from card") + .change_context(UnifiedConnectorServiceError::InvalidDataFormat { + field_name: "card_exp_month", + })? + .peek() + .to_string(); + + let card_network = card + .card_network + .clone() + .map(payments_grpc::CardNetwork::foreign_try_from) + .transpose()?; + + let card_details = CardDetails { + card_number: card.card_number.get_card_no(), + card_exp_month, + card_exp_year: card.get_expiry_year_4_digit().peek().to_string(), + card_cvc: card.card_cvc.peek().to_string(), + card_holder_name: card.card_holder_name.map(|name| name.expose()), + card_issuer: card.card_issuer.clone(), + card_network: card_network.map(|card_network| card_network.into()), + card_type: card.card_type.clone(), + bank_code: card.bank_code.clone(), + nick_name: card.nick_name.map(|n| n.expose()), + card_issuing_country_alpha2: card.card_issuing_country.clone(), + }; + + let grpc_card_type = match payment_method_type { + PaymentMethodType::Credit => { + payments_grpc::card_payment_method_type::CardType::Credit(card_details) + } + PaymentMethodType::Debit => { + payments_grpc::card_payment_method_type::CardType::Debit(card_details) + } + _ => { + return Err(UnifiedConnectorServiceError::NotImplemented(format!( + "Unimplemented card payment method type: {:?}", + payment_method_type + )) + .into()); + } + }; + + Ok(payments_grpc::PaymentMethod { + payment_method: Some(PaymentMethod::Card(CardPaymentMethodType { + card_type: Some(grpc_card_type), + })), + }) + } + + _ => Err(UnifiedConnectorServiceError::NotImplemented( + "Unimplemented Payment Method".to_string(), + ) + .into()), + } +} + +pub fn build_unified_connector_service_auth_metadata( + merchant_connector_account: MerchantConnectorAccountType, + merchant_context: &MerchantContext, +) -> CustomResult { + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(UnifiedConnectorServiceError::FailedToObtainAuthType) + .attach_printable("Failed while parsing value for ConnectorAuthType")?; + + let connector_name = { + #[cfg(feature = "v1")] + { + merchant_connector_account + .get_connector_name() + .ok_or(UnifiedConnectorServiceError::MissingConnectorName) + .attach_printable("Missing connector name")? + } + + #[cfg(feature = "v2")] + { + merchant_connector_account + .get_connector_name() + .map(|connector| connector.to_string()) + .ok_or(UnifiedConnectorServiceError::MissingConnectorName) + .attach_printable("Missing connector name")? + } + }; + + let merchant_id = merchant_context + .get_merchant_account() + .get_id() + .get_string_repr(); + + match &auth_type { + ConnectorAuthType::SignatureKey { + api_key, + key1, + api_secret, + } => Ok(ConnectorAuthMetadata { + connector_name, + auth_type: consts::UCS_AUTH_SIGNATURE_KEY.to_string(), + api_key: Some(api_key.clone()), + key1: Some(key1.clone()), + api_secret: Some(api_secret.clone()), + merchant_id: Secret::new(merchant_id.to_string()), + }), + ConnectorAuthType::BodyKey { api_key, key1 } => Ok(ConnectorAuthMetadata { + connector_name, + auth_type: consts::UCS_AUTH_BODY_KEY.to_string(), + api_key: Some(api_key.clone()), + key1: Some(key1.clone()), + api_secret: None, + merchant_id: Secret::new(merchant_id.to_string()), + }), + ConnectorAuthType::HeaderKey { api_key } => Ok(ConnectorAuthMetadata { + connector_name, + auth_type: consts::UCS_AUTH_HEADER_KEY.to_string(), + api_key: Some(api_key.clone()), + key1: None, + api_secret: None, + merchant_id: Secret::new(merchant_id.to_string()), + }), + _ => Err(UnifiedConnectorServiceError::FailedToObtainAuthType) + .attach_printable("Unsupported ConnectorAuthType for header injection"), + } +} + +pub fn handle_unified_connector_service_response_for_payment_authorize( + response: PaymentServiceAuthorizeResponse, +) -> CustomResult< + (AttemptStatus, Result), + UnifiedConnectorServiceError, +> { + let status = AttemptStatus::foreign_try_from(response.status())?; + + let connector_response_reference_id = + response.response_ref_id.as_ref().and_then(|identifier| { + identifier + .id_type + .clone() + .and_then(|id_type| match id_type { + payments_grpc::identifier::IdType::Id(id) => Some(id), + payments_grpc::identifier::IdType::EncodedData(encoded_data) => { + Some(encoded_data) + } + payments_grpc::identifier::IdType::NoResponseIdMarker(_) => None, + }) + }); + + let router_data_response = match status { + AttemptStatus::Charged | + AttemptStatus::Authorized | + AttemptStatus::AuthenticationPending | + AttemptStatus::DeviceDataCollectionPending => Ok(PaymentsResponseData::TransactionResponse { + resource_id: match connector_response_reference_id.as_ref() { + Some(connector_response_reference_id) => hyperswitch_domain_models::router_request_types::ResponseId::ConnectorTransactionId(connector_response_reference_id.clone()), + None => hyperswitch_domain_models::router_request_types::ResponseId::NoResponseId, + }, + redirection_data: Box::new( + response + .redirection_data + .clone() + .map(RedirectForm::foreign_try_from) + .transpose()? + ), + mandate_reference: Box::new(None), + connector_metadata: None, + network_txn_id: response.network_txn_id.clone(), + connector_response_reference_id, + incremental_authorization_allowed: response.incremental_authorization_allowed, + charges: None, + }), + _ => Err(ErrorResponse { + code: response.error_code().to_owned(), + message: response.error_message().to_owned(), + reason: Some(response.error_message().to_owned()), + status_code: 500, + attempt_status: Some(status), + connector_transaction_id: connector_response_reference_id, + network_decline_code: None, + network_advice_code: None, + network_error_message: None, + }) + }; + + Ok((status, router_data_response)) +} diff --git a/crates/router/src/core/unified_connector_service/transformers.rs b/crates/router/src/core/unified_connector_service/transformers.rs new file mode 100644 index 0000000000..0e2ae6fd34 --- /dev/null +++ b/crates/router/src/core/unified_connector_service/transformers.rs @@ -0,0 +1,419 @@ +use std::collections::HashMap; + +use common_enums::{AttemptStatus, AuthenticationType}; +use common_utils::request::Method; +use diesel_models::enums as storage_enums; +use error_stack::ResultExt; +use external_services::grpc_client::unified_connector_service::UnifiedConnectorServiceError; +use hyperswitch_domain_models::{ + router_data::RouterData, + router_flow_types::payments::Authorize, + router_request_types::{AuthenticationData, PaymentsAuthorizeData}, + router_response_types::{PaymentsResponseData, RedirectForm}, +}; +use masking::{ExposeInterface, PeekInterface}; +use unified_connector_service_client::payments::{self as payments_grpc, Identifier}; + +use crate::{ + core::unified_connector_service::build_unified_connector_service_payment_method, + types::transformers::ForeignTryFrom, +}; + +impl ForeignTryFrom<&RouterData> + for payments_grpc::PaymentServiceAuthorizeRequest +{ + type Error = error_stack::Report; + + fn foreign_try_from( + router_data: &RouterData, + ) -> Result { + let currency = payments_grpc::Currency::foreign_try_from(router_data.request.currency)?; + + let payment_method = router_data + .request + .payment_method_type + .map(|payment_method_type| { + build_unified_connector_service_payment_method( + router_data.request.payment_method_data.clone(), + payment_method_type, + ) + }) + .transpose()?; + + let address = payments_grpc::PaymentAddress::foreign_try_from(router_data.address.clone())?; + + let auth_type = payments_grpc::AuthenticationType::foreign_try_from(router_data.auth_type)?; + + let browser_info = router_data + .request + .browser_info + .clone() + .map(payments_grpc::BrowserInformation::foreign_try_from) + .transpose()?; + + let capture_method = router_data + .request + .capture_method + .map(payments_grpc::CaptureMethod::foreign_try_from) + .transpose()?; + + let authentication_data = router_data + .request + .authentication_data + .clone() + .map(payments_grpc::AuthenticationData::foreign_try_from) + .transpose()?; + + Ok(Self { + amount: router_data.request.amount, + currency: currency.into(), + payment_method, + return_url: router_data.request.router_return_url.clone(), + address: Some(address), + auth_type: auth_type.into(), + enrolled_for_3ds: router_data.request.enrolled_for_3ds, + request_incremental_authorization: router_data + .request + .request_incremental_authorization, + minor_amount: router_data.request.amount, + email: router_data + .request + .email + .clone() + .map(|e| e.expose().expose()), + browser_info, + access_token: None, + session_token: None, + order_tax_amount: router_data + .request + .order_tax_amount + .map(|order_tax_amount| order_tax_amount.get_amount_as_i64()), + customer_name: router_data + .request + .customer_name + .clone() + .map(|customer_name| customer_name.peek().to_owned()), + capture_method: capture_method.map(|capture_method| capture_method.into()), + webhook_url: router_data.request.webhook_url.clone(), + complete_authorize_url: router_data.request.complete_authorize_url.clone(), + setup_future_usage: None, + off_session: None, + customer_acceptance: None, + order_category: router_data.request.order_category.clone(), + payment_experience: None, + authentication_data, + request_extended_authorization: router_data + .request + .request_extended_authorization + .map(|request_extended_authorization| request_extended_authorization.is_true()), + merchant_order_reference_id: router_data.request.merchant_order_reference_id.clone(), + shipping_cost: router_data + .request + .shipping_cost + .map(|shipping_cost| shipping_cost.get_amount_as_i64()), + request_ref_id: Some(Identifier { + id_type: Some(payments_grpc::identifier::IdType::Id( + router_data.payment_id.clone(), + )), + }), + connector_customer_id: router_data + .request + .customer_id + .as_ref() + .map(|id| id.get_string_repr().to_string()), + metadata: router_data + .request + .metadata + .as_ref() + .and_then(|val| val.as_object()) + .map(|map| { + map.iter() + .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string()))) + .collect::>() + }) + .unwrap_or_default(), + }) + } +} + +impl ForeignTryFrom for payments_grpc::Currency { + type Error = error_stack::Report; + + fn foreign_try_from(currency: common_enums::Currency) -> Result { + Self::from_str_name(¤cy.to_string()).ok_or_else(|| { + UnifiedConnectorServiceError::RequestEncodingFailedWithReason( + "Failed to parse currency".to_string(), + ) + .into() + }) + } +} + +impl ForeignTryFrom for payments_grpc::CardNetwork { + type Error = error_stack::Report; + + fn foreign_try_from(card_network: common_enums::CardNetwork) -> Result { + match card_network { + common_enums::CardNetwork::Visa => Ok(Self::Visa), + common_enums::CardNetwork::Mastercard => Ok(Self::Mastercard), + common_enums::CardNetwork::JCB => Ok(Self::Jcb), + common_enums::CardNetwork::DinersClub => Ok(Self::Diners), + common_enums::CardNetwork::Discover => Ok(Self::Discover), + common_enums::CardNetwork::CartesBancaires => Ok(Self::CartesBancaires), + common_enums::CardNetwork::UnionPay => Ok(Self::Unionpay), + common_enums::CardNetwork::RuPay => Ok(Self::Rupay), + common_enums::CardNetwork::Maestro => Ok(Self::Maestro), + _ => Err( + UnifiedConnectorServiceError::RequestEncodingFailedWithReason( + "Card Network not supported".to_string(), + ) + .into(), + ), + } + } +} + +impl ForeignTryFrom + for payments_grpc::PaymentAddress +{ + type Error = error_stack::Report; + + fn foreign_try_from( + payment_address: hyperswitch_domain_models::payment_address::PaymentAddress, + ) -> Result { + let shipping = payment_address.get_shipping().and_then(|address| { + let details = address.address.as_ref()?; + + let get_str = + |opt: &Option>| opt.as_ref().map(|s| s.peek().to_owned()); + + let get_plain = |opt: &Option| opt.clone(); + + let country = details + .country + .as_ref() + .and_then(|c| payments_grpc::CountryAlpha2::from_str_name(&c.to_string())) + .ok_or_else(|| { + UnifiedConnectorServiceError::RequestEncodingFailedWithReason( + "Invalid country code".to_string(), + ) + }) + .attach_printable("Invalid country code") + .ok()? // Return None if invalid + .into(); + + Some(payments_grpc::Address { + first_name: get_str(&details.first_name), + last_name: get_str(&details.last_name), + line1: get_str(&details.line1), + line2: get_str(&details.line2), + line3: get_str(&details.line3), + city: get_plain(&details.city), + state: get_str(&details.state), + zip_code: get_str(&details.zip), + country_alpha2_code: Some(country), + email: address.email.as_ref().map(|e| e.peek().to_string()), + phone_number: address + .phone + .as_ref() + .and_then(|phone| phone.number.as_ref().map(|n| n.peek().to_string())), + phone_country_code: address.phone.as_ref().and_then(|p| p.country_code.clone()), + }) + }); + + let billing = payment_address.get_payment_billing().and_then(|address| { + let details = address.address.as_ref()?; + + let get_str = + |opt: &Option>| opt.as_ref().map(|s| s.peek().to_owned()); + + let get_plain = |opt: &Option| opt.clone(); + + let country = details + .country + .as_ref() + .and_then(|c| payments_grpc::CountryAlpha2::from_str_name(&c.to_string())) + .ok_or_else(|| { + UnifiedConnectorServiceError::RequestEncodingFailedWithReason( + "Invalid country code".to_string(), + ) + }) + .attach_printable("Invalid country code") + .ok()? // Return None if invalid + .into(); + + Some(payments_grpc::Address { + first_name: get_str(&details.first_name), + last_name: get_str(&details.last_name), + line1: get_str(&details.line1), + line2: get_str(&details.line2), + line3: get_str(&details.line3), + city: get_plain(&details.city), + state: get_str(&details.state), + zip_code: get_str(&details.zip), + country_alpha2_code: Some(country), + email: address.email.as_ref().map(|e| e.peek().to_string()), + phone_number: address + .phone + .as_ref() + .and_then(|phone| phone.number.as_ref().map(|n| n.peek().to_string())), + phone_country_code: address.phone.as_ref().and_then(|p| p.country_code.clone()), + }) + }); + + Ok(Self { + shipping_address: shipping, + billing_address: billing, + }) + } +} + +impl ForeignTryFrom for payments_grpc::AuthenticationType { + type Error = error_stack::Report; + + fn foreign_try_from(auth_type: AuthenticationType) -> Result { + match auth_type { + AuthenticationType::ThreeDs => Ok(Self::ThreeDs), + AuthenticationType::NoThreeDs => Ok(Self::NoThreeDs), + } + } +} + +impl ForeignTryFrom + for payments_grpc::BrowserInformation +{ + type Error = error_stack::Report; + + fn foreign_try_from( + browser_info: hyperswitch_domain_models::router_request_types::BrowserInformation, + ) -> Result { + Ok(Self { + color_depth: browser_info.color_depth.map(|v| v.into()), + java_enabled: browser_info.java_enabled, + java_script_enabled: browser_info.java_script_enabled, + language: browser_info.language, + screen_height: browser_info.screen_height, + screen_width: browser_info.screen_width, + ip_address: browser_info.ip_address.map(|ip| ip.to_string()), + accept_header: browser_info.accept_header, + user_agent: browser_info.user_agent, + os_type: browser_info.os_type, + os_version: browser_info.os_version, + device_model: browser_info.device_model, + accept_language: browser_info.accept_language, + time_zone_offset_minutes: browser_info.time_zone, + }) + } +} + +impl ForeignTryFrom for payments_grpc::CaptureMethod { + type Error = error_stack::Report; + + fn foreign_try_from(capture_method: storage_enums::CaptureMethod) -> Result { + match capture_method { + common_enums::CaptureMethod::Automatic => Ok(Self::Automatic), + common_enums::CaptureMethod::Manual => Ok(Self::Manual), + common_enums::CaptureMethod::ManualMultiple => Ok(Self::ManualMultiple), + common_enums::CaptureMethod::Scheduled => Ok(Self::Scheduled), + common_enums::CaptureMethod::SequentialAutomatic => Ok(Self::SequentialAutomatic), + } + } +} + +impl ForeignTryFrom for payments_grpc::AuthenticationData { + type Error = error_stack::Report; + + fn foreign_try_from(authentication_data: AuthenticationData) -> Result { + Ok(Self { + eci: authentication_data.eci, + cavv: authentication_data.cavv.peek().to_string(), + threeds_server_transaction_id: authentication_data.threeds_server_transaction_id.map( + |id| Identifier { + id_type: Some(payments_grpc::identifier::IdType::Id(id)), + }, + ), + message_version: None, + ds_transaction_id: authentication_data.ds_trans_id, + }) + } +} + +impl ForeignTryFrom for AttemptStatus { + type Error = error_stack::Report; + + fn foreign_try_from(grpc_status: payments_grpc::PaymentStatus) -> Result { + match grpc_status { + payments_grpc::PaymentStatus::Started => Ok(Self::Started), + payments_grpc::PaymentStatus::AuthenticationFailed => Ok(Self::AuthenticationFailed), + payments_grpc::PaymentStatus::RouterDeclined => Ok(Self::RouterDeclined), + payments_grpc::PaymentStatus::AuthenticationPending => Ok(Self::AuthenticationPending), + payments_grpc::PaymentStatus::AuthenticationSuccessful => { + Ok(Self::AuthenticationSuccessful) + } + payments_grpc::PaymentStatus::Authorized => Ok(Self::Authorized), + payments_grpc::PaymentStatus::AuthorizationFailed => Ok(Self::AuthorizationFailed), + payments_grpc::PaymentStatus::Charged => Ok(Self::Charged), + payments_grpc::PaymentStatus::Authorizing => Ok(Self::Authorizing), + payments_grpc::PaymentStatus::CodInitiated => Ok(Self::CodInitiated), + payments_grpc::PaymentStatus::Voided => Ok(Self::Voided), + payments_grpc::PaymentStatus::VoidInitiated => Ok(Self::VoidInitiated), + payments_grpc::PaymentStatus::CaptureInitiated => Ok(Self::CaptureInitiated), + payments_grpc::PaymentStatus::CaptureFailed => Ok(Self::CaptureFailed), + payments_grpc::PaymentStatus::VoidFailed => Ok(Self::VoidFailed), + payments_grpc::PaymentStatus::AutoRefunded => Ok(Self::AutoRefunded), + payments_grpc::PaymentStatus::PartialCharged => Ok(Self::PartialCharged), + payments_grpc::PaymentStatus::PartialChargedAndChargeable => { + Ok(Self::PartialChargedAndChargeable) + } + payments_grpc::PaymentStatus::Unresolved => Ok(Self::Unresolved), + payments_grpc::PaymentStatus::Pending => Ok(Self::Pending), + payments_grpc::PaymentStatus::Failure => Ok(Self::Failure), + payments_grpc::PaymentStatus::PaymentMethodAwaited => Ok(Self::PaymentMethodAwaited), + payments_grpc::PaymentStatus::ConfirmationAwaited => Ok(Self::ConfirmationAwaited), + payments_grpc::PaymentStatus::DeviceDataCollectionPending => { + Ok(Self::DeviceDataCollectionPending) + } + payments_grpc::PaymentStatus::AttemptStatusUnspecified => Ok(Self::Unresolved), + } + } +} + +impl ForeignTryFrom for RedirectForm { + type Error = error_stack::Report; + + fn foreign_try_from(value: payments_grpc::RedirectForm) -> Result { + match value.form_type { + Some(payments_grpc::redirect_form::FormType::Form(form)) => Ok(Self::Form { + endpoint: form.clone().endpoint, + method: Method::foreign_try_from(form.clone().method())?, + form_fields: form.clone().form_fields, + }), + Some(payments_grpc::redirect_form::FormType::Html(html)) => Ok(Self::Html { + html_data: html.html_data, + }), + None => Err( + UnifiedConnectorServiceError::RequestEncodingFailedWithReason( + "Missing form type".to_string(), + ) + .into(), + ), + } + } +} + +impl ForeignTryFrom for Method { + type Error = error_stack::Report; + + fn foreign_try_from(value: payments_grpc::HttpMethod) -> Result { + match value { + payments_grpc::HttpMethod::Get => Ok(Self::Get), + payments_grpc::HttpMethod::Post => Ok(Self::Post), + payments_grpc::HttpMethod::Put => Ok(Self::Put), + payments_grpc::HttpMethod::Delete => Ok(Self::Delete), + payments_grpc::HttpMethod::Unspecified => { + Err(UnifiedConnectorServiceError::ResponseDeserializationFailed) + .attach_printable("Invalid Http Method") + } + } + } +}