diff --git a/Cargo.lock b/Cargo.lock index 77f9b33ddc4..6ea7937eca6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3695,11 +3695,22 @@ name = "hyperswitch_interfaces" version = "0.1.0" dependencies = [ "async-trait", + "bytes 1.6.0", "common_utils", "dyn-clone", + "http 0.2.12", + "hyperswitch_domain_models", "masking", + "mime", + "once_cell", + "reqwest", + "router_derive", + "router_env", "serde", + "serde_json", + "storage_impl", "thiserror", + "time", ] [[package]] diff --git a/crates/hyperswitch_interfaces/Cargo.toml b/crates/hyperswitch_interfaces/Cargo.toml index 034e0a67d92..95b3b98af05 100644 --- a/crates/hyperswitch_interfaces/Cargo.toml +++ b/crates/hyperswitch_interfaces/Cargo.toml @@ -6,12 +6,28 @@ rust-version.workspace = true readme = "README.md" license.workspace = true +[features] +default = ["dummy_connector", "payouts"] +dummy_connector = [] +payouts = [] + [dependencies] async-trait = "0.1.79" +bytes = "1.6.0" dyn-clone = "1.0.17" +http = "0.2.12" +mime = "0.3.17" +once_cell = "1.19.0" +reqwest = "0.11.27" serde = { version = "1.0.197", features = ["derive"] } +serde_json = "1.0.115" thiserror = "1.0.58" +time = "0.3.35" # First party crates common_utils = { version = "0.1.0", path = "../common_utils" } +hyperswitch_domain_models = { version = "0.1.0", path = "../hyperswitch_domain_models", default-features = false } masking = { version = "0.1.0", path = "../masking" } +router_derive = { version = "0.1.0", path = "../router_derive" } +router_env = { version = "0.1.0", path = "../router_env" } +storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false } diff --git a/crates/hyperswitch_interfaces/src/api.rs b/crates/hyperswitch_interfaces/src/api.rs new file mode 100644 index 00000000000..63cbc6dabf8 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/api.rs @@ -0,0 +1,183 @@ +//! API interface + +use common_utils::{ + errors::CustomResult, + request::{Method, Request, RequestContent}, +}; +use hyperswitch_domain_models::router_data::{ErrorResponse, RouterData}; +use masking::Maskable; +use serde_json::json; + +use crate::{ + configs::Connectors, errors, events::connector_api_logs::ConnectorEvent, metrics, types, +}; + +/// type BoxedConnectorIntegration +pub type BoxedConnectorIntegration<'a, T, Req, Resp> = + Box<&'a (dyn ConnectorIntegration + Send + Sync)>; + +/// trait ConnectorIntegrationAny +pub trait ConnectorIntegrationAny: Send + Sync + 'static { + /// fn get_connector_integration + fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>; +} + +impl ConnectorIntegrationAny for S +where + S: ConnectorIntegration + Send + Sync, +{ + fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> { + Box::new(self) + } +} + +/// trait ConnectorIntegration +pub trait ConnectorIntegration: ConnectorIntegrationAny + Sync { + /// fn get_headers + fn get_headers( + &self, + _req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + Ok(vec![]) + } + + /// fn get_content_type + fn get_content_type(&self) -> &'static str { + mime::APPLICATION_JSON.essence_str() + } + + /// primarily used when creating signature based on request method of payment flow + fn get_http_method(&self) -> Method { + Method::Post + } + + /// fn get_url + fn get_url( + &self, + _req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult { + Ok(String::new()) + } + + /// fn get_request_body + fn get_request_body( + &self, + _req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult { + Ok(RequestContent::Json(Box::new(json!(r#"{}"#)))) + } + + /// fn get_request_form_data + fn get_request_form_data( + &self, + _req: &RouterData, + ) -> CustomResult, errors::ConnectorError> { + Ok(None) + } + + /// fn build_request + fn build_request( + &self, + req: &RouterData, + _connectors: &Connectors, + ) -> CustomResult, errors::ConnectorError> { + metrics::UNIMPLEMENTED_FLOW.add( + &metrics::CONTEXT, + 1, + &[metrics::add_attributes("connector", req.connector.clone())], + ); + Ok(None) + } + + /// fn handle_response + fn handle_response( + &self, + data: &RouterData, + event_builder: Option<&mut ConnectorEvent>, + _res: types::Response, + ) -> CustomResult, errors::ConnectorError> + where + T: Clone, + Req: Clone, + Resp: Clone, + { + event_builder.map(|e| e.set_error(json!({"error": "Not Implemented"}))); + Ok(data.clone()) + } + + /// fn get_error_response + fn get_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + event_builder.map(|event| event.set_error(json!({"error": res.response.escape_ascii().to_string(), "status_code": res.status_code}))); + Ok(ErrorResponse::get_not_implemented()) + } + + /// fn get_5xx_error_response + fn get_5xx_error_response( + &self, + res: types::Response, + event_builder: Option<&mut ConnectorEvent>, + ) -> CustomResult { + event_builder.map(|event| event.set_error(json!({"error": res.response.escape_ascii().to_string(), "status_code": res.status_code}))); + let error_message = match res.status_code { + 500 => "internal_server_error", + 501 => "not_implemented", + 502 => "bad_gateway", + 503 => "service_unavailable", + 504 => "gateway_timeout", + 505 => "http_version_not_supported", + 506 => "variant_also_negotiates", + 507 => "insufficient_storage", + 508 => "loop_detected", + 510 => "not_extended", + 511 => "network_authentication_required", + _ => "unknown_error", + }; + Ok(ErrorResponse { + code: res.status_code.to_string(), + message: error_message.to_string(), + reason: String::from_utf8(res.response.to_vec()).ok(), + status_code: res.status_code, + attempt_status: None, + connector_transaction_id: None, + }) + } + + /// whenever capture sync is implemented at the connector side, this method should be overridden + fn get_multiple_capture_sync_method( + &self, + ) -> CustomResult { + Err(errors::ConnectorError::NotImplemented("multiple capture sync".into()).into()) + } + + /// fn get_certificate + fn get_certificate( + &self, + _req: &RouterData, + ) -> CustomResult, errors::ConnectorError> { + Ok(None) + } + + /// fn get_certificate_key + fn get_certificate_key( + &self, + _req: &RouterData, + ) -> CustomResult, errors::ConnectorError> { + Ok(None) + } +} + +/// Sync Methods for multiple captures +#[derive(Debug)] +pub enum CaptureSyncMethod { + /// For syncing multiple captures individually + Individual, + /// For syncing multiple captures together + Bulk, +} diff --git a/crates/hyperswitch_interfaces/src/configs.rs b/crates/hyperswitch_interfaces/src/configs.rs new file mode 100644 index 00000000000..8f3b5600f08 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/configs.rs @@ -0,0 +1,132 @@ +//! Configs interface +use router_derive; +use serde::Deserialize; +use storage_impl::errors::ApplicationError; + +// struct Connectors +#[allow(missing_docs, missing_debug_implementations)] +#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] +#[serde(default)] +pub struct Connectors { + pub aci: ConnectorParams, + #[cfg(feature = "payouts")] + pub adyen: ConnectorParamsWithSecondaryBaseUrl, + pub adyenplatform: ConnectorParams, + #[cfg(not(feature = "payouts"))] + pub adyen: ConnectorParams, + pub airwallex: ConnectorParams, + pub applepay: ConnectorParams, + pub authorizedotnet: ConnectorParams, + pub bambora: ConnectorParams, + pub bankofamerica: ConnectorParams, + pub billwerk: ConnectorParams, + pub bitpay: ConnectorParams, + pub bluesnap: ConnectorParamsWithSecondaryBaseUrl, + pub boku: ConnectorParams, + pub braintree: ConnectorParams, + pub cashtocode: ConnectorParams, + pub checkout: ConnectorParams, + pub coinbase: ConnectorParams, + pub cryptopay: ConnectorParams, + pub cybersource: ConnectorParams, + pub datatrans: ConnectorParams, + pub dlocal: ConnectorParams, + #[cfg(feature = "dummy_connector")] + pub dummyconnector: ConnectorParams, + pub ebanx: ConnectorParams, + pub fiserv: ConnectorParams, + pub forte: ConnectorParams, + pub globalpay: ConnectorParams, + pub globepay: ConnectorParams, + pub gocardless: ConnectorParams, + pub gpayments: ConnectorParams, + pub helcim: ConnectorParams, + pub iatapay: ConnectorParams, + pub klarna: ConnectorParams, + pub mifinity: ConnectorParams, + pub mollie: ConnectorParams, + pub multisafepay: ConnectorParams, + pub netcetera: ConnectorParams, + pub nexinets: ConnectorParams, + pub nmi: ConnectorParams, + pub noon: ConnectorParamsWithModeType, + pub nuvei: ConnectorParams, + pub opayo: ConnectorParams, + pub opennode: ConnectorParams, + pub payeezy: ConnectorParams, + pub payme: ConnectorParams, + pub payone: ConnectorParams, + pub paypal: ConnectorParams, + pub payu: ConnectorParams, + pub placetopay: ConnectorParams, + pub powertranz: ConnectorParams, + pub prophetpay: ConnectorParams, + pub rapyd: ConnectorParams, + pub riskified: ConnectorParams, + pub shift4: ConnectorParams, + pub signifyd: ConnectorParams, + pub square: ConnectorParams, + pub stax: ConnectorParams, + pub stripe: ConnectorParamsWithFileUploadUrl, + pub threedsecureio: ConnectorParams, + pub trustpay: ConnectorParamsWithMoreUrls, + pub tsys: ConnectorParams, + pub volt: ConnectorParams, + pub wise: ConnectorParams, + pub worldline: ConnectorParams, + pub worldpay: ConnectorParams, + pub zen: ConnectorParams, + pub zsl: ConnectorParams, +} + +/// struct ConnectorParams +#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] +#[serde(default)] +pub struct ConnectorParams { + /// base url + pub base_url: String, + /// secondary base url + pub secondary_base_url: Option, +} + +/// struct ConnectorParamsWithModeType +#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] +#[serde(default)] +pub struct ConnectorParamsWithModeType { + /// base url + pub base_url: String, + /// secondary base url + pub secondary_base_url: Option, + /// Can take values like Test or Live for Noon + pub key_mode: String, +} + +/// struct ConnectorParamsWithMoreUrls +#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] +#[serde(default)] +pub struct ConnectorParamsWithMoreUrls { + /// base url + pub base_url: String, + /// base url for bank redirects + pub base_url_bank_redirects: String, +} + +/// struct ConnectorParamsWithFileUploadUrl +#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] +#[serde(default)] +pub struct ConnectorParamsWithFileUploadUrl { + /// base url + pub base_url: String, + /// base url for file upload + pub base_url_file_upload: String, +} + +/// struct ConnectorParamsWithSecondaryBaseUrl +#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] +#[serde(default)] +pub struct ConnectorParamsWithSecondaryBaseUrl { + /// base url + pub base_url: String, + /// secondary base url + pub secondary_base_url: String, +} diff --git a/crates/hyperswitch_interfaces/src/errors.rs b/crates/hyperswitch_interfaces/src/errors.rs new file mode 100644 index 00000000000..e36707af6b0 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/errors.rs @@ -0,0 +1,149 @@ +//! Errors interface + +use common_utils::errors::ErrorSwitch; +use hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse; + +/// Connector Errors +#[allow(missing_docs, missing_debug_implementations)] +#[derive(Debug, thiserror::Error, PartialEq)] +pub enum ConnectorError { + #[error("Error while obtaining URL for the integration")] + FailedToObtainIntegrationUrl, + #[error("Failed to encode connector request")] + RequestEncodingFailed, + #[error("Request encoding failed : {0}")] + RequestEncodingFailedWithReason(String), + #[error("Parsing failed")] + ParsingFailed, + #[error("Failed to deserialize connector response")] + ResponseDeserializationFailed, + #[error("Failed to execute a processing step: {0:?}")] + ProcessingStepFailed(Option), + #[error("The connector returned an unexpected response: {0:?}")] + UnexpectedResponseError(bytes::Bytes), + #[error("Failed to parse custom routing rules from merchant account")] + RoutingRulesParsingError, + #[error("Failed to obtain preferred connector from merchant account")] + FailedToObtainPreferredConnector, + #[error("An invalid connector name was provided")] + InvalidConnectorName, + #[error("An invalid Wallet was used")] + InvalidWallet, + #[error("Failed to handle connector response")] + ResponseHandlingFailed, + #[error("Missing required field: {field_name}")] + MissingRequiredField { field_name: &'static str }, + #[error("Missing required fields: {field_names:?}")] + MissingRequiredFields { field_names: Vec<&'static str> }, + #[error("Failed to obtain authentication type")] + FailedToObtainAuthType, + #[error("Failed to obtain certificate")] + FailedToObtainCertificate, + #[error("Connector meta data not found")] + NoConnectorMetaData, + #[error("Failed to obtain certificate key")] + FailedToObtainCertificateKey, + #[error("This step has not been implemented for: {0}")] + NotImplemented(String), + #[error("{message} is not supported by {connector}")] + NotSupported { + message: String, + connector: &'static str, + }, + #[error("{flow} flow not supported by {connector} connector")] + FlowNotSupported { flow: String, connector: String }, + #[error("Capture method not supported")] + CaptureMethodNotSupported, + #[error("Missing connector mandate ID")] + MissingConnectorMandateID, + #[error("Missing connector transaction ID")] + MissingConnectorTransactionID, + #[error("Missing connector refund ID")] + MissingConnectorRefundID, + #[error("Missing apple pay tokenization data")] + MissingApplePayTokenData, + #[error("Webhooks not implemented for this connector")] + WebhooksNotImplemented, + #[error("Failed to decode webhook event body")] + WebhookBodyDecodingFailed, + #[error("Signature not found for incoming webhook")] + WebhookSignatureNotFound, + #[error("Failed to verify webhook source")] + WebhookSourceVerificationFailed, + #[error("Could not find merchant secret in DB for incoming webhook source verification")] + WebhookVerificationSecretNotFound, + #[error("Merchant secret found for incoming webhook source verification is invalid")] + WebhookVerificationSecretInvalid, + #[error("Incoming webhook object reference ID not found")] + WebhookReferenceIdNotFound, + #[error("Incoming webhook event type not found")] + WebhookEventTypeNotFound, + #[error("Incoming webhook event resource object not found")] + WebhookResourceObjectNotFound, + #[error("Could not respond to the incoming webhook event")] + WebhookResponseEncodingFailed, + #[error("Invalid Date/time format")] + InvalidDateFormat, + #[error("Date Formatting Failed")] + DateFormattingFailed, + #[error("Invalid Data format")] + InvalidDataFormat { field_name: &'static str }, + #[error("Payment Method data / Payment Method Type / Payment Experience Mismatch ")] + MismatchedPaymentData, + #[error("Failed to parse {wallet_name} wallet token")] + InvalidWalletToken { wallet_name: String }, + #[error("Missing Connector Related Transaction ID")] + MissingConnectorRelatedTransactionID { id: String }, + #[error("File Validation failed")] + FileValidationFailed { reason: String }, + #[error("Missing 3DS redirection payload: {field_name}")] + MissingConnectorRedirectionPayload { field_name: &'static str }, + #[error("Failed at connector's end with code '{code}'")] + FailedAtConnector { message: String, code: String }, + #[error("Payment Method Type not found")] + MissingPaymentMethodType, + #[error("Balance in the payment method is low")] + InSufficientBalanceInPaymentMethod, + #[error("Server responded with Request Timeout")] + RequestTimeoutReceived, + #[error("The given currency method is not configured with the given connector")] + CurrencyNotSupported { + message: String, + connector: &'static str, + }, + #[error("Invalid Configuration")] + InvalidConnectorConfig { config: &'static str }, + #[error("Failed to convert amount to required type")] + AmountConversionFailed, +} + +impl ConnectorError { + /// fn is_connector_timeout + pub fn is_connector_timeout(&self) -> bool { + self == &Self::RequestTimeoutReceived + } +} + +impl ErrorSwitch for common_utils::errors::ParsingError { + fn switch(&self) -> ConnectorError { + ConnectorError::ParsingFailed + } +} + +impl ErrorSwitch for ConnectorError { + fn switch(&self) -> ApiErrorResponse { + match self { + Self::WebhookSourceVerificationFailed => ApiErrorResponse::WebhookAuthenticationFailed, + Self::WebhookSignatureNotFound + | Self::WebhookReferenceIdNotFound + | Self::WebhookResourceObjectNotFound + | Self::WebhookBodyDecodingFailed + | Self::WebhooksNotImplemented => ApiErrorResponse::WebhookBadRequest, + Self::WebhookEventTypeNotFound => ApiErrorResponse::WebhookUnprocessableEntity, + Self::WebhookVerificationSecretInvalid => { + ApiErrorResponse::WebhookInvalidMerchantSecret + } + _ => ApiErrorResponse::InternalServerError, + } + } +} diff --git a/crates/hyperswitch_interfaces/src/events.rs b/crates/hyperswitch_interfaces/src/events.rs new file mode 100644 index 00000000000..3dcb7519545 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/events.rs @@ -0,0 +1,3 @@ +//! Events interface + +pub mod connector_api_logs; diff --git a/crates/hyperswitch_interfaces/src/events/connector_api_logs.rs b/crates/hyperswitch_interfaces/src/events/connector_api_logs.rs new file mode 100644 index 00000000000..79b172e9ea7 --- /dev/null +++ b/crates/hyperswitch_interfaces/src/events/connector_api_logs.rs @@ -0,0 +1,96 @@ +//! Connector API logs interface + +use common_utils::request::Method; +use router_env::tracing_actix_web::RequestId; +use serde::Serialize; +use serde_json::json; +use time::OffsetDateTime; + +/// struct ConnectorEvent +#[derive(Debug, Serialize)] +pub struct ConnectorEvent { + connector_name: String, + flow: String, + request: String, + masked_response: Option, + error: Option, + url: String, + method: String, + payment_id: String, + merchant_id: String, + created_at: i128, + /// Connector Event Request ID + pub request_id: String, + latency: u128, + refund_id: Option, + dispute_id: Option, + status_code: u16, +} + +impl ConnectorEvent { + /// fn new ConnectorEvent + #[allow(clippy::too_many_arguments)] + pub fn new( + connector_name: String, + flow: &str, + request: serde_json::Value, + url: String, + method: Method, + payment_id: String, + merchant_id: String, + request_id: Option<&RequestId>, + latency: u128, + refund_id: Option, + dispute_id: Option, + status_code: u16, + ) -> Self { + Self { + connector_name, + flow: flow + .rsplit_once("::") + .map(|(_, s)| s) + .unwrap_or(flow) + .to_string(), + request: request.to_string(), + masked_response: None, + error: None, + url, + method: method.to_string(), + payment_id, + merchant_id, + created_at: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, + request_id: request_id + .map(|i| i.as_hyphenated().to_string()) + .unwrap_or("NO_REQUEST_ID".to_string()), + latency, + refund_id, + dispute_id, + status_code, + } + } + + /// fn set_response_body + pub fn set_response_body(&mut self, response: &T) { + match masking::masked_serialize(response) { + Ok(masked) => { + self.masked_response = Some(masked.to_string()); + } + Err(er) => self.set_error(json!({"error": er.to_string()})), + } + } + + /// fn set_error_response_body + pub fn set_error_response_body(&mut self, response: &T) { + match masking::masked_serialize(response) { + Ok(masked) => { + self.error = Some(masked.to_string()); + } + Err(er) => self.set_error(json!({"error": er.to_string()})), + } + } + + /// fn set_error + pub fn set_error(&mut self, error: serde_json::Value) { + self.error = Some(error.to_string()); + } +} diff --git a/crates/hyperswitch_interfaces/src/lib.rs b/crates/hyperswitch_interfaces/src/lib.rs index 3f7b8d41c3e..7d3b319df83 100644 --- a/crates/hyperswitch_interfaces/src/lib.rs +++ b/crates/hyperswitch_interfaces/src/lib.rs @@ -1,7 +1,11 @@ //! Hyperswitch interface - #![warn(missing_docs, missing_debug_implementations)] -pub mod secrets_interface; - +pub mod api; +pub mod configs; pub mod encryption_interface; +pub mod errors; +pub mod events; +pub mod metrics; +pub mod secrets_interface; +pub mod types; diff --git a/crates/hyperswitch_interfaces/src/metrics.rs b/crates/hyperswitch_interfaces/src/metrics.rs new file mode 100644 index 00000000000..a03215a175d --- /dev/null +++ b/crates/hyperswitch_interfaces/src/metrics.rs @@ -0,0 +1,16 @@ +//! Metrics interface + +use router_env::{counter_metric, global_meter, metrics_context, opentelemetry}; + +metrics_context!(CONTEXT); +global_meter!(GLOBAL_METER, "ROUTER_API"); + +counter_metric!(UNIMPLEMENTED_FLOW, GLOBAL_METER); + +/// fn add attributes +pub fn add_attributes>( + key: &'static str, + value: T, +) -> opentelemetry::KeyValue { + opentelemetry::KeyValue::new(key, value) +} diff --git a/crates/hyperswitch_interfaces/src/types.rs b/crates/hyperswitch_interfaces/src/types.rs new file mode 100644 index 00000000000..2f0c5c2fbda --- /dev/null +++ b/crates/hyperswitch_interfaces/src/types.rs @@ -0,0 +1,12 @@ +//! Types interface + +/// struct Response +#[derive(Clone, Debug)] +pub struct Response { + /// headers + pub headers: Option, + /// response + pub response: bytes::Bytes, + /// status code + pub status_code: u16, +} diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index ac8689d21b4..06cff596312 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -17,6 +17,7 @@ use external_services::{ secrets_management::SecretsManagementConfig, }, }; +pub use hyperswitch_interfaces::configs::Connectors; use hyperswitch_interfaces::secrets_interface::secret_state::{ RawSecret, SecretState, SecretStateContainer, SecuredSecret, }; @@ -544,117 +545,6 @@ pub struct SupportedConnectors { pub wallets: Vec, } -#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] -#[serde(default)] -pub struct Connectors { - pub aci: ConnectorParams, - #[cfg(feature = "payouts")] - pub adyen: ConnectorParamsWithSecondaryBaseUrl, - pub adyenplatform: ConnectorParams, - #[cfg(not(feature = "payouts"))] - pub adyen: ConnectorParams, - pub airwallex: ConnectorParams, - pub applepay: ConnectorParams, - pub authorizedotnet: ConnectorParams, - pub bambora: ConnectorParams, - pub bankofamerica: ConnectorParams, - pub billwerk: ConnectorParams, - pub bitpay: ConnectorParams, - pub bluesnap: ConnectorParamsWithSecondaryBaseUrl, - pub boku: ConnectorParams, - pub braintree: ConnectorParams, - pub cashtocode: ConnectorParams, - pub checkout: ConnectorParams, - pub coinbase: ConnectorParams, - pub cryptopay: ConnectorParams, - pub cybersource: ConnectorParams, - pub datatrans: ConnectorParams, - pub dlocal: ConnectorParams, - #[cfg(feature = "dummy_connector")] - pub dummyconnector: ConnectorParams, - pub ebanx: ConnectorParams, - pub fiserv: ConnectorParams, - pub forte: ConnectorParams, - pub globalpay: ConnectorParams, - pub globepay: ConnectorParams, - pub gocardless: ConnectorParams, - pub gpayments: ConnectorParams, - pub helcim: ConnectorParams, - pub iatapay: ConnectorParams, - pub klarna: ConnectorParams, - pub mifinity: ConnectorParams, - pub mollie: ConnectorParams, - pub multisafepay: ConnectorParams, - pub netcetera: ConnectorParams, - pub nexinets: ConnectorParams, - pub nmi: ConnectorParams, - pub noon: ConnectorParamsWithModeType, - pub nuvei: ConnectorParams, - pub opayo: ConnectorParams, - pub opennode: ConnectorParams, - pub payeezy: ConnectorParams, - pub payme: ConnectorParams, - pub payone: ConnectorParams, - pub paypal: ConnectorParams, - pub payu: ConnectorParams, - pub placetopay: ConnectorParams, - pub powertranz: ConnectorParams, - pub prophetpay: ConnectorParams, - pub rapyd: ConnectorParams, - pub riskified: ConnectorParams, - pub shift4: ConnectorParams, - pub signifyd: ConnectorParams, - pub square: ConnectorParams, - pub stax: ConnectorParams, - pub stripe: ConnectorParamsWithFileUploadUrl, - pub threedsecureio: ConnectorParams, - pub trustpay: ConnectorParamsWithMoreUrls, - pub tsys: ConnectorParams, - pub volt: ConnectorParams, - pub wise: ConnectorParams, - pub worldline: ConnectorParams, - pub worldpay: ConnectorParams, - pub zen: ConnectorParams, - pub zsl: ConnectorParams, -} - -#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] -#[serde(default)] -pub struct ConnectorParams { - pub base_url: String, - pub secondary_base_url: Option, -} - -#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] -#[serde(default)] -pub struct ConnectorParamsWithModeType { - pub base_url: String, - pub secondary_base_url: Option, - /// Can take values like Test or Live for Noon - pub key_mode: String, -} - -#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] -#[serde(default)] -pub struct ConnectorParamsWithMoreUrls { - pub base_url: String, - pub base_url_bank_redirects: String, -} - -#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] -#[serde(default)] -pub struct ConnectorParamsWithFileUploadUrl { - pub base_url: String, - pub base_url_file_upload: String, -} - -#[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] -#[serde(default)] -pub struct ConnectorParamsWithSecondaryBaseUrl { - pub base_url: String, - pub secondary_base_url: String, -} - #[cfg(feature = "kv_store")] #[derive(Debug, Clone, Deserialize)] #[serde(default)] diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index e1ef11265c0..d6d376fa0ef 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1837,12 +1837,6 @@ where json.parse_value(std::any::type_name::()).switch() } -impl common_utils::errors::ErrorSwitch for errors::ParsingError { - fn switch(&self) -> errors::ConnectorError { - errors::ConnectorError::ParsingFailed - } -} - pub fn base64_decode(data: String) -> Result, Error> { consts::BASE64_ENGINE .decode(data) diff --git a/crates/router/src/core/errors.rs b/crates/router/src/core/errors.rs index 0e9e4022798..83fa2143648 100644 --- a/crates/router/src/core/errors.rs +++ b/crates/router/src/core/errors.rs @@ -14,6 +14,7 @@ pub use hyperswitch_domain_models::errors::{ api_error_response::{ApiErrorResponse, ErrorType, NotImplementedMessage}, StorageError as DataStorageError, }; +pub use hyperswitch_interfaces::errors::ConnectorError; pub use redis_interface::errors::RedisError; use scheduler::errors as sch_errors; use storage_impl::errors as storage_impl_errors; @@ -111,118 +112,6 @@ pub fn http_not_implemented() -> actix_web::HttpResponse { .error_response() } -#[derive(Debug, thiserror::Error, PartialEq)] -pub enum ConnectorError { - #[error("Error while obtaining URL for the integration")] - FailedToObtainIntegrationUrl, - #[error("Failed to encode connector request")] - RequestEncodingFailed, - #[error("Request encoding failed : {0}")] - RequestEncodingFailedWithReason(String), - #[error("Parsing failed")] - ParsingFailed, - #[error("Failed to deserialize connector response")] - ResponseDeserializationFailed, - #[error("Failed to execute a processing step: {0:?}")] - ProcessingStepFailed(Option), - #[error("The connector returned an unexpected response: {0:?}")] - UnexpectedResponseError(bytes::Bytes), - #[error("Failed to parse custom routing rules from merchant account")] - RoutingRulesParsingError, - #[error("Failed to obtain preferred connector from merchant account")] - FailedToObtainPreferredConnector, - #[error("An invalid connector name was provided")] - InvalidConnectorName, - #[error("An invalid Wallet was used")] - InvalidWallet, - #[error("Failed to handle connector response")] - ResponseHandlingFailed, - #[error("Missing required field: {field_name}")] - MissingRequiredField { field_name: &'static str }, - #[error("Missing required fields: {field_names:?}")] - MissingRequiredFields { field_names: Vec<&'static str> }, - #[error("Failed to obtain authentication type")] - FailedToObtainAuthType, - #[error("Failed to obtain certificate")] - FailedToObtainCertificate, - #[error("Connector meta data not found")] - NoConnectorMetaData, - #[error("Failed to obtain certificate key")] - FailedToObtainCertificateKey, - #[error("This step has not been implemented for: {0}")] - NotImplemented(String), - #[error("{message} is not supported by {connector}")] - NotSupported { - message: String, - connector: &'static str, - }, - #[error("{flow} flow not supported by {connector} connector")] - FlowNotSupported { flow: String, connector: String }, - #[error("Capture method not supported")] - CaptureMethodNotSupported, - #[error("Missing connector mandate ID")] - MissingConnectorMandateID, - #[error("Missing connector transaction ID")] - MissingConnectorTransactionID, - #[error("Missing connector refund ID")] - MissingConnectorRefundID, - #[error("Missing apple pay tokenization data")] - MissingApplePayTokenData, - #[error("Webhooks not implemented for this connector")] - WebhooksNotImplemented, - #[error("Failed to decode webhook event body")] - WebhookBodyDecodingFailed, - #[error("Signature not found for incoming webhook")] - WebhookSignatureNotFound, - #[error("Failed to verify webhook source")] - WebhookSourceVerificationFailed, - #[error("Could not find merchant secret in DB for incoming webhook source verification")] - WebhookVerificationSecretNotFound, - #[error("Merchant secret found for incoming webhook source verification is invalid")] - WebhookVerificationSecretInvalid, - #[error("Incoming webhook object reference ID not found")] - WebhookReferenceIdNotFound, - #[error("Incoming webhook event type not found")] - WebhookEventTypeNotFound, - #[error("Incoming webhook event resource object not found")] - WebhookResourceObjectNotFound, - #[error("Could not respond to the incoming webhook event")] - WebhookResponseEncodingFailed, - #[error("Invalid Date/time format")] - InvalidDateFormat, - #[error("Date Formatting Failed")] - DateFormattingFailed, - #[error("Invalid Data format")] - InvalidDataFormat { field_name: &'static str }, - #[error("Payment Method data / Payment Method Type / Payment Experience Mismatch ")] - MismatchedPaymentData, - #[error("Failed to parse {wallet_name} wallet token")] - InvalidWalletToken { wallet_name: String }, - #[error("Missing Connector Related Transaction ID")] - MissingConnectorRelatedTransactionID { id: String }, - #[error("File Validation failed")] - FileValidationFailed { reason: String }, - #[error("Missing 3DS redirection payload: {field_name}")] - MissingConnectorRedirectionPayload { field_name: &'static str }, - #[error("Failed at connector's end with code '{code}'")] - FailedAtConnector { message: String, code: String }, - #[error("Payment Method Type not found")] - MissingPaymentMethodType, - #[error("Balance in the payment method is low")] - InSufficientBalanceInPaymentMethod, - #[error("Server responded with Request Timeout")] - RequestTimeoutReceived, - #[error("The given currency method is not configured with the given connector")] - CurrencyNotSupported { - message: String, - connector: &'static str, - }, - #[error("Invalid Configuration")] - InvalidConnectorConfig { config: &'static str }, - #[error("Failed to convert amount to required type")] - AmountConversionFailed, -} - #[derive(Debug, thiserror::Error)] pub enum HealthCheckOutGoing { #[error("Outgoing call failed with error: {message}")] @@ -335,12 +224,6 @@ pub enum ApplePayDecryptionError { DerivingSharedSecretKeyFailed, } -impl ConnectorError { - pub fn is_connector_timeout(&self) -> bool { - self == &Self::RequestTimeoutReceived - } -} - #[cfg(feature = "detailed_errors")] pub mod error_stack_parsing { diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index df529f81803..f5e90771c14 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -1,25 +1,7 @@ use common_utils::errors::ErrorSwitch; use hyperswitch_domain_models::errors::api_error_response::ApiErrorResponse; -use super::{ConnectorError, CustomersErrorResponse, StorageError}; - -impl ErrorSwitch for ConnectorError { - fn switch(&self) -> ApiErrorResponse { - match self { - Self::WebhookSourceVerificationFailed => ApiErrorResponse::WebhookAuthenticationFailed, - Self::WebhookSignatureNotFound - | Self::WebhookReferenceIdNotFound - | Self::WebhookResourceObjectNotFound - | Self::WebhookBodyDecodingFailed - | Self::WebhooksNotImplemented => ApiErrorResponse::WebhookBadRequest, - Self::WebhookEventTypeNotFound => ApiErrorResponse::WebhookUnprocessableEntity, - Self::WebhookVerificationSecretInvalid => { - ApiErrorResponse::WebhookInvalidMerchantSecret - } - _ => ApiErrorResponse::InternalServerError, - } - } -} +use super::{CustomersErrorResponse, StorageError}; impl ErrorSwitch for CustomersErrorResponse { fn switch(&self) -> api_models::errors::types::ApiErrorResponse { diff --git a/crates/router/src/events/connector_api_logs.rs b/crates/router/src/events/connector_api_logs.rs index af026356826..7c354620b5a 100644 --- a/crates/router/src/events/connector_api_logs.rs +++ b/crates/router/src/events/connector_api_logs.rs @@ -1,95 +1,8 @@ -use common_utils::request::Method; -use router_env::tracing_actix_web::RequestId; -use serde::Serialize; -use serde_json::json; -use time::OffsetDateTime; +pub use hyperswitch_interfaces::events::connector_api_logs::ConnectorEvent; use super::EventType; use crate::services::kafka::KafkaMessage; -#[derive(Debug, Serialize)] -pub struct ConnectorEvent { - connector_name: String, - flow: String, - request: String, - masked_response: Option, - error: Option, - url: String, - method: String, - payment_id: String, - merchant_id: String, - created_at: i128, - request_id: String, - latency: u128, - refund_id: Option, - dispute_id: Option, - status_code: u16, -} - -impl ConnectorEvent { - #[allow(clippy::too_many_arguments)] - pub fn new( - connector_name: String, - flow: &str, - request: serde_json::Value, - url: String, - method: Method, - payment_id: String, - merchant_id: String, - request_id: Option<&RequestId>, - latency: u128, - refund_id: Option, - dispute_id: Option, - status_code: u16, - ) -> Self { - Self { - connector_name, - flow: flow - .rsplit_once("::") - .map(|(_, s)| s) - .unwrap_or(flow) - .to_string(), - request: request.to_string(), - masked_response: None, - error: None, - url, - method: method.to_string(), - payment_id, - merchant_id, - created_at: OffsetDateTime::now_utc().unix_timestamp_nanos() / 1_000_000, - request_id: request_id - .map(|i| i.as_hyphenated().to_string()) - .unwrap_or("NO_REQUEST_ID".to_string()), - latency, - refund_id, - dispute_id, - status_code, - } - } - - pub fn set_response_body(&mut self, response: &T) { - match masking::masked_serialize(response) { - Ok(masked) => { - self.masked_response = Some(masked.to_string()); - } - Err(er) => self.set_error(json!({"error": er.to_string()})), - } - } - - pub fn set_error_response_body(&mut self, response: &T) { - match masking::masked_serialize(response) { - Ok(masked) => { - self.error = Some(masked.to_string()); - } - Err(er) => self.set_error(json!({"error": er.to_string()})), - } - } - - pub fn set_error(&mut self, error: serde_json::Value) { - self.error = Some(error.to_string()); - } -} - impl KafkaMessage for ConnectorEvent { fn event_type(&self) -> EventType { EventType::ConnectorApiLogs diff --git a/crates/router/src/routes/metrics.rs b/crates/router/src/routes/metrics.rs index 18014c6e193..eef85410981 100644 --- a/crates/router/src/routes/metrics.rs +++ b/crates/router/src/routes/metrics.rs @@ -76,7 +76,6 @@ counter_metric!(REDIRECTION_TRIGGERED, GLOBAL_METER); // Connector Level Metric counter_metric!(REQUEST_BUILD_FAILURE, GLOBAL_METER); -counter_metric!(UNIMPLEMENTED_FLOW, GLOBAL_METER); // Connector http status code metrics counter_metric!(CONNECTOR_HTTP_STATUS_CODE_1XX_COUNT, GLOBAL_METER); counter_metric!(CONNECTOR_HTTP_STATUS_CODE_2XX_COUNT, GLOBAL_METER); diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index baa824ced3f..cccbaa496e1 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -25,6 +25,9 @@ use common_utils::{ }; use error_stack::{report, Report, ResultExt}; pub use hyperswitch_domain_models::router_response_types::RedirectForm; +pub use hyperswitch_interfaces::api::{ + BoxedConnectorIntegration, CaptureSyncMethod, ConnectorIntegration, ConnectorIntegrationAny, +}; use masking::{Maskable, PeekInterface}; use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag}; use serde::Serialize; @@ -34,7 +37,7 @@ use tera::{Context, Tera}; use self::request::{HeaderExt, RequestBuilderExt}; use super::authentication::AuthenticateAndFetch; use crate::{ - configs::{settings::Connectors, Settings}, + configs::Settings, consts, core::{ api_locking, @@ -58,22 +61,6 @@ use crate::{ }, }; -pub type BoxedConnectorIntegration<'a, T, Req, Resp> = - Box<&'a (dyn ConnectorIntegration + Send + Sync)>; - -pub trait ConnectorIntegrationAny: Send + Sync + 'static { - fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp>; -} - -impl ConnectorIntegrationAny for S -where - S: ConnectorIntegration + Send + Sync, -{ - fn get_connector_integration(&self) -> BoxedConnectorIntegration<'_, T, Req, Resp> { - Box::new(self) - } -} - pub trait ConnectorValidation: ConnectorCommon { fn validate_capture_method( &self, @@ -129,145 +116,6 @@ pub trait ConnectorValidation: ConnectorCommon { } } -#[async_trait::async_trait] -pub trait ConnectorIntegration: ConnectorIntegrationAny + Sync { - fn get_headers( - &self, - _req: &types::RouterData, - _connectors: &Connectors, - ) -> CustomResult)>, errors::ConnectorError> { - Ok(vec![]) - } - - fn get_content_type(&self) -> &'static str { - mime::APPLICATION_JSON.essence_str() - } - - /// primarily used when creating signature based on request method of payment flow - fn get_http_method(&self) -> Method { - Method::Post - } - - fn get_url( - &self, - _req: &types::RouterData, - _connectors: &Connectors, - ) -> CustomResult { - Ok(String::new()) - } - - fn get_request_body( - &self, - _req: &types::RouterData, - _connectors: &Connectors, - ) -> CustomResult { - Ok(RequestContent::Json(Box::new(json!(r#"{}"#)))) - } - - fn get_request_form_data( - &self, - _req: &types::RouterData, - ) -> CustomResult, errors::ConnectorError> { - Ok(None) - } - - fn build_request( - &self, - req: &types::RouterData, - _connectors: &Connectors, - ) -> CustomResult, errors::ConnectorError> { - metrics::UNIMPLEMENTED_FLOW.add( - &metrics::CONTEXT, - 1, - &[metrics::request::add_attributes( - "connector", - req.connector.clone(), - )], - ); - Ok(None) - } - - fn handle_response( - &self, - data: &types::RouterData, - event_builder: Option<&mut ConnectorEvent>, - _res: types::Response, - ) -> CustomResult, errors::ConnectorError> - where - T: Clone, - Req: Clone, - Resp: Clone, - { - event_builder.map(|e| e.set_error(json!({"error": "Not Implemented"}))); - Ok(data.clone()) - } - - fn get_error_response( - &self, - res: types::Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - event_builder.map(|event| event.set_error(json!({"error": res.response.escape_ascii().to_string(), "status_code": res.status_code}))); - Ok(ErrorResponse::get_not_implemented()) - } - - fn get_5xx_error_response( - &self, - res: types::Response, - event_builder: Option<&mut ConnectorEvent>, - ) -> CustomResult { - event_builder.map(|event| event.set_error(json!({"error": res.response.escape_ascii().to_string(), "status_code": res.status_code}))); - let error_message = match res.status_code { - 500 => "internal_server_error", - 501 => "not_implemented", - 502 => "bad_gateway", - 503 => "service_unavailable", - 504 => "gateway_timeout", - 505 => "http_version_not_supported", - 506 => "variant_also_negotiates", - 507 => "insufficient_storage", - 508 => "loop_detected", - 510 => "not_extended", - 511 => "network_authentication_required", - _ => "unknown_error", - }; - Ok(ErrorResponse { - code: res.status_code.to_string(), - message: error_message.to_string(), - reason: String::from_utf8(res.response.to_vec()).ok(), - status_code: res.status_code, - attempt_status: None, - connector_transaction_id: None, - }) - } - - // whenever capture sync is implemented at the connector side, this method should be overridden - fn get_multiple_capture_sync_method( - &self, - ) -> CustomResult { - Err(errors::ConnectorError::NotImplemented("multiple capture sync".into()).into()) - } - - fn get_certificate( - &self, - _req: &types::RouterData, - ) -> CustomResult, errors::ConnectorError> { - Ok(None) - } - - fn get_certificate_key( - &self, - _req: &types::RouterData, - ) -> CustomResult, errors::ConnectorError> { - Ok(None) - } -} - -pub enum CaptureSyncMethod { - Individual, - Bulk, -} - /// Handle the flow by interacting with connector module /// `connector_request` is applicable only in case if the `CallConnectorAction` is `Trigger` /// In other cases, It will be created if required, even if it is not passed diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index d864eabd093..38e0bed699f 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -52,6 +52,7 @@ pub use hyperswitch_domain_models::{ VerifyWebhookSourceResponseData, VerifyWebhookStatus, }, }; +pub use hyperswitch_interfaces::types::Response; pub use crate::core::payments::CustomerDetails; #[cfg(feature = "payouts")] @@ -684,13 +685,6 @@ pub struct ConnectorsList { pub connectors: Vec, } -#[derive(Clone, Debug)] -pub struct Response { - pub headers: Option, - pub response: bytes::Bytes, - pub status_code: u16, -} - impl ForeignTryFrom for AccessTokenRequestData { type Error = errors::ApiErrorResponse; fn foreign_try_from(connector_auth: ConnectorAuthType) -> Result { diff --git a/crates/router_derive/src/macros/misc.rs b/crates/router_derive/src/macros/misc.rs index 4717fe730d9..04c37e75950 100644 --- a/crates/router_derive/src/macros/misc.rs +++ b/crates/router_derive/src/macros/misc.rs @@ -52,6 +52,7 @@ pub fn validate_config(input: syn::DeriveInput) -> Result Result<(), ApplicationError> { #(#function_expansions)* Ok(())