From 8be82f2af250b5cfac67d75084e64d1d61d97150 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 23 Nov 2023 17:22:28 +0530 Subject: [PATCH 1/5] feat: move FRM module to OSS --- config/config.example.toml | 3 + config/development.toml | 3 + config/docker_compose.toml | 3 + crates/api_models/Cargo.toml | 3 +- crates/api_models/src/enums.rs | 31 + crates/api_models/src/routing.rs | 1 + crates/common_utils/src/events.rs | 1 + crates/euclid/src/enums.rs | 1 + crates/router/Cargo.toml | 3 +- crates/router/src/configs/settings.rs | 8 + crates/router/src/connector.rs | 7 +- crates/router/src/connector/signifyd.rs | 631 ++++++++++++++ .../src/connector/signifyd/transformers.rs | 607 ++++++++++++++ crates/router/src/connector/utils.rs | 47 +- crates/router/src/core.rs | 2 + crates/router/src/core/admin.rs | 8 +- crates/router/src/core/fraud_check.rs | 778 ++++++++++++++++++ crates/router/src/core/fraud_check/flows.rs | 36 + .../core/fraud_check/flows/checkout_flow.rs | 147 ++++ .../fraud_check/flows/fulfillment_flow.rs | 110 +++ .../core/fraud_check/flows/record_return.rs | 149 ++++ .../src/core/fraud_check/flows/sale_flow.rs | 145 ++++ .../fraud_check/flows/transaction_flow.rs | 158 ++++ .../router/src/core/fraud_check/operation.rs | 106 +++ .../fraud_check/operation/fraud_check_post.rs | 457 ++++++++++ .../fraud_check/operation/fraud_check_pre.rs | 337 ++++++++ crates/router/src/core/fraud_check/types.rs | 208 +++++ crates/router/src/core/payments.rs | 333 +++++--- crates/router/src/core/payments/flows.rs | 485 ++++++++++- .../src/core/payments/routing/transformers.rs | 1 + crates/router/src/routes.rs | 2 + crates/router/src/routes/app.rs | 24 +- crates/router/src/routes/fraud_check.rs | 42 + crates/router/src/routes/lock_utils.rs | 2 +- crates/router/src/routes/payments.rs | 138 ++++ crates/router/src/services/api.rs | 2 + crates/router/src/types.rs | 5 +- crates/router/src/types/api.rs | 10 +- crates/router/src/types/api/fraud_check.rs | 101 +++ crates/router/src/types/fraud_check.rs | 126 +++ crates/router/src/types/storage.rs | 16 +- .../router/src/types/storage/fraud_check.rs | 3 + crates/router/src/types/transformers.rs | 1 + crates/router_env/Cargo.toml | 3 +- crates/router_env/src/logger/types.rs | 3 + loadtest/config/development.toml | 3 + 46 files changed, 5134 insertions(+), 156 deletions(-) create mode 100644 crates/router/src/connector/signifyd.rs create mode 100644 crates/router/src/connector/signifyd/transformers.rs create mode 100644 crates/router/src/core/fraud_check.rs create mode 100644 crates/router/src/core/fraud_check/flows.rs create mode 100644 crates/router/src/core/fraud_check/flows/checkout_flow.rs create mode 100644 crates/router/src/core/fraud_check/flows/fulfillment_flow.rs create mode 100644 crates/router/src/core/fraud_check/flows/record_return.rs create mode 100644 crates/router/src/core/fraud_check/flows/sale_flow.rs create mode 100644 crates/router/src/core/fraud_check/flows/transaction_flow.rs create mode 100644 crates/router/src/core/fraud_check/operation.rs create mode 100644 crates/router/src/core/fraud_check/operation/fraud_check_post.rs create mode 100644 crates/router/src/core/fraud_check/operation/fraud_check_pre.rs create mode 100644 crates/router/src/core/fraud_check/types.rs create mode 100644 crates/router/src/routes/fraud_check.rs create mode 100644 crates/router/src/types/api/fraud_check.rs create mode 100644 crates/router/src/types/fraud_check.rs create mode 100644 crates/router/src/types/storage/fraud_check.rs diff --git a/config/config.example.toml b/config/config.example.toml index 7815f2400d04..492954fa1e09 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -462,3 +462,6 @@ connection_timeout = 10 # Timeout for database connection in seconds [kv_config] # TTL for KV in seconds ttl = 900 + +[frm] +is_frm_enabled = true diff --git a/config/development.toml b/config/development.toml index c82607a704c3..11af03260821 100644 --- a/config/development.toml +++ b/config/development.toml @@ -459,3 +459,6 @@ delay_between_retries_in_milliseconds = 500 [kv_config] ttl = 900 # 15 * 60 seconds + +[frm] +is_frm_enabled = true diff --git a/config/docker_compose.toml b/config/docker_compose.toml index a5294546de41..8aefcadd5c00 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -337,3 +337,6 @@ pool_size = 5 [kv_config] ttl = 900 # 15 * 60 seconds + +[frm] +is_frm_enabled = true diff --git a/crates/api_models/Cargo.toml b/crates/api_models/Cargo.toml index ce882e913282..a53f4d423f92 100644 --- a/crates/api_models/Cargo.toml +++ b/crates/api_models/Cargo.toml @@ -8,7 +8,7 @@ readme = "README.md" license.workspace = true [features] -default = ["payouts"] +default = ["payouts", "frm"] business_profile_routing = [] connector_choice_bcompat = [] errors = ["dep:actix-web", "dep:reqwest"] @@ -17,6 +17,7 @@ connector_choice_mca_id = ["euclid/connector_choice_mca_id"] dummy_connector = ["euclid/dummy_connector"] detailed_errors = [] payouts = [] +frm = [] [dependencies] actix-web = { version = "4.3.1", optional = true } diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index c4e4aa90c4b8..6a428fe9b4d8 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -232,6 +232,7 @@ pub enum RoutableConnectors { Prophetpay, Rapyd, Shift4, + Signifyd, Square, Stax, Stripe, @@ -276,6 +277,36 @@ impl From for RoutableConnectors { } } +#[cfg(feature = "frm")] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, + ToSchema, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum FrmConnectors { + /// Signifyd Risk Manager. Official docs: https://docs.signifyd.com/ + Signifyd, +} + +#[cfg(feature = "frm")] +impl From for RoutableConnectors { + fn from(value: FrmConnectors) -> Self { + match value { + FrmConnectors::Signifyd => Self::Signifyd, + } + } +} + #[derive( Clone, Copy, diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 47a44ea7443e..68624e8fa1f5 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -340,6 +340,7 @@ impl From for ast::ConnectorChoice { RoutableConnectors::Prophetpay => euclid_enums::Connector::Prophetpay, RoutableConnectors::Rapyd => euclid_enums::Connector::Rapyd, RoutableConnectors::Shift4 => euclid_enums::Connector::Shift4, + RoutableConnectors::Signifyd => euclid_enums::Connector::Signifyd, RoutableConnectors::Square => euclid_enums::Connector::Square, RoutableConnectors::Stax => euclid_enums::Connector::Stax, RoutableConnectors::Stripe => euclid_enums::Connector::Stripe, diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 14b8d4de1c36..c9efbb73c208 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -45,6 +45,7 @@ pub enum ApiEventsType { // TODO: This has to be removed once the corresponding apiEventTypes are created Miscellaneous, RustLocker, + FraudCheck, } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/euclid/src/enums.rs b/crates/euclid/src/enums.rs index dc6d9f66a58f..e34489249044 100644 --- a/crates/euclid/src/enums.rs +++ b/crates/euclid/src/enums.rs @@ -120,6 +120,7 @@ pub enum Connector { Prophetpay, Rapyd, Shift4, + Signifyd, Square, Stax, Stripe, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index 25feb373b734..648fad087be8 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -9,10 +9,11 @@ readme = "README.md" license.workspace = true [features] -default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry"] +default = ["kv_store", "stripe", "oltp", "olap", "backwards_compatibility", "accounts_cache", "dummy_connector", "payouts", "profile_specific_fallback_routing", "retry", "frm"] s3 = ["dep:aws-sdk-s3", "dep:aws-config"] kms = ["external_services/kms", "dep:aws-config"] email = ["external_services/email", "dep:aws-config"] +frm = [] basilisk = ["kms"] stripe = ["dep:serde_qs"] release = ["kms", "stripe", "basilisk", "s3", "email", "business_profile_routing", "accounts_cache", "kv_store", "profile_specific_fallback_routing"] diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 0007e636926c..68b0cc625ceb 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -107,6 +107,14 @@ pub struct Settings { pub analytics: AnalyticsConfig, #[cfg(feature = "kv_store")] pub kv_config: KvConfig, + #[cfg(feature = "frm")] + pub frm: Frm, +} + +#[cfg(feature = "frm")] +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Frm { + pub is_frm_enabled: bool, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 3a83fea0d910..55c61442591d 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -40,6 +40,7 @@ pub mod powertranz; pub mod prophetpay; pub mod rapyd; pub mod shift4; +pub mod signifyd; pub mod square; pub mod stax; pub mod stripe; @@ -63,7 +64,7 @@ pub use self::{ iatapay::Iatapay, klarna::Klarna, mollie::Mollie, multisafepay::Multisafepay, nexinets::Nexinets, nmi::Nmi, noon::Noon, nuvei::Nuvei, opayo::Opayo, opennode::Opennode, payeezy::Payeezy, payme::Payme, paypal::Paypal, payu::Payu, powertranz::Powertranz, - prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, square::Square, stax::Stax, - stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, - worldpay::Worldpay, zen::Zen, + prophetpay::Prophetpay, rapyd::Rapyd, shift4::Shift4, signifyd::Signifyd, square::Square, + stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, + worldline::Worldline, worldpay::Worldpay, zen::Zen, }; diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs new file mode 100644 index 000000000000..4b2abe893882 --- /dev/null +++ b/crates/router/src/connector/signifyd.rs @@ -0,0 +1,631 @@ +pub mod transformers; +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::PeekInterface; +use transformers as signifyd; + +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{self, request, ConnectorIntegration, ConnectorValidation}, + types::{ + self, + api::{self, fraud_check as frm_api, ConnectorCommon, ConnectorCommonExt}, + fraud_check as frm_types, ErrorResponse, Response, + }, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Signifyd; + +impl ConnectorCommonExt for Signifyd +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + _connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let mut header = vec![( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + )]; + + let mut api_key = self.get_auth_header(&req.connector_auth_type)?; + header.append(&mut api_key); + Ok(header) + } +} + +impl ConnectorCommon for Signifyd { + fn id(&self) -> &'static str { + "signifyd" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, _connectors: &'a settings::Connectors) -> &'a str { + "https://api.signifyd.com" + } + + fn get_auth_header( + &self, + auth_type: &types::ConnectorAuthType, + ) -> CustomResult)>, errors::ConnectorError> { + let auth = signifyd::SignifydAuthType::try_from(auth_type) + .change_context(errors::ConnectorError::FailedToObtainAuthType)?; + let auth_api_key = format!("Basic {}", auth.api_key.peek()); + + Ok(vec![( + headers::AUTHORIZATION.to_string(), + request::Mask::into_masked(auth_api_key), + )]) + } + + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydErrorResponse = res + .response + .parse_struct("SignifydErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.messages.join(" &"), + reason: Some(response.errors.to_string()), + attempt_status: None, + }) + } +} + +impl api::Payment for Signifyd {} +impl api::PaymentAuthorize for Signifyd {} +impl api::PaymentSync for Signifyd {} +impl api::PaymentVoid for Signifyd {} +impl api::PaymentCapture for Signifyd {} +impl api::MandateSetup for Signifyd {} +impl api::ConnectorAccessToken for Signifyd {} +impl api::PaymentToken for Signifyd {} +impl api::Refund for Signifyd {} +impl api::RefundExecute for Signifyd {} +impl api::RefundSync for Signifyd {} +impl ConnectorValidation for Signifyd {} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Signifyd +{ +} + +impl api::PaymentSession for Signifyd {} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration + for Signifyd +{ +} + +impl ConnectorIntegration for Signifyd {} + +impl frm_api::FraudCheck for Signifyd {} +impl frm_api::FraudCheckSale for Signifyd {} +impl frm_api::FraudCheckCheckout for Signifyd {} +impl frm_api::FraudCheckTransaction for Signifyd {} +impl frm_api::FraudCheckFulfillment for Signifyd {} +impl frm_api::FraudCheckRecordReturn for Signifyd {} + +impl + ConnectorIntegration< + frm_api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmSaleRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &frm_types::FrmSaleRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/sales" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmSaleRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsSaleRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmSaleRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmSaleType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmSaleType::get_headers(self, req, connectors)?) + .body(frm_types::FrmSaleType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmSaleRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Sale") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + frm_api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/checkouts" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmCheckoutRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsCheckoutRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmCheckoutRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmCheckoutType::get_url(self, req, connectors)?) + .attach_default_headers() + .headers(frm_types::FrmCheckoutType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmCheckoutType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmCheckoutRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Checkout") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + frm_api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/transactions" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmTransactionRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsTransactionRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmTransactionRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmTransactionType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmTransactionType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmTransactionType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmTransactionRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsResponse = res + .response + .parse_struct("SignifydPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + frm_api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/fulfillments" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmFulfillmentRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = &req.request.fulfillment_request; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmFulfillmentRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmFulfillmentType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmFulfillmentType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmFulfillmentType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmFulfillmentRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::FrmFullfillmentSignifydApiResponse = res + .response + .parse_struct("FrmFullfillmentSignifydApiResponse Sale") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + frm_types::FrmFulfillmentRouterData::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +impl + ConnectorIntegration< + frm_api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for Signifyd +{ + fn get_headers( + &self, + req: &frm_types::FrmRecordReturnRouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + self.build_headers(req, connectors) + } + + fn get_content_type(&self) -> &'static str { + self.common_get_content_type() + } + + fn get_url( + &self, + _req: &frm_types::FrmRecordReturnRouterData, + connectors: &settings::Connectors, + ) -> CustomResult { + Ok(format!( + "{}{}", + self.base_url(connectors), + "/v3/orders/events/returns/records" + )) + } + + fn get_request_body( + &self, + req: &frm_types::FrmRecordReturnRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = signifyd::SignifydPaymentsRecordReturnRequest::try_from(req)?; + let signifyd_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(signifyd_req)) + } + + fn build_request( + &self, + req: &frm_types::FrmRecordReturnRouterData, + connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + Ok(Some( + services::RequestBuilder::new() + .method(services::Method::Post) + .url(&frm_types::FrmRecordReturnType::get_url( + self, req, connectors, + )?) + .attach_default_headers() + .headers(frm_types::FrmRecordReturnType::get_headers( + self, req, connectors, + )?) + .body(frm_types::FrmRecordReturnType::get_request_body( + self, req, connectors, + )?) + .build(), + )) + } + + fn handle_response( + &self, + data: &frm_types::FrmRecordReturnRouterData, + res: Response, + ) -> CustomResult { + let response: signifyd::SignifydPaymentsRecordReturnResponse = res + .response + .parse_struct("SignifydPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + ::try_from(types::ResponseRouterData { + response, + data: data.clone(), + http_code: res.status_code, + }) + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Signifyd { + fn get_webhook_object_reference_id( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_event_type( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } + + fn get_webhook_resource_object( + &self, + _request: &api::IncomingWebhookRequestDetails<'_>, + ) -> CustomResult, errors::ConnectorError> { + Err(errors::ConnectorError::WebhooksNotImplemented).into_report() + } +} diff --git a/crates/router/src/connector/signifyd/transformers.rs b/crates/router/src/connector/signifyd/transformers.rs new file mode 100644 index 000000000000..958abaa7f007 --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers.rs @@ -0,0 +1,607 @@ +use bigdecimal::ToPrimitive; +use common_utils::pii::Email; +use error_stack; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; +use utoipa::ToSchema; + +use crate::{ + connector::utils::{ + AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckRecordReturnRequest, + FraudCheckSaleRequest, FraudCheckTransactionRequest, RouterData, + }, + core::{ + errors, + fraud_check::types::{self as core_types, FrmFulfillmentRequest}, + }, + types::{ + self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + ResponseId, ResponseRouterData, + }, +}; + +#[allow(dead_code)] +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DecisionDelivery { + Sync, + AsyncOnly, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Purchase { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + order_channel: OrderChannel, + total_price: i64, + products: Vec, + shipments: Shipments, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderChannel { + Web, + Phone, + MobileApp, + Social, + Marketplace, + InStoreKiosk, + ScanAndGo, + SmartTv, + Mit, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Products { + item_name: String, + item_price: i64, + item_quantity: i32, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +pub struct Shipments { + destination: Destination, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Destination { + full_name: Secret, + organization: Option, + email: Option, + address: Address, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Address { + street_address: Secret, + unit: Option>, + postal_code: Secret, + city: String, + province_code: Secret, + country_code: common_enums::CountryAlpha2, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsSaleRequest { + order_id: String, + purchase: Purchase, + decision_delivery: DecisionDelivery, +} + +impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmSaleRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + purchase, + decision_delivery: DecisionDelivery::Sync, + }) + } +} + +pub struct SignifydAuthType { + pub api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for SignifydAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} + +// PaymentsResponse + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Decision { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + checkpoint_action: SignifydPaymentStatus, + checkpoint_action_reason: Option, + checkpoint_action_policy: Option, + score: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SignifydPaymentStatus { + Accept, + Challenge, + Credit, + Hold, + Reject, +} + +impl From for storage_enums::FraudCheckStatus { + fn from(item: SignifydPaymentStatus) -> Self { + match item { + SignifydPaymentStatus::Accept => Self::Legit, + SignifydPaymentStatus::Reject => Self::Fraud, + SignifydPaymentStatus::Hold => Self::ManualReview, + _ => Self::Pending, + } + } +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsResponse { + signifyd_id: i64, + order_id: String, + decision: Decision, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + status: storage_enums::FraudCheckStatus::from( + item.response.decision.checkpoint_action, + ), + connector_metadata: None, + score: item.response.decision.score.and_then(|data| data.to_i32()), + reason: item + .response + .decision + .checkpoint_action_reason + .map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct SignifydErrorResponse { + pub messages: Vec, + pub errors: serde_json::Value, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Transactions { + transaction_id: String, + gateway_status_code: String, + payment_method: storage_enums::PaymentMethod, + amount: i64, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsTransactionRequest { + order_id: String, + checkout_id: String, + transactions: Transactions, +} + +impl From for GatewayStatusCode { + fn from(item: storage_enums::AttemptStatus) -> Self { + match item { + storage_enums::AttemptStatus::Pending => Self::Pending, + storage_enums::AttemptStatus::Failure => Self::Failure, + storage_enums::AttemptStatus::Charged => Self::Success, + _ => Self::Pending, + } + } +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for SignifydPaymentsTransactionRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + let currency = item.request.get_currency()?; + let transactions = Transactions { + amount: item.request.amount, + transaction_id: item.clone().payment_id, + gateway_status_code: GatewayStatusCode::from(item.status).to_string(), + payment_method: item.payment_method, + currency, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + checkout_id: item.payment_id.clone(), + transactions, + }) + } +} + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum GatewayStatusCode { + Success, + Failure, + #[default] + Pending, + Error, + Cancelled, + Expired, + SoftDecline, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsCheckoutRequest { + checkout_id: String, + order_id: String, + purchase: Purchase, +} + +impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmCheckoutRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments: Shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + checkout_id: item.payment_id.clone(), + order_id: item.attempt_id.clone(), + purchase, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydApiRequest { + pub order_id: String, + pub fulfillment_status: Option, + pub fulfillments: Vec, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub enum FulfillmentStatus { + PARTIAL, + COMPLETE, + REPLACEMENT, + CANCELED, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Fulfillments { + pub shipment_id: String, + pub products: Option>, + pub destination: Destination, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Product { + pub item_name: String, + pub item_quantity: i64, + pub item_id: String, +} + +impl From for FrmFullfillmentSignifydApiRequest { + fn from(req: FrmFulfillmentRequest) -> Self { + Self { + order_id: req.order_id, + fulfillment_status: req.fulfillment_status.map(FulfillmentStatus::from), + fulfillments: req + .fulfillments + .iter() + .map(|f| Fulfillments::from(f.clone())) + .collect(), + } + } +} + +impl From for FulfillmentStatus { + fn from(status: core_types::FulfillmentStatus) -> Self { + match status { + core_types::FulfillmentStatus::PARTIAL => Self::PARTIAL, + core_types::FulfillmentStatus::COMPLETE => Self::COMPLETE, + core_types::FulfillmentStatus::REPLACEMENT => Self::REPLACEMENT, + core_types::FulfillmentStatus::CANCELED => Self::CANCELED, + } + } +} + +impl From for Fulfillments { + fn from(fulfillment: core_types::Fulfillments) -> Self { + Self { + shipment_id: fulfillment.shipment_id, + products: fulfillment + .products + .map(|products| products.iter().map(|p| Product::from(p.clone())).collect()), + destination: Destination::from(fulfillment.destination), + } + } +} + +impl From for Product { + fn from(product: core_types::Product) -> Self { + Self { + item_name: product.item_name, + item_quantity: product.item_quantity, + item_id: product.item_id, + } + } +} + +impl From for Destination { + fn from(destination: core_types::Destination) -> Self { + Self { + full_name: destination.full_name, + organization: destination.organization, + email: destination.email, + address: Address::from(destination.address), + } + } +} + +impl From for Address { + fn from(address: core_types::Address) -> Self { + Self { + street_address: address.street_address, + unit: address.unit, + postal_code: address.postal_code, + city: address.city, + province_code: address.province_code, + country_code: address.country_code, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydApiResponse { + pub order_id: String, + pub shipment_ids: Vec, +} + +impl + TryFrom< + ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + > + for types::RouterData< + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { + order_id: item.response.order_id, + shipment_ids: item.response.shipment_ids, + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydRefund { + method: RefundMethod, + amount: String, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnRequest { + order_id: String, + return_id: String, + refund_transaction_id: Option, + refund: SignifydRefund, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RefundMethod { + StoreCredit, + OriginalPaymentInstrument, + NewPaymentInstrument, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnResponse { + return_id: String, + order_id: String, +} + +impl + TryFrom< + ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::RecordReturnResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + return_id: Some(item.response.return_id.to_string()), + connector_metadata: None, + }), + ..item.data + }) + } +} + +impl TryFrom<&frm_types::FrmRecordReturnRouterData> for SignifydPaymentsRecordReturnRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmRecordReturnRouterData) -> Result { + let currency = item.request.get_currency()?; + let refund = SignifydRefund { + method: item.request.refund_method.clone(), + amount: item.request.amount.to_string(), + currency, + }; + Ok(Self { + return_id: uuid::Uuid::new_v4().to_string(), + refund_transaction_id: item.request.refund_transaction_id.clone(), + refund, + order_id: item.attempt_id.clone(), + }) + } +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index a098cef5b778..b0cd79b3a05e 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -25,7 +25,9 @@ use crate::{ }, pii::PeekInterface, types::{ - self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, + self, api, fraud_check, + storage::{enums as storage_enums, payment_attempt::PaymentAttemptExt}, + transformers::ForeignTryFrom, PaymentsCancelData, ResponseId, }, utils::{OptionExt, ValueExt}, @@ -1580,3 +1582,46 @@ pub fn validate_currency( } Ok(()) } + +pub trait FraudCheckSaleRequest { + fn get_order_details(&self) -> Result, Error>; +} + +impl FraudCheckSaleRequest for fraud_check::FraudCheckSaleData { + fn get_order_details(&self) -> Result, Error> { + self.order_details + .clone() + .ok_or_else(missing_field_err("order_details")) + } +} + +pub trait FraudCheckCheckoutRequest { + fn get_order_details(&self) -> Result, Error>; +} +impl FraudCheckCheckoutRequest for fraud_check::FraudCheckCheckoutData { + fn get_order_details(&self) -> Result, Error> { + self.order_details + .clone() + .ok_or_else(missing_field_err("order_details")) + } +} + +pub trait FraudCheckTransactionRequest { + fn get_currency(&self) -> Result; +} + +impl FraudCheckTransactionRequest for fraud_check::FraudCheckTransactionData { + fn get_currency(&self) -> Result { + self.currency.ok_or_else(missing_field_err("currency")) + } +} + +pub trait FraudCheckRecordReturnRequest { + fn get_currency(&self) -> Result; +} + +impl FraudCheckRecordReturnRequest for fraud_check::FraudCheckRecordReturnData { + fn get_currency(&self) -> Result { + self.currency.ok_or_else(missing_field_err("currency")) + } +} diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index a429cab482b4..67461c559497 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -9,6 +9,8 @@ pub mod customers; pub mod disputes; pub mod errors; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod gsm; pub mod locker_migration; pub mod mandate; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 107e8f8859d6..5ab543d382f5 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1716,10 +1716,12 @@ pub(crate) fn validate_auth_and_metadata_type( zen::transformers::ZenAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Signifyd | api_enums::Connector::Plaid => { - Err(report!(errors::ConnectorError::InvalidConnectorName) - .attach_printable(format!("invalid connector name: {connector_name}"))) + api_enums::Connector::Signifyd => { + signifyd::transformers::SignifydAuthType::try_from(val)?; + Ok(()) } + api_enums::Connector::Plaid => Err(report!(errors::ConnectorError::InvalidConnectorName) + .attach_printable(format!("invalid connector name: {connector_name}"))), } } diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs new file mode 100644 index 000000000000..0d2a10c7d25c --- /dev/null +++ b/crates/router/src/core/fraud_check.rs @@ -0,0 +1,778 @@ +use std::fmt::Debug; + +use api_models::{admin::FrmConfigs, enums as api_enums, payments::AdditionalPaymentData}; +use error_stack::ResultExt; +use masking::PeekInterface; +use router_env::{ + logger, + tracing::{self, instrument}, +}; + +use self::{ + flows::{self as frm_flows, FeatureFrm}, + types::{ + self as frm_core_types, ConnectorDetailsCore, FrmConfigsObject, FrmData, FrmInfo, + PaymentDetails, PaymentToFrmData, + }, +}; +use super::errors::{ConnectorErrorExt, RouterResponse}; +use crate::{ + connector::signifyd::transformers::FrmFullfillmentSignifydApiRequest, + core::{ + errors::{self, RouterResult}, + payments::{ + self, flows::ConstructFlowSpecificData, helpers::get_additional_payment_data, + operations::BoxedOperation, + }, + utils as core_utils, + }, + db::StorageInterface, + routes::AppState, + services, + types::{ + self as oss_types, + api::{routing::FrmRoutingAlgorithm, Connector, FraudCheckConnectorData, Fulfillment}, + domain, fraud_check as frm_types, + storage::{ + enums::{ + AttemptStatus, FraudCheckLastStep, FraudCheckStatus, FraudCheckType, FrmSuggestion, + IntentStatus, + }, + fraud_check::{FraudCheck, FraudCheckUpdate}, + PaymentIntent, + }, + }, + utils::ValueExt, +}; +pub mod flows; +pub mod operation; +pub mod types; + +#[instrument(skip_all)] +pub async fn call_frm_service( + state: &AppState, + payment_data: &mut payments::PaymentData, + frm_data: FrmData, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, +) -> RouterResult> +where + F: Send + Clone, + + // To create connector flow specific interface data + FrmData: ConstructFlowSpecificData, + oss_types::RouterData: FeatureFrm + Send, + + // To construct connector flow specific api + dyn Connector: services::api::ConnectorIntegration, +{ + let merchant_connector_account = payments::construct_profile_id_and_get_mca( + state, + merchant_account, + payment_data, + &frm_data.connector_details.connector_name, + None, + key_store, + false, + ) + .await?; + + let router_data = frm_data + .construct_router_data( + state, + &frm_data.connector_details.connector_name, + merchant_account, + key_store, + customer, + &merchant_connector_account, + ) + .await?; + let connector = + FraudCheckConnectorData::get_connector_by_name(&frm_data.connector_details.connector_name)?; + let router_data_res = router_data + .decide_frm_flows( + state, + &connector, + payments::CallConnectorAction::Trigger, + merchant_account, + ) + .await?; + + Ok(router_data_res) +} + +pub async fn should_call_frm( + merchant_account: &domain::MerchantAccount, + payment_data: &payments::PaymentData, + db: &dyn StorageInterface, + key_store: domain::MerchantKeyStore, +) -> RouterResult<( + bool, + Option, + Option, + Option, +)> +where + F: Send + Clone, +{ + match merchant_account.frm_routing_algorithm.clone() { + Some(frm_routing_algorithm_value) => { + let frm_routing_algorithm_struct: FrmRoutingAlgorithm = frm_routing_algorithm_value + .clone() + .parse_value("FrmRoutingAlgorithm") + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_routing_algorithm", + }) + .attach_printable("Data field not found in frm_routing_algorithm")?; + + let profile_id = core_utils::get_profile_id_from_business_details( + payment_data.payment_intent.business_country, + payment_data.payment_intent.business_label.as_ref(), + merchant_account, + payment_data.payment_intent.profile_id.as_ref(), + db, + false, + ) + .await + .attach_printable("Could not find profile id from business details")?; + + let merchant_connector_account_from_db_option = db + .find_merchant_connector_account_by_profile_id_connector_name( + &profile_id, + &frm_routing_algorithm_struct.data, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: merchant_account.merchant_id.clone(), + }) + .ok(); + + match merchant_connector_account_from_db_option { + Some(merchant_connector_account_from_db) => { + let frm_configs_option = merchant_connector_account_from_db + .frm_configs + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_configs", + }) + .ok(); + match frm_configs_option { + Some(frm_configs_value) => { + let frm_configs_struct: Vec = frm_configs_value + .iter() + .map(|config| { config + .peek() + .clone() + .parse_value("FrmConfigs") + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "frm_configs".to_string(), + expected_format: "[{ \"gateway\": \"stripe\", \"payment_methods\": [{ \"payment_method\": \"card\",\"payment_method_types\": [{\"payment_method_type\": \"credit\",\"card_networks\": [\"Visa\"],\"flow\": \"pre\",\"action\": \"cancel_txn\"}]}]}]".to_string(), + }) + }) + .collect::, _>>()?; + + let mut is_frm_connector_enabled = false; + let mut is_frm_pm_enabled = false; + let mut is_frm_pmt_enabled = false; + let filtered_frm_config = frm_configs_struct + .iter() + .filter(|frm_config| { + match ( + &payment_data.clone().payment_attempt.connector, + &frm_config.gateway, + ) { + (Some(current_connector), Some(configured_connector)) => { + let is_enabled = *current_connector + == configured_connector.to_string(); + if is_enabled { + is_frm_connector_enabled = true; + } + is_enabled + } + (None, _) | (_, None) => true, + } + }) + .collect::>(); + let filtered_payment_methods = filtered_frm_config + .iter() + .map(|frm_config| { + let filtered_frm_config_by_pm = frm_config + .payment_methods + .iter() + .filter(|frm_config_pm| { + match ( + payment_data.payment_attempt.payment_method, + frm_config_pm.payment_method, + ) { + ( + Some(current_pm), + Some(configured_connector_pm), + ) => { + let is_enabled = current_pm.to_string() + == configured_connector_pm.to_string(); + if is_enabled { + is_frm_pm_enabled = true; + } + is_enabled + } + (None, _) | (_, None) => true, + } + }) + .collect::>(); + filtered_frm_config_by_pm + }) + .collect::>() + .concat(); + let additional_payment_data = match &payment_data.payment_method_data { + Some(pmd) => { + let additional_payment_data = + get_additional_payment_data(pmd, db).await; + Some(additional_payment_data) + } + None => payment_data + .payment_attempt + .payment_method_data + .as_ref() + .map(|pm_data| { + pm_data.clone().parse_value::( + "AdditionalPaymentData", + ) + }) + .transpose() + .unwrap_or_default(), // Making this default in case of error as we don't want to fail payment for frm errors + }; + let filtered_payment_method_types = filtered_payment_methods + .iter() + .map(|frm_pm_config| { + let filtered_pm_config_by_pmt = frm_pm_config + .payment_method_types + .iter() + .filter(|frm_pm_config_by_pmt| { + match ( + &payment_data + .clone() + .payment_attempt + .payment_method_type, + frm_pm_config_by_pmt.payment_method_type, + ) { + (Some(curr), Some(conf)) + if curr.to_string() == conf.to_string() => + { + is_frm_pmt_enabled = true; + true + } + (None, Some(conf)) => match additional_payment_data + .clone() + { + Some(AdditionalPaymentData::Card(card)) => { + let card_type = card + .card_type + .unwrap_or_else(|| "debit".to_string()); + let is_enabled = card_type.to_lowercase() + == conf.to_string().to_lowercase(); + if is_enabled { + is_frm_pmt_enabled = true; + } + is_enabled + } + _ => false, + }, + _ => false, + } + }) + .collect::>(); + filtered_pm_config_by_pmt + }) + .collect::>() + .concat(); + let is_frm_enabled = + is_frm_connector_enabled && is_frm_pm_enabled && is_frm_pmt_enabled; + logger::debug!( + "frm_configs {:?} {:?} {:?} {:?}", + is_frm_connector_enabled, + is_frm_pm_enabled, + is_frm_pmt_enabled, + is_frm_enabled + ); + // filtered_frm_config... + // Panic Safety: we are first checking if the object is present... only if present, we try to fetch index 0 + let frm_configs_object = FrmConfigsObject { + frm_enabled_gateway: if filtered_frm_config.is_empty() { + None + } else { + filtered_frm_config[0].gateway + }, + frm_enabled_pm: if filtered_payment_methods.is_empty() { + None + } else { + filtered_payment_methods[0].payment_method + }, + frm_enabled_pm_type: if filtered_payment_method_types.is_empty() { + None + } else { + filtered_payment_method_types[0].payment_method_type + }, + frm_action: if filtered_payment_method_types.is_empty() { + api_enums::FrmAction::ManualReview + } else { + filtered_payment_method_types[0].clone().action + }, + frm_preferred_flow_type: if filtered_payment_method_types.is_empty() + { + api_enums::FrmPreferredFlowTypes::Pre + } else { + filtered_payment_method_types[0].clone().flow + }, + }; + logger::debug!( + "frm_routing_configs: {:?} {:?} {:?} {:?}", + frm_routing_algorithm_struct, + profile_id, + frm_configs_object, + is_frm_enabled + ); + Ok(( + is_frm_enabled, + Some(frm_routing_algorithm_struct), + Some(profile_id.to_string()), + Some(frm_configs_object), + )) + } + None => { + logger::error!("Cannot find frm_configs for FRM provider"); + Ok((false, None, None, None)) + } + } + } + None => { + logger::error!("Cannot find merchant connector account for FRM provider"); + Ok((false, None, None, None)) + } + } + } + _ => Ok((false, None, None, None)), + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn make_frm_data_and_fraud_check_operation<'a, F>( + _db: &dyn StorageInterface, + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: payments::PaymentData, + frm_routing_algorithm: FrmRoutingAlgorithm, + profile_id: String, + frm_configs: FrmConfigsObject, + _customer: &Option, +) -> RouterResult> +where + F: Send + Clone, +{ + let order_details = payment_data + .payment_intent + .order_details + .clone() + .or_else(|| + // when the order_details are present within the meta_data, we need to take those to support backward compatibility + payment_data.payment_intent.metadata.clone().and_then(|meta| { + let order_details = meta.peek().get("order_details").to_owned(); + order_details.map(|order| vec![masking::Secret::new(order.to_owned())]) + })) + .map(|order_details_value| { + order_details_value + .into_iter() + .map(|data| { + data.peek() + .to_owned() + .parse_value("OrderDetailsWithAmount") + .attach_printable("unable to parse OrderDetailsWithAmount") + }) + .collect::, _>>() + .unwrap_or_default() + }); + + let frm_connector_details = ConnectorDetailsCore { + connector_name: frm_routing_algorithm.data, + profile_id, + }; + + let payment_to_frm_data = PaymentToFrmData { + amount: payment_data.amount, + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: merchant_account.to_owned(), + address: payment_data.address.clone(), + connector_details: frm_connector_details.clone(), + order_details, + }; + + let fraud_check_operation: operation::BoxedFraudCheckOperation = + match frm_configs.frm_preferred_flow_type { + api_enums::FrmPreferredFlowTypes::Pre => Box::new(operation::FraudCheckPre), + api_enums::FrmPreferredFlowTypes::Post => Box::new(operation::FraudCheckPost), + }; + let frm_data = fraud_check_operation + .to_get_tracker()? + .get_trackers(state, payment_to_frm_data, frm_connector_details) + .await?; + Ok(FrmInfo { + fraud_check_operation, + frm_data, + suggested_action: None, + }) +} + +#[allow(clippy::too_many_arguments)] +pub async fn pre_payment_frm_core<'a, F>( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + frm_info: &mut FrmInfo, + frm_configs: FrmConfigsObject, + customer: &Option, + should_continue_transaction: &mut bool, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if let Some(frm_data) = &mut frm_info.frm_data { + if matches!( + frm_configs.frm_preferred_flow_type, + api_enums::FrmPreferredFlowTypes::Pre + ) { + let fraud_check_operation = &mut frm_info.fraud_check_operation; + + let frm_router_data = fraud_check_operation + .to_domain()? + .pre_payment_frm( + state, + payment_data, + frm_data, + merchant_account, + customer, + key_store, + ) + .await?; + let frm_data_updated = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.clone(), + payment_data, + None, + frm_router_data, + ) + .await?; + let frm_fraud_check = frm_data_updated.fraud_check.clone(); + payment_data.frm_message = Some(frm_fraud_check.clone()); + if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) + //DontTakeAction + { + *should_continue_transaction = false; + if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) { + frm_info.suggested_action = Some(FrmSuggestion::FrmCancelTransaction); + } else if matches!(frm_configs.frm_action, api_enums::FrmAction::ManualReview) { + frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview); + } + } + logger::debug!( + "frm_updated_data: {:?} {:?}", + frm_info.fraud_check_operation, + frm_info.suggested_action + ); + Ok(Some(frm_data_updated)) + } else { + Ok(Some(frm_data.to_owned())) + } + } else { + Ok(None) + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn post_payment_frm_core<'a, F>( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + frm_info: &mut FrmInfo, + frm_configs: FrmConfigsObject, + customer: &Option, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if let Some(frm_data) = &mut frm_info.frm_data { + // Allow the Post flow only if the payment is succeeded, + // this logic has to be removed if we are going to call /sale or /transaction after failed transaction + let fraud_check_operation = &mut frm_info.fraud_check_operation; + if payment_data.payment_attempt.status == AttemptStatus::Charged { + let frm_router_data_opt = fraud_check_operation + .to_domain()? + .post_payment_frm( + state, + payment_data, + frm_data, + merchant_account, + customer, + key_store.clone(), + ) + .await?; + if let Some(frm_router_data) = frm_router_data_opt { + let mut frm_data = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.to_owned(), + payment_data, + None, + frm_router_data.to_owned(), + ) + .await?; + + payment_data.frm_message = Some(frm_data.fraud_check.clone()); + logger::debug!( + "frm_updated_data: {:?} {:?}", + frm_data, + payment_data.frm_message + ); + let mut frm_suggestion = None; + fraud_check_operation + .to_domain()? + .execute_post_tasks( + state, + &mut frm_data, + merchant_account, + frm_configs, + &mut frm_suggestion, + key_store, + payment_data, + customer, + ) + .await?; + logger::debug!("frm_post_tasks_data: {:?}", frm_data); + let updated_frm_data = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.to_owned(), + payment_data, + frm_suggestion, + frm_router_data.to_owned(), + ) + .await?; + return Ok(Some(updated_frm_data)); + } + } + + Ok(Some(frm_data.to_owned())) + } else { + Ok(None) + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn call_frm_before_connector_call<'a, F, Req, Ctx>( + db: &dyn StorageInterface, + operation: &BoxedOperation<'_, F, Req, Ctx>, + merchant_account: &domain::MerchantAccount, + payment_data: &mut payments::PaymentData, + state: &AppState, + frm_info: &mut Option>, + customer: &Option, + should_continue_transaction: &mut bool, + key_store: domain::MerchantKeyStore, +) -> RouterResult> +where + F: Send + Clone, +{ + if is_operation_allowed(operation) { + let (is_frm_enabled, frm_routing_algorithm, frm_connector_label, frm_configs) = + should_call_frm(merchant_account, payment_data, db, key_store.clone()).await?; + if let Some((frm_routing_algorithm_val, profile_id)) = + frm_routing_algorithm.zip(frm_connector_label) + { + if let Some(frm_configs) = frm_configs.clone() { + let mut updated_frm_info = make_frm_data_and_fraud_check_operation( + db, + state, + merchant_account, + payment_data.to_owned(), + frm_routing_algorithm_val, + profile_id, + frm_configs.clone(), + customer, + ) + .await?; + + if is_frm_enabled { + pre_payment_frm_core( + state, + merchant_account, + payment_data, + &mut updated_frm_info, + frm_configs, + customer, + should_continue_transaction, + key_store, + ) + .await?; + } + *frm_info = Some(updated_frm_info); + } + } + logger::debug!("frm_configs: {:?} {:?}", frm_configs, is_frm_enabled); + return Ok(frm_configs); + } + Ok(None) +} + +pub fn is_operation_allowed(operation: &Op) -> bool { + !["PaymentSession", "PaymentApprove", "PaymentReject"] + .contains(&format!("{operation:?}").as_str()) +} + +impl From for PaymentDetails { + fn from(payment_data: PaymentToFrmData) -> Self { + Self { + amount: payment_data.amount.into(), + currency: payment_data.payment_attempt.currency, + payment_method: payment_data.payment_attempt.payment_method, + payment_method_type: payment_data.payment_attempt.payment_method_type, + refund_transaction_id: None, + } + } +} + +#[instrument(skip_all)] +pub async fn frm_fulfillment_core( + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: frm_core_types::FrmFulfillmentRequest, +) -> RouterResponse { + let db = &*state.clone().store; + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id( + &req.payment_id.clone(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + match payment_intent.status { + IntentStatus::Succeeded => { + let invalid_request_error = errors::ApiErrorResponse::InvalidRequestData { + message: "no fraud check entry found for this payment_id".to_string(), + }; + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + req.payment_id.clone(), + merchant_account.merchant_id.clone(), + ) + .await + .change_context(invalid_request_error.to_owned())?; + match existing_fraud_check { + Some(fraud_check) => { + if (matches!(fraud_check.frm_transaction_type, FraudCheckType::PreFrm) + && fraud_check.last_step == FraudCheckLastStep::TransactionOrRecordRefund) + || (matches!(fraud_check.frm_transaction_type, FraudCheckType::PostFrm) + && fraud_check.last_step == FraudCheckLastStep::CheckoutOrSale) + { + Box::pin(make_fulfillment_api_call( + db, + fraud_check, + payment_intent, + state, + merchant_account, + key_store, + req, + )) + .await + } else { + Err(errors::ApiErrorResponse::PreconditionFailed {message:"Frm pre/post flow hasn't terminated yet, so fulfillment cannot be called".to_string(),}.into()) + } + } + None => Err(invalid_request_error.into()), + } + } + _ => Err(errors::ApiErrorResponse::PreconditionFailed { + message: "Fulfillment can be performed only for succeeded payment".to_string(), + } + .into()), + } +} + +#[instrument(skip_all)] +pub async fn make_fulfillment_api_call( + db: &dyn StorageInterface, + fraud_check: FraudCheck, + payment_intent: PaymentIntent, + state: AppState, + merchant_account: domain::MerchantAccount, + key_store: domain::MerchantKeyStore, + req: frm_core_types::FrmFulfillmentRequest, +) -> RouterResponse { + let payment_attempt = db + .find_payment_attempt_by_attempt_id_merchant_id( + &payment_intent.active_attempt.get_id(), + &merchant_account.merchant_id, + merchant_account.storage_scheme, + ) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + let connector_data = FraudCheckConnectorData::get_connector_by_name(&fraud_check.frm_name)?; + let connector_integration: services::BoxedConnectorIntegration< + '_, + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > = connector_data.connector.get_connector_integration(); + let modified_request_for_api_call = FrmFullfillmentSignifydApiRequest::from(req); + let router_data = frm_flows::fulfillment_flow::construct_fulfillment_router_data( + &state, + &payment_intent, + &payment_attempt, + &merchant_account, + &key_store, + "signifyd".to_string(), + modified_request_for_api_call, + ) + .await?; + let response = services::execute_connector_processing_step( + &state, + connector_integration, + &router_data, + payments::CallConnectorAction::Trigger, + None, + ) + .await + .to_payment_failed_response()?; + let fraud_check_copy = fraud_check.clone(); + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: fraud_check.frm_status, + frm_transaction_id: fraud_check.frm_transaction_id, + frm_reason: fraud_check.frm_reason, + frm_score: fraud_check.frm_score, + metadata: fraud_check.metadata, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Fulfillment, + }; + let _updated = db + .update_fraud_check_response_with_attempt_id(fraud_check_copy, fraud_check_update) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?; + let fulfillment_response = + response + .response + .map_err(|err| errors::ApiErrorResponse::ExternalConnectorError { + code: err.code, + message: err.message, + connector: connector_data.connector_name.clone().to_string(), + status_code: err.status_code, + reason: err.reason, + })?; + Ok(services::ApplicationResponse::Json(fulfillment_response)) +} diff --git a/crates/router/src/core/fraud_check/flows.rs b/crates/router/src/core/fraud_check/flows.rs new file mode 100644 index 000000000000..3d4916a372be --- /dev/null +++ b/crates/router/src/core/fraud_check/flows.rs @@ -0,0 +1,36 @@ +pub mod checkout_flow; +pub mod fulfillment_flow; +pub mod record_return; +pub mod sale_flow; +pub mod transaction_flow; + +use async_trait::async_trait; + +use crate::{ + core::{ + errors::RouterResult, + payments::{self, flows::ConstructFlowSpecificData}, + }, + routes::AppState, + services, + types::{ + api::{Connector, FraudCheckConnectorData}, + domain, + fraud_check::FraudCheckResponseData, + }, +}; + +#[async_trait] +pub trait FeatureFrm { + async fn decide_frm_flows<'a>( + self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult + where + Self: Sized, + F: Clone, + dyn Connector: services::ConnectorIntegration; +} diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs new file mode 100644 index 000000000000..47a29d657484 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -0,0 +1,147 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use super::{ConstructFlowSpecificData, FeatureFrm}; +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::types::FrmData, + payments::{self, helpers}, + }, + errors, services, + types::{ + api::fraud_check::{self as frm_api, FraudCheckConnectorData}, + domain, + fraud_check::{FraudCheckCheckoutData, FraudCheckResponseData, FrmCheckoutRouterData}, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> + { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckCheckoutData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + }, // self.order_details + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmCheckoutRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmCheckoutRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Checkout, + FraudCheckCheckoutData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs new file mode 100644 index 000000000000..6865a9510819 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/fulfillment_flow.rs @@ -0,0 +1,110 @@ +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; +use router_env::tracing::{self, instrument}; + +use crate::{ + connector::signifyd::transformers::FrmFullfillmentSignifydApiRequest, + core::{ + errors::RouterResult, + payments::{helpers, PaymentAddress}, + utils as core_utils, + }, + errors, + types::{ + domain, + fraud_check::{FraudCheckFulfillmentData, FrmFulfillmentRouterData}, + storage, ConnectorAuthType, ErrorResponse, RouterData, + }, + utils, AppState, +}; + +#[instrument(skip_all)] +pub async fn construct_fulfillment_router_data<'a>( + state: &'a AppState, + payment_intent: &'a storage::PaymentIntent, + payment_attempt: &storage::PaymentAttempt, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + connector: String, + fulfillment_request: FrmFullfillmentSignifydApiRequest, +) -> RouterResult { + let profile_id = core_utils::get_profile_id_from_business_details( + payment_intent.business_country, + payment_intent.business_label.as_ref(), + merchant_account, + payment_intent.profile_id.as_ref(), + &*state.store, + false, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("profile_id is not set in payment_intent")?; + + let merchant_connector_account = helpers::get_merchant_connector_account( + state, + merchant_account.merchant_id.as_str(), + None, + key_store, + &profile_id, + &connector, + None, + ) + .await?; + + let test_mode: Option = merchant_connector_account.is_test_mode_on(); + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::InternalServerError)?; + let payment_method = utils::OptionExt::get_required_value( + payment_attempt.payment_method, + "payment_method_type", + )?; + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + connector, + payment_id: payment_attempt.payment_id.clone(), + attempt_id: payment_attempt.attempt_id.clone(), + status: payment_attempt.status, + payment_method, + connector_auth_type: auth_type, + description: None, + return_url: payment_intent.return_url.clone(), + payment_method_id: payment_attempt.payment_method_id.clone(), + address: PaymentAddress::default(), + auth_type: payment_attempt.authentication_type.unwrap_or_default(), + connector_meta_data: merchant_connector_account.get_metadata(), + amount_captured: payment_intent.amount_captured, + request: FraudCheckFulfillmentData { + amount: payment_attempt.amount, + order_details: payment_intent.order_details.clone(), + fulfillment_request, + }, + response: Err(ErrorResponse::default()), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + customer_id: None, + recurring_mandate_payment_data: None, + preprocessing_id: None, + payment_method_balance: None, + connector_request_reference_id: core_utils::get_connector_request_reference_id( + &state.conf, + &merchant_account.merchant_id, + payment_attempt, + ), + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + test_mode, + connector_api_version: None, + connector_http_status_code: None, + external_latency: None, + apple_pay_flow: None, + }; + Ok(router_data) +} diff --git a/crates/router/src/core/fraud_check/flows/record_return.rs b/crates/router/src/core/fraud_check/flows/record_return.rs new file mode 100644 index 000000000000..eaefdbefcc77 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -0,0 +1,149 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + connector::signifyd::transformers::RefundMethod, + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FraudCheckConnectorData, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::RecordReturn, + domain, + fraud_check::{ + FraudCheckRecordReturnData, FraudCheckResponseData, FrmRecordReturnRouterData, + }, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + utils, AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> + { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + let currency = self.payment_attempt.clone().currency; + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: utils::OptionExt::get_required_value( + self.payment_attempt.payment_method, + "payment_method_type", + )?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckRecordReturnData { + amount: self.payment_attempt.amount, + refund_method: RefundMethod::OriginalPaymentInstrument, //we dont consume this data now in payments...hence hardcoded + currency, + refund_transaction_id: self.refund.clone().map(|refund| refund.refund_id), + }, // self.order_details + response: Ok(FraudCheckResponseData::RecordReturnResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + return_id: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmRecordReturnRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmRecordReturnRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + RecordReturn, + FraudCheckRecordReturnData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/sale_flow.rs b/crates/router/src/core/fraud_check/flows/sale_flow.rs new file mode 100644 index 000000000000..c62b096ab374 --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/sale_flow.rs @@ -0,0 +1,145 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FraudCheckConnectorData, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{FraudCheckResponseData, FraudCheckSaleData, FrmSaleRouterData}, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult> { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckSaleData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + }, + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmSaleRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmSaleRouterData, + state: &'a AppState, + connector: &FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Sale, + FraudCheckSaleData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/flows/transaction_flow.rs b/crates/router/src/core/fraud_check/flows/transaction_flow.rs new file mode 100644 index 000000000000..1c2b8995dfab --- /dev/null +++ b/crates/router/src/core/fraud_check/flows/transaction_flow.rs @@ -0,0 +1,158 @@ +use async_trait::async_trait; +use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; + +use crate::{ + core::{ + errors::{ConnectorErrorExt, RouterResult}, + fraud_check::{FeatureFrm, FrmData}, + payments::{self, flows::ConstructFlowSpecificData, helpers}, + }, + errors, services, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{ + FraudCheckResponseData, FraudCheckTransactionData, FrmTransactionRouterData, + }, + storage::enums as storage_enums, + ConnectorAuthType, ResponseId, RouterData, + }, + AppState, +}; + +#[async_trait] +impl + ConstructFlowSpecificData< + frm_api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, + > for FrmData +{ + async fn construct_router_data<'a>( + &self, + _state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + _key_store: &domain::MerchantKeyStore, + customer: &Option, + merchant_connector_account: &helpers::MerchantConnectorAccountType, + ) -> RouterResult< + RouterData, + > { + let status = storage_enums::AttemptStatus::Pending; + + let auth_type: ConnectorAuthType = merchant_connector_account + .get_connector_account_details() + .parse_value("ConnectorAuthType") + .change_context(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { + id: "ConnectorAuthType".to_string(), + })?; + + let customer_id = customer.to_owned().map(|customer| customer.customer_id); + + let payment_method = self.payment_attempt.payment_method; + let currency = self.payment_attempt.currency; + + let router_data = RouterData { + flow: std::marker::PhantomData, + merchant_id: merchant_account.merchant_id.clone(), + customer_id, + connector: connector_id.to_string(), + payment_id: self.payment_intent.payment_id.clone(), + attempt_id: self.payment_attempt.attempt_id.clone(), + status, + payment_method: self + .payment_attempt + .payment_method + .ok_or(errors::ApiErrorResponse::PaymentMethodNotFound)?, + connector_auth_type: auth_type, + description: None, + return_url: None, + payment_method_id: None, + address: self.address.clone(), + auth_type: storage_enums::AuthenticationType::NoThreeDs, + connector_meta_data: None, + amount_captured: None, + request: FraudCheckTransactionData { + amount: self.payment_attempt.amount, + order_details: self.order_details.clone(), + currency, + payment_method, + }, // self.order_details + response: Ok(FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId("".to_string()), + connector_metadata: None, + status: storage_enums::FraudCheckStatus::Pending, + score: None, + reason: None, + }), + access_token: None, + session_token: None, + reference_id: None, + payment_method_token: None, + connector_customer: None, + preprocessing_id: None, + connector_request_reference_id: uuid::Uuid::new_v4().to_string(), + test_mode: None, + recurring_mandate_payment_data: None, + #[cfg(feature = "payouts")] + payout_method_data: None, + #[cfg(feature = "payouts")] + quote_id: None, + payment_method_balance: None, + connector_http_status_code: None, + external_latency: None, + connector_api_version: None, + apple_pay_flow: None, + }; + + Ok(router_data) + } +} + +#[async_trait] +impl FeatureFrm for FrmTransactionRouterData { + async fn decide_frm_flows<'a>( + mut self, + state: &AppState, + connector: &frm_api::FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + decide_frm_flow( + &mut self, + state, + connector, + call_connector_action, + merchant_account, + ) + .await + } +} + +pub async fn decide_frm_flow<'a, 'b>( + router_data: &'b mut FrmTransactionRouterData, + state: &'a AppState, + connector: &frm_api::FraudCheckConnectorData, + call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, +) -> RouterResult { + let connector_integration: services::BoxedConnectorIntegration< + '_, + frm_api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, + > = connector.connector.get_connector_integration(); + let resp = services::execute_connector_processing_step( + state, + connector_integration, + router_data, + call_connector_action, + None, + ) + .await + .to_payment_failed_response()?; + + Ok(resp) +} diff --git a/crates/router/src/core/fraud_check/operation.rs b/crates/router/src/core/fraud_check/operation.rs new file mode 100644 index 000000000000..e7677dad6f3a --- /dev/null +++ b/crates/router/src/core/fraud_check/operation.rs @@ -0,0 +1,106 @@ +pub mod fraud_check_post; +pub mod fraud_check_pre; +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use error_stack::{report, ResultExt}; + +pub use self::{fraud_check_post::FraudCheckPost, fraud_check_pre::FraudCheckPre}; +use super::{ + types::{ConnectorDetailsCore, FrmConfigsObject, PaymentToFrmData}, + FrmData, +}; +use crate::{ + core::{ + errors::{self, RouterResult}, + payments, + }, + db::StorageInterface, + routes::AppState, + types::{domain, fraud_check::FrmRouterData}, +}; + +pub type BoxedFraudCheckOperation = Box + Send + Sync>; + +pub trait FraudCheckOperation: Send + std::fmt::Debug { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("get tracker interface not found for {self:?}")) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("domain interface not found for {self:?}")) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Err(report!(errors::ApiErrorResponse::InternalServerError)) + .attach_printable_lazy(|| format!("get tracker interface not found for {self:?}")) + } +} + +#[async_trait] +pub trait GetTracker: Send { + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: D, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult>; +} + +#[async_trait] +pub trait Domain: Send + Sync { + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> + where + F: Send + Clone; + + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult + where + F: Send + Clone; + + // To execute several tasks conditionally based on the result of post_flow. + // Eg: If the /sale(post flow) is returning the transaction as fraud we can execute refund in post task + #[allow(clippy::too_many_arguments)] + async fn execute_post_tasks( + &self, + _state: &AppState, + frm_data: &mut FrmData, + _merchant_account: &domain::MerchantAccount, + _frm_configs: FrmConfigsObject, + _frm_suggestion: &mut Option, + _key_store: domain::MerchantKeyStore, + _payment_data: &mut payments::PaymentData, + _customer: &Option, + ) -> RouterResult> + where + F: Send + Clone, + { + return Ok(Some(frm_data.to_owned())); + } +} + +#[async_trait] +pub trait UpdateTracker: Send { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + frm_data: D, + payment_data: &mut payments::PaymentData, + _frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult; +} diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_post.rs b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs new file mode 100644 index 000000000000..37838ddaab5a --- /dev/null +++ b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs @@ -0,0 +1,457 @@ +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use common_utils::ext_traits::Encode; +use data_models::payments::{ + payment_attempt::PaymentAttemptUpdate, payment_intent::PaymentIntentUpdate, +}; +use router_env::{instrument, logger, tracing}; + +use super::{Domain, FraudCheckOperation, GetTracker, UpdateTracker}; +use crate::{ + consts, + core::{ + errors::{RouterResult, StorageErrorExt}, + fraud_check::{ + self as frm_core, + types::{FrmData, PaymentDetails, PaymentToFrmData, REFUND_INITIATED}, + ConnectorDetailsCore, FrmConfigsObject, + }, + payments, refunds, + }, + db::StorageInterface, + errors, services, + types::{ + api::{ + enums::{AttemptStatus, FrmAction, IntentStatus}, + fraud_check as frm_api, + refunds::{RefundRequest, RefundType}, + }, + domain, + fraud_check::{ + FraudCheckResponseData, FraudCheckSaleData, FrmRequest, FrmResponse, FrmRouterData, + }, + storage::{ + enums::{FraudCheckLastStep, FraudCheckStatus, FraudCheckType, MerchantDecision}, + fraud_check::{FraudCheckNew, FraudCheckUpdate}, + }, + ResponseId, + }, + utils, AppState, +}; + +#[derive(Debug, Clone, Copy)] +pub struct FraudCheckPost; + +impl FraudCheckOperation for &FraudCheckPost { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(*self) + } +} + +impl FraudCheckOperation for FraudCheckPost { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(self) + } +} + +#[async_trait] +impl GetTracker for FraudCheckPost { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: PaymentToFrmData, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult> { + let db = &*state.store; + + let payment_details: Option = + Encode::::encode_to_value(&PaymentDetails::from(payment_data.clone())) + .ok(); + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + payment_data.payment_intent.payment_id.clone(), + payment_data.merchant_account.merchant_id.clone(), + ) + .await + .ok(); + let fraud_check = match existing_fraud_check { + Some(Some(fraud_check)) => Ok(fraud_check), + _ => { + db.insert_fraud_check_response(FraudCheckNew { + frm_id: utils::generate_id(consts::ID_LENGTH, "frm"), + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: payment_data.merchant_account.merchant_id.clone(), + attempt_id: payment_data.payment_attempt.attempt_id.clone(), + created_at: common_utils::date_time::now(), + frm_name: frm_connector_details.connector_name, + frm_transaction_id: None, + frm_transaction_type: FraudCheckType::PostFrm, + frm_status: FraudCheckStatus::Pending, + frm_score: None, + frm_reason: None, + frm_error: None, + payment_details, + metadata: None, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Processing, + }) + .await + } + }; + match fraud_check { + Ok(fraud_check_value) => { + let frm_data = FrmData { + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: payment_data.merchant_account, + address: payment_data.address, + fraud_check: fraud_check_value, + connector_details: payment_data.connector_details, + order_details: payment_data.order_details, + refund: None, + }; + Ok(Some(frm_data)) + } + Err(error) => { + router_env::logger::error!("inserting into fraud_check table failed {error:?}"); + Ok(None) + } + } + } +} + +#[async_trait] +impl Domain for FraudCheckPost { + #[instrument(skip_all)] + async fn post_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> { + if frm_data.fraud_check.last_step != FraudCheckLastStep::Processing { + logger::debug!("post_flow::Sale Skipped"); + return Ok(None); + } + let router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::CheckoutOrSale; + Ok(Some(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Sale(FraudCheckSaleData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Sale(router_data.response), + })) + } + + #[instrument(skip_all)] + async fn execute_post_tasks( + &self, + state: &AppState, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + frm_configs: FrmConfigsObject, + frm_suggestion: &mut Option, + key_store: domain::MerchantKeyStore, + payment_data: &mut payments::PaymentData, + customer: &Option, + ) -> RouterResult> { + if matches!(frm_data.fraud_check.frm_status, FraudCheckStatus::Fraud) + && matches!(frm_configs.frm_action, FrmAction::AutoRefund) + && matches!( + frm_data.fraud_check.last_step, + FraudCheckLastStep::CheckoutOrSale + ) + { + *frm_suggestion = Some(FrmSuggestion::FrmAutoRefund); + let ref_req = RefundRequest { + refund_id: None, + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: Some(merchant_account.merchant_id.clone()), + amount: None, + reason: frm_data + .fraud_check + .frm_reason + .clone() + .map(|data| data.to_string()), + refund_type: Some(RefundType::Instant), + metadata: None, + merchant_connector_details: None, + }; + let refund = Box::pin(refunds::refund_create_core( + state.clone(), + merchant_account.clone(), + key_store.clone(), + ref_req, + )) + .await?; + if let services::ApplicationResponse::Json(new_refund) = refund { + frm_data.refund = Some(new_refund); + } + let _router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund; + }; + return Ok(Some(frm_data.to_owned())); + } + + #[instrument(skip_all)] + async fn pre_payment_frm<'a>( + &'a self, + state: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let router_data = frm_core::call_frm_service::( + state, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + Ok(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Sale(FraudCheckSaleData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Sale(router_data.response), + }) + } +} + +#[async_trait] +impl UpdateTracker for FraudCheckPost { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + mut frm_data: FrmData, + payment_data: &mut payments::PaymentData, + frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult { + let frm_check_update = match frm_router_data.response { + FrmResponse::Sale(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + }, + FraudCheckResponseData::RecordReturnResponse { resource_id: _, connector_metadata: _, return_id: _ } => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Sale flow".to_string(), + )), + }) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + }, + }, + FrmResponse::Fulfillment(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { resource_id: _, connector_metadata: _, return_id: _ } => None, + + }, + }, + + FrmResponse::RecordReturn(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id: _, + connector_metadata: _, + status: _, + reason: _, + score: _, + } => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Transaction Response response in current Record Return flow".to_string(), + )), + }) + }, + FraudCheckResponseData::FulfillmentResponse {order_id: _, shipment_ids: _ } => { + None + }, + FraudCheckResponseData::RecordReturnResponse { resource_id, connector_metadata, return_id: _ } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: frm_data.fraud_check.frm_status, + frm_transaction_id: connector_transaction_id, + frm_reason: frm_data.fraud_check.frm_reason.clone(), + frm_score: frm_data.fraud_check.frm_score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + + } + }, + }, + + + FrmResponse::Checkout(_) | FrmResponse::Transaction(_) => { + Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Pre(Sale) flow response in current post flow".to_string(), + )), + }) + } + }; + + if frm_suggestion == Some(FrmSuggestion::FrmAutoRefund) { + payment_data.payment_attempt = db + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + PaymentAttemptUpdate::RejectUpdate { + status: AttemptStatus::Failure, + error_code: Some(Some(frm_data.fraud_check.frm_status.to_string())), + error_message: Some(Some(REFUND_INITIATED.to_string())), + updated_by: frm_data.merchant_account.storage_scheme.to_string(), // merchant_decision: Some(MerchantDecision::AutoRefunded), + }, + frm_data.merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_data.payment_intent = db + .update_payment_intent( + payment_data.payment_intent.clone(), + PaymentIntentUpdate::RejectUpdate { + status: IntentStatus::Failed, + merchant_decision: Some(MerchantDecision::AutoRefunded.to_string()), + updated_by: frm_data.merchant_account.storage_scheme.to_string(), + }, + frm_data.merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + } + frm_data.fraud_check = match frm_check_update { + Some(fraud_check_update) => db + .update_fraud_check_response_with_attempt_id( + frm_data.fraud_check.clone(), + fraud_check_update, + ) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?, + None => frm_data.fraud_check.clone(), + }; + + Ok(frm_data) + } +} diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs new file mode 100644 index 000000000000..8cc486e17140 --- /dev/null +++ b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs @@ -0,0 +1,337 @@ +use async_trait::async_trait; +use common_enums::FrmSuggestion; +use common_utils::ext_traits::Encode; +use diesel_models::enums::FraudCheckLastStep; +use router_env::{instrument, tracing}; +use uuid::Uuid; + +use super::{Domain, FraudCheckOperation, GetTracker, UpdateTracker}; +use crate::{ + core::{ + errors::RouterResult, + fraud_check::{ + self as frm_core, + types::{FrmData, PaymentDetails, PaymentToFrmData}, + ConnectorDetailsCore, + }, + payments, + }, + db::StorageInterface, + errors, + types::{ + api::fraud_check as frm_api, + domain, + fraud_check::{ + FraudCheckCheckoutData, FraudCheckResponseData, FraudCheckTransactionData, FrmRequest, + FrmResponse, FrmRouterData, + }, + storage::{ + enums::{FraudCheckStatus, FraudCheckType}, + fraud_check::{FraudCheckNew, FraudCheckUpdate}, + }, + ResponseId, + }, + AppState, +}; + +#[derive(Debug, Clone, Copy)] +pub struct FraudCheckPre; + +impl FraudCheckOperation for &FraudCheckPre { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(*self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(*self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(*self) + } +} + +impl FraudCheckOperation for FraudCheckPre { + fn to_get_tracker(&self) -> RouterResult<&(dyn GetTracker + Send + Sync)> { + Ok(self) + } + fn to_domain(&self) -> RouterResult<&(dyn Domain)> { + Ok(self) + } + fn to_update_tracker(&self) -> RouterResult<&(dyn UpdateTracker + Send + Sync)> { + Ok(self) + } +} + +#[async_trait] +impl GetTracker for FraudCheckPre { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_data: PaymentToFrmData, + frm_connector_details: ConnectorDetailsCore, + ) -> RouterResult> { + let db = &*state.store; + + let payment_details: Option = + Encode::::encode_to_value(&PaymentDetails::from(payment_data.clone())) + .ok(); + + let existing_fraud_check = db + .find_fraud_check_by_payment_id_if_present( + payment_data.payment_intent.payment_id.clone(), + payment_data.merchant_account.merchant_id.clone(), + ) + .await + .ok(); + + let fraud_check = match existing_fraud_check { + Some(Some(fraud_check)) => Ok(fraud_check), + _ => { + db.insert_fraud_check_response(FraudCheckNew { + frm_id: Uuid::new_v4().simple().to_string(), + payment_id: payment_data.payment_intent.payment_id.clone(), + merchant_id: payment_data.merchant_account.merchant_id.clone(), + attempt_id: payment_data.payment_attempt.attempt_id.clone(), + created_at: common_utils::date_time::now(), + frm_name: frm_connector_details.connector_name, + frm_transaction_id: None, + frm_transaction_type: FraudCheckType::PreFrm, + frm_status: FraudCheckStatus::Pending, + frm_score: None, + frm_reason: None, + frm_error: None, + payment_details, + metadata: None, + modified_at: common_utils::date_time::now(), + last_step: FraudCheckLastStep::Processing, + }) + .await + } + }; + + match fraud_check { + Ok(fraud_check_value) => { + let frm_data = FrmData { + payment_intent: payment_data.payment_intent, + payment_attempt: payment_data.payment_attempt, + merchant_account: payment_data.merchant_account, + address: payment_data.address, + fraud_check: fraud_check_value, + connector_details: payment_data.connector_details, + order_details: payment_data.order_details, + refund: None, + }; + Ok(Some(frm_data)) + } + Err(error) => { + router_env::logger::error!("inserting into fraud_check table failed {error:?}"); + Ok(None) + } + } + } +} + +#[async_trait] +impl Domain for FraudCheckPre { + #[instrument(skip_all)] + async fn post_payment_frm<'a>( + &'a self, + state_vas: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult> { + let router_data = frm_core::call_frm_service::( + state_vas, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund; + Ok(Some(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Transaction(FraudCheckTransactionData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + currency: router_data.request.currency, + payment_method: Some(router_data.payment_method), + }), + response: FrmResponse::Transaction(router_data.response), + })) + } + + async fn pre_payment_frm<'a>( + &'a self, + state_vas: &'a AppState, + payment_data: &mut payments::PaymentData, + frm_data: &mut FrmData, + merchant_account: &domain::MerchantAccount, + customer: &Option, + key_store: domain::MerchantKeyStore, + ) -> RouterResult { + let router_data = frm_core::call_frm_service::( + state_vas, + payment_data, + frm_data.to_owned(), + merchant_account, + &key_store, + customer, + ) + .await?; + frm_data.fraud_check.last_step = FraudCheckLastStep::CheckoutOrSale; + Ok(FrmRouterData { + merchant_id: router_data.merchant_id, + connector: router_data.connector, + payment_id: router_data.payment_id, + attempt_id: router_data.attempt_id, + request: FrmRequest::Checkout(FraudCheckCheckoutData { + amount: router_data.request.amount, + order_details: router_data.request.order_details, + }), + response: FrmResponse::Checkout(router_data.response), + }) + } +} + +#[async_trait] +impl UpdateTracker for FraudCheckPre { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + mut frm_data: FrmData, + payment_data: &mut payments::PaymentData, + _frm_suggestion: Option, + frm_router_data: FrmRouterData, + ) -> RouterResult { + let frm_check_update = match frm_router_data.response { + FrmResponse::Checkout(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status: status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { + resource_id: _, + connector_metadata: _, + return_id: _, + } => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Checkout flow" + .to_string(), + )), + }), + }, + }, + FrmResponse::Transaction(response) => match response { + Err(err) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some(err.message)), + }), + Ok(payments_response) => match payments_response { + FraudCheckResponseData::TransactionResponse { + resource_id, + connector_metadata, + status, + reason, + score, + } => { + let connector_transaction_id = match resource_id { + ResponseId::NoResponseId => None, + ResponseId::ConnectorTransactionId(id) => Some(id), + ResponseId::EncodedData(id) => Some(id), + }; + + let frm_status = payment_data + .frm_message + .as_ref() + .map_or(status, |frm_data| frm_data.frm_status); + + let fraud_check_update = FraudCheckUpdate::ResponseUpdate { + frm_status, + frm_transaction_id: connector_transaction_id, + frm_reason: reason, + frm_score: score, + metadata: connector_metadata, + modified_at: common_utils::date_time::now(), + last_step: frm_data.fraud_check.last_step, + }; + Some(fraud_check_update) + } + FraudCheckResponseData::FulfillmentResponse { + order_id: _, + shipment_ids: _, + } => None, + FraudCheckResponseData::RecordReturnResponse { + resource_id: _, + connector_metadata: _, + return_id: _, + } => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Record Return Response response in current Checkout flow" + .to_string(), + )), + }), + }, + }, + FrmResponse::Sale(_response) + | FrmResponse::Fulfillment(_response) + | FrmResponse::RecordReturn(_response) => Some(FraudCheckUpdate::ErrorUpdate { + status: FraudCheckStatus::TransactionFailure, + error_message: Some(Some( + "Error: Got Pre(Sale) flow response in current post flow".to_string(), + )), + }), + }; + + frm_data.fraud_check = match frm_check_update { + Some(fraud_check_update) => db + .update_fraud_check_response_with_attempt_id( + frm_data.clone().fraud_check, + fraud_check_update, + ) + .await + .map_err(|error| error.change_context(errors::ApiErrorResponse::PaymentNotFound))?, + None => frm_data.clone().fraud_check, + }; + + Ok(frm_data) + } +} diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs new file mode 100644 index 000000000000..1d6e7cb45a58 --- /dev/null +++ b/crates/router/src/core/fraud_check/types.rs @@ -0,0 +1,208 @@ +use api_models::{ + enums as api_enums, + enums::{PaymentMethod, PaymentMethodType}, + payments::Amount, + refunds::RefundResponse, +}; +use common_enums::FrmSuggestion; +use common_utils::pii::Email; +use data_models::payments::{payment_attempt::PaymentAttempt, PaymentIntent}; +use masking::Serialize; +use serde::Deserialize; +use utoipa::ToSchema; + +use super::operation::BoxedFraudCheckOperation; +use crate::{ + pii::Secret, + types::{ + domain::MerchantAccount, + storage::{enums as storage_enums, fraud_check::FraudCheck}, + PaymentAddress, + }, +}; + +#[derive(Clone, Default, Debug)] +pub struct PaymentIntentCore { + pub payment_id: String, +} + +#[derive(Clone, Debug)] +pub struct PaymentAttemptCore { + pub attempt_id: String, + pub payment_details: Option, + pub amount: Amount, +} + +#[derive(Clone, Debug, Serialize)] +pub struct PaymentDetails { + pub amount: i64, + pub currency: Option, + pub payment_method: Option, + pub payment_method_type: Option, + pub refund_transaction_id: Option, +} +#[derive(Clone, Default, Debug)] +pub struct FrmMerchantAccount { + pub merchant_id: String, +} + +#[derive(Clone, Debug)] +pub struct FrmData { + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, + pub merchant_account: MerchantAccount, + pub fraud_check: FraudCheck, + pub address: PaymentAddress, + pub connector_details: ConnectorDetailsCore, + pub order_details: Option>, + pub refund: Option, +} + +#[derive(Debug)] +pub struct FrmInfo { + pub fraud_check_operation: BoxedFraudCheckOperation, + pub frm_data: Option, + pub suggested_action: Option, +} + +#[derive(Clone, Debug)] +pub struct ConnectorDetailsCore { + pub connector_name: String, + pub profile_id: String, +} +#[derive(Clone)] +pub struct PaymentToFrmData { + pub amount: Amount, + pub payment_intent: PaymentIntent, + pub payment_attempt: PaymentAttempt, + pub merchant_account: MerchantAccount, + pub address: PaymentAddress, + pub connector_details: ConnectorDetailsCore, + pub order_details: Option>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FrmConfigsObject { + pub frm_enabled_pm: Option, + pub frm_enabled_pm_type: Option, + pub frm_enabled_gateway: Option, + pub frm_action: api_enums::FrmAction, + pub frm_preferred_flow_type: api_enums::FrmPreferredFlowTypes, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFulfillmentSignifydApiRequest { + ///unique order_id for the order_details in the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///denotes the status of the fulfillment... can be one of PARTIAL, COMPLETE, REPLACEMENT, CANCELED + #[schema(value_type = Option, example = "COMPLETE")] + pub fulfillment_status: Option, + ///contains details of the fulfillment + #[schema(value_type = Vec)] + pub fulfillments: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct FrmFulfillmentRequest { + ///unique payment_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub payment_id: String, + ///unique order_id for the order_details in the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///denotes the status of the fulfillment... can be one of PARTIAL, COMPLETE, REPLACEMENT, CANCELED + #[schema(value_type = Option, example = "COMPLETE")] + pub fulfillment_status: Option, + ///contains details of the fulfillment + #[schema(value_type = Vec)] + pub fulfillments: Vec, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Fulfillments { + ///shipment_id of the shipped items + #[schema(max_length = 255, example = "ship_101")] + pub shipment_id: String, + ///products sent in the shipment + #[schema(value_type = Option>)] + pub products: Option>, + ///destination address of the shipment + #[schema(value_type = Destination)] + pub destination: Destination, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub enum FulfillmentStatus { + PARTIAL, + COMPLETE, + REPLACEMENT, + CANCELED, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Product { + pub item_name: String, + pub item_quantity: i64, + pub item_id: String, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Destination { + pub full_name: Secret, + pub organization: Option, + pub email: Option, + pub address: Address, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "snake_case")] +pub struct Address { + pub street_address: Secret, + pub unit: Option>, + pub postal_code: Secret, + pub city: String, + pub province_code: Secret, + pub country_code: common_enums::CountryAlpha2, +} + +#[derive(Debug, ToSchema, Clone, Serialize)] +pub struct FrmFulfillmentResponse { + ///unique order_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///shipment_ids used in the fulfillment overall...also data from previous fulfillments for the same transactions/order is sent + #[schema(example = r#"["ship_101", "ship_102"]"#)] + pub shipment_ids: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFulfillmentSignifydApiResponse { + ///unique order_id for the transaction + #[schema(max_length = 255, example = "pay_qiYfHcDou1ycIaxVXKHF")] + pub order_id: String, + ///shipment_ids used in the fulfillment overall...also data from previous fulfillments for the same transactions/order is sent + #[schema(example = r#"["ship_101","ship_102"]"#)] + pub shipment_ids: Vec, +} + +pub const REFUND_INITIATED: &str = "Refund Initiated with the processor"; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 1c40ef81f497..abb83f288ab7 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -46,6 +46,8 @@ use self::{ use super::{ errors::StorageErrorExt, payment_methods::surcharge_decision_configs, utils as core_utils, }; +#[cfg(feature = "frm")] +use crate::core::fraud_check as frm_core; use crate::{ configs::settings::PaymentMethodTypeTokenFilter, core::{ @@ -175,158 +177,231 @@ where let mut connector_http_status_code = None; let mut external_latency = None; if let Some(connector_details) = connector { - operation - .to_domain()? - .populate_payment_data(state, &mut payment_data, &req, &merchant_account) - .await?; - payment_data = match connector_details { - api::ConnectorCallType::PreDetermined(connector) => { - let schedule_time = if should_add_task_to_process_tracker { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector.connector.id(), - &merchant_account.merchant_id, - 0, - ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - }; - let router_data = call_connector_service( - state, - &merchant_account, - &key_store, - connector, - &operation, - &mut payment_data, - &customer, - call_connector_action, - &validate_result, - schedule_time, - header_payload, - ) + // Fetch and check FRM configs + #[cfg(feature = "frm")] + let mut frm_info = None; + #[cfg(feature = "frm")] + let db = &*state.store; + #[allow(unused_variables, unused_mut)] + let mut should_continue_transaction: bool = true; + #[cfg(feature = "frm")] + let frm_configs = if state.conf.frm.is_frm_enabled { + frm_core::call_frm_before_connector_call( + db, + &operation, + &merchant_account, + &mut payment_data, + state, + &mut frm_info, + &customer, + &mut should_continue_transaction, + key_store.clone(), + ) + .await? + } else { + None + }; + #[cfg(feature = "frm")] + logger::debug!( + "should_cancel_transaction: {:?} {:?} ", + frm_configs, + should_continue_transaction + ); + + if should_continue_transaction { + operation + .to_domain()? + .populate_payment_data(state, &mut payment_data, &req, &merchant_account) .await?; - let operation = Box::new(PaymentResponse); - - connector_http_status_code = router_data.connector_http_status_code; - external_latency = router_data.external_latency; - //add connector http status code metrics - add_connector_http_status_code_metrics(connector_http_status_code); - operation - .to_post_update_tracker()? - .update_tracker( + payment_data = match connector_details { + api::ConnectorCallType::PreDetermined(connector) => { + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( state, - &validate_result.payment_id, - payment_data, - router_data, - merchant_account.storage_scheme, + &merchant_account, + &key_store, + connector, + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, ) - .await? - } + .await?; + let operation = Box::new(PaymentResponse); + + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( + state, + &validate_result.payment_id, + payment_data, + router_data, + merchant_account.storage_scheme, + ) + .await? + } - api::ConnectorCallType::Retryable(connectors) => { - let mut connectors = connectors.into_iter(); + api::ConnectorCallType::Retryable(connectors) => { + let mut connectors = connectors.into_iter(); - let connector_data = get_connector_data(&mut connectors)?; + let connector_data = get_connector_data(&mut connectors)?; - let schedule_time = if should_add_task_to_process_tracker { - payment_sync::get_sync_process_schedule_time( - &*state.store, - connector_data.connector.id(), - &merchant_account.merchant_id, - 0, + let schedule_time = if should_add_task_to_process_tracker { + payment_sync::get_sync_process_schedule_time( + &*state.store, + connector_data.connector.id(), + &merchant_account.merchant_id, + 0, + ) + .await + .into_report() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed while getting process schedule time")? + } else { + None + }; + let router_data = call_connector_service( + state, + &merchant_account, + &key_store, + connector_data.clone(), + &operation, + &mut payment_data, + &customer, + call_connector_action, + &validate_result, + schedule_time, + header_payload, ) - .await - .into_report() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed while getting process schedule time")? - } else { - None - }; - let router_data = call_connector_service( - state, - &merchant_account, - &key_store, - connector_data.clone(), - &operation, - &mut payment_data, - &customer, - call_connector_action, - &validate_result, - schedule_time, - header_payload, - ) - .await?; + .await?; - #[cfg(feature = "retry")] - let mut router_data = router_data; - #[cfg(feature = "retry")] - { - use crate::core::payments::retry::{self, GsmValidation}; - let config_bool = - retry::config_should_call_gsm(&*state.store, &merchant_account.merchant_id) - .await; + #[cfg(feature = "retry")] + let mut router_data = router_data; + #[cfg(feature = "retry")] + { + use crate::core::payments::retry::{self, GsmValidation}; + let config_bool = retry::config_should_call_gsm( + &*state.store, + &merchant_account.merchant_id, + ) + .await; + + if config_bool && router_data.should_call_gsm() { + router_data = retry::do_gsm_actions( + state, + &mut payment_data, + connectors, + connector_data, + router_data, + &merchant_account, + &key_store, + &operation, + &customer, + &validate_result, + schedule_time, + ) + .await?; + }; + } - if config_bool && router_data.should_call_gsm() { - router_data = retry::do_gsm_actions( + let operation = Box::new(PaymentResponse); + connector_http_status_code = router_data.connector_http_status_code; + external_latency = router_data.external_latency; + //add connector http status code metrics + add_connector_http_status_code_metrics(connector_http_status_code); + operation + .to_post_update_tracker()? + .update_tracker( state, - &mut payment_data, - connectors, - connector_data, + &validate_result.payment_id, + payment_data, router_data, - &merchant_account, - &key_store, - &operation, - &customer, - &validate_result, - schedule_time, + merchant_account.storage_scheme, ) - .await?; - }; + .await? } - let operation = Box::new(PaymentResponse); - connector_http_status_code = router_data.connector_http_status_code; - external_latency = router_data.external_latency; - //add connector http status code metrics - add_connector_http_status_code_metrics(connector_http_status_code); - operation - .to_post_update_tracker()? - .update_tracker( + api::ConnectorCallType::SessionMultiple(connectors) => { + let session_surcharge_details = + call_surcharge_decision_management_for_session_flow( + state, + &merchant_account, + &mut payment_data, + &connectors, + ) + .await?; + call_multiple_connectors_service( state, - &validate_result.payment_id, + &merchant_account, + &key_store, + connectors, + &operation, payment_data, - router_data, - merchant_account.storage_scheme, + &customer, + session_surcharge_details, ) .await? - } + } + }; - api::ConnectorCallType::SessionMultiple(connectors) => { - let session_surcharge_details = - call_surcharge_decision_management_for_session_flow( - state, - &merchant_account, - &mut payment_data, - &connectors, - ) - .await?; - call_multiple_connectors_service( + #[cfg(feature = "frm")] + if let Some(fraud_info) = &mut frm_info { + Box::pin(frm_core::post_payment_frm_core( state, &merchant_account, - &key_store, - connectors, - &operation, - payment_data, + &mut payment_data, + fraud_info, + frm_configs + .clone() + .ok_or(errors::ApiErrorResponse::MissingRequiredField { + field_name: "frm_configs", + }) + .into_report() + .attach_printable("Frm configs label not found")?, &customer, - session_surcharge_details, - ) - .await? + key_store, + )) + .await?; } - }; + } else { + (_, payment_data) = operation + .to_update_tracker()? + .update_trackers( + state, + payment_data.clone(), + customer.clone(), + validate_result.storage_scheme, + None, + &key_store, + #[cfg(feature = "frm")] + frm_info.and_then(|info| info.suggested_action), + #[cfg(not(feature = "frm"))] + None, + header_payload, + ) + .await?; + } + payment_data .payment_attempt .payment_token diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 46eaca26f7cc..c8e9bfe925a8 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -18,7 +18,7 @@ use crate::{ }, routes::AppState, services, - types::{self, api, domain}, + types::{self, api, domain, fraud_check as frm_types}, }; #[async_trait] @@ -169,6 +169,7 @@ default_imp_for_complete_authorize!( connector::Payeezy, connector::Payu, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -246,6 +247,7 @@ default_imp_for_webhook_source_verification!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -325,6 +327,7 @@ default_imp_for_create_customer!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Trustpay, connector::Tsys, @@ -393,6 +396,7 @@ default_imp_for_connector_redirect_response!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Tsys, @@ -452,6 +456,7 @@ default_imp_for_connector_request_id!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -534,6 +539,7 @@ default_imp_for_accept_dispute!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -634,6 +640,7 @@ default_imp_for_file_upload!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Trustpay, @@ -712,6 +719,7 @@ default_imp_for_submit_evidence!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Trustpay, @@ -790,6 +798,7 @@ default_imp_for_defend_dispute!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -869,6 +878,7 @@ default_imp_for_pre_processing_steps!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Signifyd, connector::Square, connector::Stax, connector::Tsys, @@ -929,6 +939,7 @@ default_imp_for_payouts!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1008,6 +1019,7 @@ default_imp_for_payouts_create!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1090,6 +1102,7 @@ default_imp_for_payouts_eligibility!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1169,6 +1182,7 @@ default_imp_for_payouts_fulfill!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1248,6 +1262,7 @@ default_imp_for_payouts_cancel!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1328,6 +1343,7 @@ default_imp_for_payouts_quote!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1408,6 +1424,7 @@ default_imp_for_payouts_recipient!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1487,6 +1504,7 @@ default_imp_for_approve!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Signifyd, connector::Square, connector::Stax, connector::Stripe, @@ -1528,6 +1546,471 @@ impl } default_imp_for_reject!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Signifyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_fraud_check { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheck for $path::$connector {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheck for connector::DummyConnector {} + +default_imp_for_fraud_check!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_frm_sale { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckSale for $path::$connector {} + impl + services::ConnectorIntegration< + api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheckSale for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_frm_sale!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_frm_checkout { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckCheckout for $path::$connector {} + impl + services::ConnectorIntegration< + api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheckCheckout for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_frm_checkout!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_frm_transaction { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckTransaction for $path::$connector {} + impl + services::ConnectorIntegration< + api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheckTransaction for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_frm_transaction!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_frm_fulfillment { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckFulfillment for $path::$connector {} + impl + services::ConnectorIntegration< + api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheckFulfillment for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_frm_fulfillment!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bankofamerica, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Gocardless, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Prophetpay, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Volt, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_frm_record_return { + ($($path:ident::$connector:ident),*) => { + $( + impl api::FraudCheckRecordReturn for $path::$connector {} + impl + services::ConnectorIntegration< + api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::FraudCheckRecordReturn for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_frm_record_return!( connector::Aci, connector::Adyen, connector::Airwallex, diff --git a/crates/router/src/core/payments/routing/transformers.rs b/crates/router/src/core/payments/routing/transformers.rs index 5704f82f4983..8693143ae435 100644 --- a/crates/router/src/core/payments/routing/transformers.rs +++ b/crates/router/src/core/payments/routing/transformers.rs @@ -108,6 +108,7 @@ impl ForeignFrom for dsl_enums::Connector { api_enums::RoutableConnectors::Prophetpay => Self::Prophetpay, api_enums::RoutableConnectors::Rapyd => Self::Rapyd, api_enums::RoutableConnectors::Shift4 => Self::Shift4, + api_enums::RoutableConnectors::Signifyd => Self::Signifyd, api_enums::RoutableConnectors::Square => Self::Square, api_enums::RoutableConnectors::Stax => Self::Stax, api_enums::RoutableConnectors::Stripe => Self::Stripe, diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index 5166e326fb91..64a3d8193b86 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -10,6 +10,8 @@ pub mod disputes; pub mod dummy_connector; pub mod ephemeral_key; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod gsm; pub mod health; pub mod lock_utils; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 96bb47ea4e97..f3124692773b 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -32,7 +32,7 @@ use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, events::{event_logger::EventLogger, EventHandler}, - routes::cards_info::card_iin_info, + routes::{cards_info::card_iin_info, fraud_check as frm_routes}, services::get_store, }; @@ -277,6 +277,14 @@ impl Payments { .service( web::resource("/{payment_id}/capture").route(web::post().to(payments_capture)), ) + .service( + web::resource("/{payment_id}/approve") + .route(web::post().to(payments_approve)), + ) + .service( + web::resource("/{payment_id}/reject") + .route(web::post().to(payments_reject)), + ) .service( web::resource("/redirect/{payment_id}/{merchant_id}/{attempt_id}") .route(web::get().to(payments_start)), @@ -570,7 +578,7 @@ impl Webhooks { pub fn server(config: AppState) -> Scope { use api_models::webhooks as webhook_type; - web::scope("/webhooks") + let mut route = web::scope("/webhooks") .app_data(web::Data::new(config)) .service( web::resource("/{merchant_id}/{connector_id_or_name}") @@ -581,7 +589,17 @@ impl Webhooks { .route( web::put().to(receive_incoming_webhook::), ), - ) + ); + + #[cfg(feature = "frm")] + { + route = route.service( + web::resource("/frm_fulfillment") + .route(web::post().to(frm_routes::frm_fulfillment)), + ); + } + + route } } diff --git a/crates/router/src/routes/fraud_check.rs b/crates/router/src/routes/fraud_check.rs new file mode 100644 index 000000000000..d4363a236bb3 --- /dev/null +++ b/crates/router/src/routes/fraud_check.rs @@ -0,0 +1,42 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use common_utils::events::{ApiEventMetric, ApiEventsType}; +use router_env::Flow; + +use crate::{ + core::{api_locking, fraud_check as frm_core}, + services::{self, api}, + types::fraud_check::FraudCheckResponseData, + AppState, +}; + +pub async fn frm_fulfillment( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::FrmFulfillment; + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + json_payload.into_inner(), + |state, auth, req| { + frm_core::frm_fulfillment_core(state, auth.merchant_account, auth.key_store, req) + }, + &services::authentication::ApiKeyAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} + +impl ApiEventMetric for FraudCheckResponseData { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::FraudCheck) + } +} + +impl ApiEventMetric for frm_core::types::FrmFulfillmentRequest { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::FraudCheck) + } +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index a9cf7b44a73d..5456a27a1ecb 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -106,7 +106,7 @@ impl From for ApiIdentifier { | Flow::RefundsUpdate | Flow::RefundsList => Self::Refunds, - Flow::IncomingWebhookReceive => Self::Webhooks, + Flow::FrmFulfillment | Flow::IncomingWebhookReceive => Self::Webhooks, Flow::ApiKeyCreate | Flow::ApiKeyRetrieve diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index 81e53ade5e96..f9d1f352f317 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -888,6 +888,108 @@ pub async fn get_filters_for_payments( ) .await } + +#[cfg(feature = "oltp")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsApprove, payment_id))] +// #[post("/{payment_id}/approve")] +pub async fn payments_approve( + state: web::Data, + http_req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; + let flow = Flow::PaymentsApprove; + let fpayload = FPaymentsApproveRequest(&payload); + let locking_action = fpayload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + payload.clone(), + |state, auth, req| { + payments::payments_core::< + api_types::Authorize, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentApprove, + payment_types::PaymentsRequest { + payment_id: Some(payment_types::PaymentIdType::PaymentIntentId( + req.payment_id, + )), + ..Default::default() + }, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + payment_types::HeaderPayload::default(), + ) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, http_req.headers()), + locking_action, + )) + .await +} + +#[cfg(feature = "oltp")] +#[instrument(skip_all, fields(flow = ?Flow::PaymentsReject, payment_id))] +// #[post("/{payment_id}/reject")] +pub async fn payments_reject( + state: web::Data, + http_req: actix_web::HttpRequest, + json_payload: web::Json, + path: web::Path, +) -> impl Responder { + let mut payload = json_payload.into_inner(); + let payment_id = path.into_inner(); + tracing::Span::current().record("payment_id", &payment_id); + payload.payment_id = payment_id; + let flow = Flow::PaymentsReject; + let fpayload = FPaymentsRejectRequest(&payload); + let locking_action = fpayload.get_locking_input(flow.clone()); + + Box::pin(api::server_wrap( + flow.clone(), + state, + &http_req, + payload.clone(), + |state, auth, req| { + payments::payments_core::< + api_types::Reject, + payment_types::PaymentsResponse, + _, + _, + _, + Oss, + >( + state, + auth.merchant_account, + auth.key_store, + payments::PaymentReject, + req, + api::AuthFlow::Merchant, + payments::CallConnectorAction::Trigger, + None, + payment_types::HeaderPayload::default(), + ) + }, + auth::auth_type(&auth::ApiKeyAuth, &auth::JWTAuth, http_req.headers()), + locking_action, + )) + .await +} + async fn authorize_verify_select( operation: Op, state: app::AppState, @@ -1116,3 +1218,39 @@ impl GetLockingInput for payment_types::PaymentsCaptureRequest { } } } + +struct FPaymentsApproveRequest<'a>(&'a payment_types::PaymentsApproveRequest); + +impl<'a> GetLockingInput for FPaymentsApproveRequest<'a> { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.0.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} + +struct FPaymentsRejectRequest<'a>(&'a payment_types::PaymentsRejectRequest); + +impl<'a> GetLockingInput for FPaymentsRejectRequest<'a> { + fn get_locking_input(&self, flow: F) -> api_locking::LockAction + where + F: types::FlowMetric, + lock_utils::ApiIdentifier: From, + { + api_locking::LockAction::Hold { + input: api_locking::LockingInput { + unique_locking_key: self.0.payment_id.to_owned(), + api_identifier: lock_utils::ApiIdentifier::from(flow), + override_lock_retries: None, + }, + } + } +} diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs index aae17195517d..4973dfac06a6 100644 --- a/crates/router/src/services/api.rs +++ b/crates/router/src/services/api.rs @@ -1182,6 +1182,8 @@ impl Authenticate for api_models::payments::PaymentsRetrieveRequest {} impl Authenticate for api_models::payments::PaymentsCancelRequest {} impl Authenticate for api_models::payments::PaymentsCaptureRequest {} impl Authenticate for api_models::payments::PaymentsStartRequest {} +// impl Authenticate for api_models::payments::PaymentsApproveRequest {} +impl Authenticate for api_models::payments::PaymentsRejectRequest {} pub fn build_redirection_form( form: &RedirectForm, diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 203d4e30bf9a..bf0cc1102af8 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -8,6 +8,8 @@ pub mod api; pub mod domain; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod storage; pub mod transformers; @@ -22,6 +24,7 @@ use common_utils::{pii, pii::Email}; use data_models::mandates::MandateData; use error_stack::{IntoReport, ResultExt}; use masking::Secret; +use serde::Serialize; use self::{api::payments, storage::enums as storage_enums}; pub use crate::core::payments::{CustomerDetails, PaymentAddress}; @@ -703,7 +706,7 @@ pub enum PreprocessingResponseId { ConnectorTransactionId(String), } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Serialize)] pub enum ResponseId { ConnectorTransactionId(String), EncodedData(String), diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index b7d2fc8db33e..8c7f8a141abb 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -6,6 +6,8 @@ pub mod disputes; pub mod enums; pub mod ephemeral_key; pub mod files; +#[cfg(feature = "frm")] +pub mod fraud_check; pub mod mandates; pub mod payment_link; pub mod payment_methods; @@ -21,8 +23,8 @@ use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; pub use self::{ - admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_link::*, - payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, + admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, fraud_check::*, + payment_link::*, payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, }; use super::ErrorResponse; use crate::{ @@ -147,6 +149,7 @@ pub trait Connector: + ConnectorTransactionId + Payouts + ConnectorVerifyWebhookSource + + FraudCheck { } @@ -166,7 +169,8 @@ impl< + FileUpload + ConnectorTransactionId + Payouts - + ConnectorVerifyWebhookSource, + + ConnectorVerifyWebhookSource + + FraudCheck, > Connector for T { } diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs new file mode 100644 index 000000000000..8597ae0702e4 --- /dev/null +++ b/crates/router/src/types/api/fraud_check.rs @@ -0,0 +1,101 @@ +use std::str::FromStr; + +use api_models::enums; +use common_utils::errors::CustomResult; +use error_stack::{IntoReport, ResultExt}; + +use super::{BoxedConnector, ConnectorCommon, ConnectorData, SessionConnectorData}; +use crate::{ + connector, + core::errors, + services::api, + types::fraud_check::{ + FraudCheckCheckoutData, FraudCheckFulfillmentData, FraudCheckRecordReturnData, + FraudCheckResponseData, FraudCheckSaleData, FraudCheckTransactionData, + }, +}; + +pub trait FraudCheck: + ConnectorCommon + + FraudCheckSale + + FraudCheckTransaction + + FraudCheckCheckout + + FraudCheckFulfillment + + FraudCheckRecordReturn +{ +} + +#[derive(Debug, Clone)] +pub struct Sale; + +pub trait FraudCheckSale: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Checkout; + +pub trait FraudCheckCheckout: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Transaction; + +pub trait FraudCheckTransaction: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct Fulfillment; + +pub trait FraudCheckFulfillment: + api::ConnectorIntegration +{ +} + +#[derive(Debug, Clone)] +pub struct RecordReturn; + +pub trait FraudCheckRecordReturn: + api::ConnectorIntegration +{ +} + +#[derive(Clone, Debug)] +pub struct FraudCheckConnectorData { + pub connector: BoxedConnector, + pub connector_name: enums::FrmConnectors, +} +pub enum ConnectorCallType { + PreDetermined(ConnectorData), + Retryable(Vec), + SessionMultiple(Vec), +} + +impl FraudCheckConnectorData { + pub fn get_connector_by_name(name: &str) -> CustomResult { + let connector_name = enums::FrmConnectors::from_str(name) + .into_report() + .change_context(errors::ApiErrorResponse::IncorrectConnectorNameGiven) + .attach_printable_lazy(|| { + format!("unable to parse connector: {:?}", name.to_string()) + })?; + let connector = Self::convert_connector(connector_name)?; + Ok(Self { + connector, + connector_name, + }) + } + + fn convert_connector( + connector_name: enums::FrmConnectors, + ) -> CustomResult { + match connector_name { + enums::FrmConnectors::Signifyd => Ok(Box::new(&connector::Signifyd)), + } + } +} diff --git a/crates/router/src/types/fraud_check.rs b/crates/router/src/types/fraud_check.rs new file mode 100644 index 000000000000..4bbba8ac4dca --- /dev/null +++ b/crates/router/src/types/fraud_check.rs @@ -0,0 +1,126 @@ +use crate::{ + connector::signifyd::transformers::{FrmFullfillmentSignifydApiRequest, RefundMethod}, + pii::Serialize, + services, + types::{api, storage_enums, ErrorResponse, ResponseId, RouterData}, +}; +pub type FrmSaleRouterData = RouterData; + +pub type FrmSaleType = + dyn services::ConnectorIntegration; + +#[derive(Debug, Clone)] +pub struct FraudCheckSaleData { + pub amount: i64, + pub order_details: Option>, +} +#[derive(Debug, Clone)] +pub struct FrmRouterData { + pub merchant_id: String, + pub connector: String, + pub payment_id: String, + pub attempt_id: String, + pub request: FrmRequest, + pub response: FrmResponse, +} +#[derive(Debug, Clone)] +pub enum FrmRequest { + Sale(FraudCheckSaleData), + Checkout(FraudCheckCheckoutData), + Transaction(FraudCheckTransactionData), + Fulfillment(FraudCheckFulfillmentData), + RecordReturn(FraudCheckRecordReturnData), +} +#[derive(Debug, Clone)] +pub enum FrmResponse { + Sale(Result), + Checkout(Result), + Transaction(Result), + Fulfillment(Result), + RecordReturn(Result), +} + +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] +pub enum FraudCheckResponseData { + TransactionResponse { + resource_id: ResponseId, + status: storage_enums::FraudCheckStatus, + connector_metadata: Option, + reason: Option, + score: Option, + }, + FulfillmentResponse { + order_id: String, + shipment_ids: Vec, + }, + RecordReturnResponse { + resource_id: ResponseId, + connector_metadata: Option, + return_id: Option, + }, +} + +pub type FrmCheckoutRouterData = + RouterData; + +pub type FrmCheckoutType = dyn services::ConnectorIntegration< + api::Checkout, + FraudCheckCheckoutData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckCheckoutData { + pub amount: i64, + pub order_details: Option>, +} + +pub type FrmTransactionRouterData = + RouterData; + +pub type FrmTransactionType = dyn services::ConnectorIntegration< + api::Transaction, + FraudCheckTransactionData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckTransactionData { + pub amount: i64, + pub order_details: Option>, + pub currency: Option, + pub payment_method: Option, +} + +pub type FrmFulfillmentRouterData = + RouterData; + +pub type FrmFulfillmentType = dyn services::ConnectorIntegration< + api::Fulfillment, + FraudCheckFulfillmentData, + FraudCheckResponseData, +>; +pub type FrmRecordReturnRouterData = + RouterData; + +pub type FrmRecordReturnType = dyn services::ConnectorIntegration< + api::RecordReturn, + FraudCheckRecordReturnData, + FraudCheckResponseData, +>; + +#[derive(Debug, Clone)] +pub struct FraudCheckFulfillmentData { + pub amount: i64, + pub order_details: Option>>, + pub fulfillment_request: FrmFullfillmentSignifydApiRequest, +} + +#[derive(Debug, Clone)] +pub struct FraudCheckRecordReturnData { + pub amount: i64, + pub currency: Option, + pub refund_method: RefundMethod, + pub refund_transaction_id: Option, +} diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index e3e19323357b..b8c6e0ae736a 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -10,6 +10,7 @@ pub mod enums; pub mod ephemeral_key; pub mod events; pub mod file; +pub mod fraud_check; pub mod gsm; #[cfg(feature = "kv_store")] pub mod kv; @@ -21,29 +22,28 @@ pub mod merchant_key_store; pub mod payment_attempt; pub mod payment_link; pub mod payment_method; -pub mod routing_algorithm; -use std::collections::HashMap; - -pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; -pub use scheduler::db::process_tracker; -pub mod reverse_lookup; - pub mod payout_attempt; pub mod payouts; mod query; pub mod refund; +pub mod reverse_lookup; +pub mod routing_algorithm; pub mod user; pub mod user_role; +use std::collections::HashMap; + pub use data_models::payments::{ payment_attempt::{PaymentAttempt, PaymentAttemptNew, PaymentAttemptUpdate}, payment_intent::{PaymentIntentNew, PaymentIntentUpdate}, PaymentIntent, }; +pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate}; +pub use scheduler::db::process_tracker; pub use self::{ address::*, api_keys::*, capture::*, cards_info::*, configs::*, customers::*, dispute::*, - ephemeral_key::*, events::*, file::*, gsm::*, locker_mock_up::*, mandate::*, + ephemeral_key::*, events::*, file::*, fraud_check::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, process_tracker::*, refund::*, reverse_lookup::*, routing_algorithm::*, user::*, user_role::*, diff --git a/crates/router/src/types/storage/fraud_check.rs b/crates/router/src/types/storage/fraud_check.rs new file mode 100644 index 000000000000..f3dd259c3ce4 --- /dev/null +++ b/crates/router/src/types/storage/fraud_check.rs @@ -0,0 +1,3 @@ +pub use diesel_models::fraud_check::{ + FraudCheck, FraudCheckNew, FraudCheckUpdate, FraudCheckUpdateInternal, +}; diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 45aad93371e2..be5c773a7e68 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -312,6 +312,7 @@ impl ForeignFrom for api_enums::RoutableConnectors { dsl_enums::Connector::Prophetpay => Self::Prophetpay, dsl_enums::Connector::Rapyd => Self::Rapyd, dsl_enums::Connector::Shift4 => Self::Shift4, + dsl_enums::Connector::Signifyd => Self::Signifyd, dsl_enums::Connector::Square => Self::Square, dsl_enums::Connector::Stax => Self::Stax, dsl_enums::Connector::Stripe => Self::Stripe, diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index 266baf0e3863..614bec49d937 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -38,9 +38,10 @@ cargo_metadata = "0.15.4" vergen = { version = "8.2.1", features = ["cargo", "git", "git2", "rustc"], optional = true } [features] -default = ["actix_web", "payouts"] +default = ["actix_web", "payouts", "frm"] actix_web = ["tracing-actix-web"] log_custom_entries_to_extra = [] log_extra_implicit_fields = [] log_active_span_json = [] payouts = [] +frm = [] diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 178f837fce18..fd5331b30d1e 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -255,6 +255,9 @@ pub enum Flow { DecisionManagerDeleteConfig, /// Retrieve Decision Manager Config DecisionManagerRetrieveConfig, + #[cfg(feature = "frm")] + /// Manual payment fullfilment acknowledgement + FrmFulfillment, } /// diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 2fb729fb7b90..ad08035f3981 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -253,3 +253,6 @@ connection_timeout = 10 [kv_config] ttl = 300 # 5 * 60 seconds + +[frm] +is_frm_enabled = true From 6191c6f1428b264691c16981286181036e051b86 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 23 Nov 2023 23:09:19 +0530 Subject: [PATCH 2/5] refactor: add feature flags for FRM functionality --- crates/router/src/connector/signifyd.rs | 24 +- .../src/connector/signifyd/transformers.rs | 612 +----------------- .../connector/signifyd/transformers/api.rs | 589 +++++++++++++++++ .../connector/signifyd/transformers/auth.rs | 20 + crates/router/src/connector/utils.rs | 17 +- .../fraud_check/operation/fraud_check_pre.rs | 8 +- crates/router/src/core/payments/flows.rs | 24 +- crates/router/src/routes/app.rs | 5 +- crates/router/src/types/api.rs | 20 +- crates/router/src/types/api/fraud_check.rs | 12 +- crates/router_env/src/logger/types.rs | 2 +- 11 files changed, 697 insertions(+), 636 deletions(-) create mode 100644 crates/router/src/connector/signifyd/transformers/api.rs create mode 100644 crates/router/src/connector/signifyd/transformers/auth.rs diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs index 4b2abe893882..5e873e12dd82 100644 --- a/crates/router/src/connector/signifyd.rs +++ b/crates/router/src/connector/signifyd.rs @@ -9,12 +9,16 @@ use crate::{ configs::settings, core::errors::{self, CustomResult}, headers, - services::{self, request, ConnectorIntegration, ConnectorValidation}, + services::{request, ConnectorIntegration, ConnectorValidation}, types::{ self, - api::{self, fraud_check as frm_api, ConnectorCommon, ConnectorCommonExt}, - fraud_check as frm_types, ErrorResponse, Response, + api::{self, ConnectorCommon, ConnectorCommonExt}, }, +}; +#[cfg(feature = "frm")] +use crate::{ + services, + types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, utils::{self, BytesExt}, }; @@ -67,6 +71,7 @@ impl ConnectorCommon for Signifyd { )]) } + #[cfg(feature = "frm")] fn build_error_response( &self, res: Response, @@ -155,13 +160,20 @@ impl ConnectorIntegration for Signifyd {} -impl frm_api::FraudCheck for Signifyd {} +#[cfg(feature = "frm")] +impl api::FraudCheck for Signifyd {} +#[cfg(feature = "frm")] impl frm_api::FraudCheckSale for Signifyd {} +#[cfg(feature = "frm")] impl frm_api::FraudCheckCheckout for Signifyd {} +#[cfg(feature = "frm")] impl frm_api::FraudCheckTransaction for Signifyd {} +#[cfg(feature = "frm")] impl frm_api::FraudCheckFulfillment for Signifyd {} +#[cfg(feature = "frm")] impl frm_api::FraudCheckRecordReturn for Signifyd {} +#[cfg(feature = "frm")] impl ConnectorIntegration< frm_api::Sale, @@ -248,6 +260,7 @@ impl } } +#[cfg(feature = "frm")] impl ConnectorIntegration< frm_api::Checkout, @@ -336,6 +349,7 @@ impl } } +#[cfg(feature = "frm")] impl ConnectorIntegration< frm_api::Transaction, @@ -426,6 +440,7 @@ impl } } +#[cfg(feature = "frm")] impl ConnectorIntegration< frm_api::Fulfillment, @@ -516,6 +531,7 @@ impl } } +#[cfg(feature = "frm")] impl ConnectorIntegration< frm_api::RecordReturn, diff --git a/crates/router/src/connector/signifyd/transformers.rs b/crates/router/src/connector/signifyd/transformers.rs index 958abaa7f007..4f155f341f6d 100644 --- a/crates/router/src/connector/signifyd/transformers.rs +++ b/crates/router/src/connector/signifyd/transformers.rs @@ -1,607 +1,7 @@ -use bigdecimal::ToPrimitive; -use common_utils::pii::Email; -use error_stack; -use masking::Secret; -use serde::{Deserialize, Serialize}; -use time::PrimitiveDateTime; -use utoipa::ToSchema; +#[cfg(feature = "frm")] +pub mod api; +pub mod auth; -use crate::{ - connector::utils::{ - AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckRecordReturnRequest, - FraudCheckSaleRequest, FraudCheckTransactionRequest, RouterData, - }, - core::{ - errors, - fraud_check::types::{self as core_types, FrmFulfillmentRequest}, - }, - types::{ - self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, - ResponseId, ResponseRouterData, - }, -}; - -#[allow(dead_code)] -#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum DecisionDelivery { - Sync, - AsyncOnly, -} - -#[derive(Debug, Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct Purchase { - #[serde(with = "common_utils::custom_serde::iso8601")] - created_at: PrimitiveDateTime, - order_channel: OrderChannel, - total_price: i64, - products: Vec, - shipments: Shipments, -} - -#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum OrderChannel { - Web, - Phone, - MobileApp, - Social, - Marketplace, - InStoreKiosk, - ScanAndGo, - SmartTv, - Mit, -} - -#[derive(Debug, Serialize, Eq, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Products { - item_name: String, - item_price: i64, - item_quantity: i32, -} - -#[derive(Debug, Serialize, Eq, PartialEq, Clone)] -pub struct Shipments { - destination: Destination, -} - -#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Destination { - full_name: Secret, - organization: Option, - email: Option, - address: Address, -} - -#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Address { - street_address: Secret, - unit: Option>, - postal_code: Secret, - city: String, - province_code: Secret, - country_code: common_enums::CountryAlpha2, -} - -#[derive(Debug, Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct SignifydPaymentsSaleRequest { - order_id: String, - purchase: Purchase, - decision_delivery: DecisionDelivery, -} - -impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { - type Error = error_stack::Report; - fn try_from(item: &frm_types::FrmSaleRouterData) -> Result { - let products = item - .request - .get_order_details()? - .iter() - .map(|order_detail| Products { - item_name: order_detail.product_name.clone(), - item_price: order_detail.amount, - item_quantity: i32::from(order_detail.quantity), - }) - .collect::>(); - let ship_address = item.get_shipping_address()?; - let street_addr = ship_address.get_line1()?; - let city_addr = ship_address.get_city()?; - let zip_code_addr = ship_address.get_zip()?; - let country_code_addr = ship_address.get_country()?; - let _first_name_addr = ship_address.get_first_name()?; - let _last_name_addr = ship_address.get_last_name()?; - let address: Address = Address { - street_address: street_addr.clone(), - unit: None, - postal_code: zip_code_addr.clone(), - city: city_addr.clone(), - province_code: zip_code_addr.clone(), - country_code: country_code_addr.to_owned(), - }; - let destination: Destination = Destination { - full_name: ship_address.get_full_name().unwrap_or_default(), - organization: None, - email: None, - address, - }; - - let created_at = common_utils::date_time::now(); - let order_channel = OrderChannel::Web; - let shipments = Shipments { destination }; - let purchase = Purchase { - created_at, - order_channel, - total_price: item.request.amount, - products, - shipments, - }; - Ok(Self { - order_id: item.attempt_id.clone(), - purchase, - decision_delivery: DecisionDelivery::Sync, - }) - } -} - -pub struct SignifydAuthType { - pub api_key: Secret, -} - -impl TryFrom<&types::ConnectorAuthType> for SignifydAuthType { - type Error = error_stack::Report; - fn try_from(auth_type: &types::ConnectorAuthType) -> Result { - match auth_type { - types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { - api_key: api_key.to_owned(), - }), - _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), - } - } -} - -// PaymentsResponse - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Decision { - #[serde(with = "common_utils::custom_serde::iso8601")] - created_at: PrimitiveDateTime, - checkpoint_action: SignifydPaymentStatus, - checkpoint_action_reason: Option, - checkpoint_action_policy: Option, - score: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum SignifydPaymentStatus { - Accept, - Challenge, - Credit, - Hold, - Reject, -} - -impl From for storage_enums::FraudCheckStatus { - fn from(item: SignifydPaymentStatus) -> Self { - match item { - SignifydPaymentStatus::Accept => Self::Legit, - SignifydPaymentStatus::Reject => Self::Fraud, - SignifydPaymentStatus::Hold => Self::ManualReview, - _ => Self::Pending, - } - } -} -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct SignifydPaymentsResponse { - signifyd_id: i64, - order_id: String, - decision: Decision, -} - -impl - TryFrom> - for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: ResponseRouterData, - ) -> Result { - Ok(Self { - response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), - status: storage_enums::FraudCheckStatus::from( - item.response.decision.checkpoint_action, - ), - connector_metadata: None, - score: item.response.decision.score.and_then(|data| data.to_i32()), - reason: item - .response - .decision - .checkpoint_action_reason - .map(serde_json::Value::from), - }), - ..item.data - }) - } -} - -#[derive(Debug, Deserialize, PartialEq)] -pub struct SignifydErrorResponse { - pub messages: Vec, - pub errors: serde_json::Value, -} - -#[derive(Debug, Serialize, Eq, PartialEq, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Transactions { - transaction_id: String, - gateway_status_code: String, - payment_method: storage_enums::PaymentMethod, - amount: i64, - currency: storage_enums::Currency, -} - -#[derive(Debug, Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct SignifydPaymentsTransactionRequest { - order_id: String, - checkout_id: String, - transactions: Transactions, -} - -impl From for GatewayStatusCode { - fn from(item: storage_enums::AttemptStatus) -> Self { - match item { - storage_enums::AttemptStatus::Pending => Self::Pending, - storage_enums::AttemptStatus::Failure => Self::Failure, - storage_enums::AttemptStatus::Charged => Self::Success, - _ => Self::Pending, - } - } -} - -impl TryFrom<&frm_types::FrmTransactionRouterData> for SignifydPaymentsTransactionRequest { - type Error = error_stack::Report; - fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { - let currency = item.request.get_currency()?; - let transactions = Transactions { - amount: item.request.amount, - transaction_id: item.clone().payment_id, - gateway_status_code: GatewayStatusCode::from(item.status).to_string(), - payment_method: item.payment_method, - currency, - }; - Ok(Self { - order_id: item.attempt_id.clone(), - checkout_id: item.payment_id.clone(), - transactions, - }) - } -} - -#[derive( - Clone, - Copy, - Debug, - Default, - Eq, - PartialEq, - serde::Serialize, - serde::Deserialize, - strum::Display, - strum::EnumString, -)] -#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] -pub enum GatewayStatusCode { - Success, - Failure, - #[default] - Pending, - Error, - Cancelled, - Expired, - SoftDecline, -} - -#[derive(Debug, Serialize, Eq, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct SignifydPaymentsCheckoutRequest { - checkout_id: String, - order_id: String, - purchase: Purchase, -} - -impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequest { - type Error = error_stack::Report; - fn try_from(item: &frm_types::FrmCheckoutRouterData) -> Result { - let products = item - .request - .get_order_details()? - .iter() - .map(|order_detail| Products { - item_name: order_detail.product_name.clone(), - item_price: order_detail.amount, - item_quantity: i32::from(order_detail.quantity), - }) - .collect::>(); - let ship_address = item.get_shipping_address()?; - let street_addr = ship_address.get_line1()?; - let city_addr = ship_address.get_city()?; - let zip_code_addr = ship_address.get_zip()?; - let country_code_addr = ship_address.get_country()?; - let _first_name_addr = ship_address.get_first_name()?; - let _last_name_addr = ship_address.get_last_name()?; - let address: Address = Address { - street_address: street_addr.clone(), - unit: None, - postal_code: zip_code_addr.clone(), - city: city_addr.clone(), - province_code: zip_code_addr.clone(), - country_code: country_code_addr.to_owned(), - }; - let destination: Destination = Destination { - full_name: ship_address.get_full_name().unwrap_or_default(), - organization: None, - email: None, - address, - }; - let created_at = common_utils::date_time::now(); - let order_channel = OrderChannel::Web; - let shipments: Shipments = Shipments { destination }; - let purchase = Purchase { - created_at, - order_channel, - total_price: item.request.amount, - products, - shipments, - }; - Ok(Self { - checkout_id: item.payment_id.clone(), - order_id: item.attempt_id.clone(), - purchase, - }) - } -} - -#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] -#[serde(deny_unknown_fields)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub struct FrmFullfillmentSignifydApiRequest { - pub order_id: String, - pub fulfillment_status: Option, - pub fulfillments: Vec, -} - -#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] -#[serde(untagged)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub enum FulfillmentStatus { - PARTIAL, - COMPLETE, - REPLACEMENT, - CANCELED, -} - -#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub struct Fulfillments { - pub shipment_id: String, - pub products: Option>, - pub destination: Destination, -} - -#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub struct Product { - pub item_name: String, - pub item_quantity: i64, - pub item_id: String, -} - -impl From for FrmFullfillmentSignifydApiRequest { - fn from(req: FrmFulfillmentRequest) -> Self { - Self { - order_id: req.order_id, - fulfillment_status: req.fulfillment_status.map(FulfillmentStatus::from), - fulfillments: req - .fulfillments - .iter() - .map(|f| Fulfillments::from(f.clone())) - .collect(), - } - } -} - -impl From for FulfillmentStatus { - fn from(status: core_types::FulfillmentStatus) -> Self { - match status { - core_types::FulfillmentStatus::PARTIAL => Self::PARTIAL, - core_types::FulfillmentStatus::COMPLETE => Self::COMPLETE, - core_types::FulfillmentStatus::REPLACEMENT => Self::REPLACEMENT, - core_types::FulfillmentStatus::CANCELED => Self::CANCELED, - } - } -} - -impl From for Fulfillments { - fn from(fulfillment: core_types::Fulfillments) -> Self { - Self { - shipment_id: fulfillment.shipment_id, - products: fulfillment - .products - .map(|products| products.iter().map(|p| Product::from(p.clone())).collect()), - destination: Destination::from(fulfillment.destination), - } - } -} - -impl From for Product { - fn from(product: core_types::Product) -> Self { - Self { - item_name: product.item_name, - item_quantity: product.item_quantity, - item_id: product.item_id, - } - } -} - -impl From for Destination { - fn from(destination: core_types::Destination) -> Self { - Self { - full_name: destination.full_name, - organization: destination.organization, - email: destination.email, - address: Address::from(destination.address), - } - } -} - -impl From for Address { - fn from(address: core_types::Address) -> Self { - Self { - street_address: address.street_address, - unit: address.unit, - postal_code: address.postal_code, - city: address.city, - province_code: address.province_code, - country_code: address.country_code, - } - } -} - -#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub struct FrmFullfillmentSignifydApiResponse { - pub order_id: String, - pub shipment_ids: Vec, -} - -impl - TryFrom< - ResponseRouterData< - Fulfillment, - FrmFullfillmentSignifydApiResponse, - frm_types::FraudCheckFulfillmentData, - frm_types::FraudCheckResponseData, - >, - > - for types::RouterData< - Fulfillment, - frm_types::FraudCheckFulfillmentData, - frm_types::FraudCheckResponseData, - > -{ - type Error = error_stack::Report; - fn try_from( - item: ResponseRouterData< - Fulfillment, - FrmFullfillmentSignifydApiResponse, - frm_types::FraudCheckFulfillmentData, - frm_types::FraudCheckResponseData, - >, - ) -> Result { - Ok(Self { - response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { - order_id: item.response.order_id, - shipment_ids: item.response.shipment_ids, - }), - ..item.data - }) - } -} - -#[derive(Debug, Serialize, Eq, PartialEq, Clone)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub struct SignifydRefund { - method: RefundMethod, - amount: String, - currency: storage_enums::Currency, -} - -#[derive(Debug, Serialize, Eq, PartialEq)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub struct SignifydPaymentsRecordReturnRequest { - order_id: String, - return_id: String, - refund_transaction_id: Option, - refund: SignifydRefund, -} - -#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] -pub enum RefundMethod { - StoreCredit, - OriginalPaymentInstrument, - NewPaymentInstrument, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -#[serde_with::skip_serializing_none] -#[serde(rename_all = "camelCase")] -pub struct SignifydPaymentsRecordReturnResponse { - return_id: String, - order_id: String, -} - -impl - TryFrom< - ResponseRouterData< - F, - SignifydPaymentsRecordReturnResponse, - T, - frm_types::FraudCheckResponseData, - >, - > for types::RouterData -{ - type Error = error_stack::Report; - fn try_from( - item: ResponseRouterData< - F, - SignifydPaymentsRecordReturnResponse, - T, - frm_types::FraudCheckResponseData, - >, - ) -> Result { - Ok(Self { - response: Ok(frm_types::FraudCheckResponseData::RecordReturnResponse { - resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), - return_id: Some(item.response.return_id.to_string()), - connector_metadata: None, - }), - ..item.data - }) - } -} - -impl TryFrom<&frm_types::FrmRecordReturnRouterData> for SignifydPaymentsRecordReturnRequest { - type Error = error_stack::Report; - fn try_from(item: &frm_types::FrmRecordReturnRouterData) -> Result { - let currency = item.request.get_currency()?; - let refund = SignifydRefund { - method: item.request.refund_method.clone(), - amount: item.request.amount.to_string(), - currency, - }; - Ok(Self { - return_id: uuid::Uuid::new_v4().to_string(), - refund_transaction_id: item.request.refund_transaction_id.clone(), - refund, - order_id: item.attempt_id.clone(), - }) - } -} +#[cfg(feature = "frm")] +pub use self::api::*; +pub use self::auth::*; diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs new file mode 100644 index 000000000000..126d97ec9cba --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -0,0 +1,589 @@ +use bigdecimal::ToPrimitive; +use common_utils::pii::Email; +use error_stack; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; +use utoipa::ToSchema; + +use crate::{ + connector::utils::{ + AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckRecordReturnRequest, + FraudCheckSaleRequest, FraudCheckTransactionRequest, RouterData, + }, + core::{ + errors, + fraud_check::types::{self as core_types, FrmFulfillmentRequest}, + }, + types::{ + self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + ResponseId, ResponseRouterData, + }, +}; + +#[allow(dead_code)] +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum DecisionDelivery { + Sync, + AsyncOnly, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct Purchase { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + order_channel: OrderChannel, + total_price: i64, + products: Vec, + shipments: Shipments, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderChannel { + Web, + Phone, + MobileApp, + Social, + Marketplace, + InStoreKiosk, + ScanAndGo, + SmartTv, + Mit, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Products { + item_name: String, + item_price: i64, + item_quantity: i32, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +pub struct Shipments { + destination: Destination, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Destination { + full_name: Secret, + organization: Option, + email: Option, + address: Address, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Address { + street_address: Secret, + unit: Option>, + postal_code: Secret, + city: String, + province_code: Secret, + country_code: common_enums::CountryAlpha2, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsSaleRequest { + order_id: String, + purchase: Purchase, + decision_delivery: DecisionDelivery, +} + +impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmSaleRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + purchase, + decision_delivery: DecisionDelivery::Sync, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Decision { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + checkpoint_action: SignifydPaymentStatus, + checkpoint_action_reason: Option, + checkpoint_action_policy: Option, + score: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SignifydPaymentStatus { + Accept, + Challenge, + Credit, + Hold, + Reject, +} + +impl From for storage_enums::FraudCheckStatus { + fn from(item: SignifydPaymentStatus) -> Self { + match item { + SignifydPaymentStatus::Accept => Self::Legit, + SignifydPaymentStatus::Reject => Self::Fraud, + SignifydPaymentStatus::Hold => Self::ManualReview, + _ => Self::Pending, + } + } +} +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsResponse { + signifyd_id: i64, + order_id: String, + decision: Decision, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + status: storage_enums::FraudCheckStatus::from( + item.response.decision.checkpoint_action, + ), + connector_metadata: None, + score: item.response.decision.score.and_then(|data| data.to_i32()), + reason: item + .response + .decision + .checkpoint_action_reason + .map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, PartialEq)] +pub struct SignifydErrorResponse { + pub messages: Vec, + pub errors: serde_json::Value, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Transactions { + transaction_id: String, + gateway_status_code: String, + payment_method: storage_enums::PaymentMethod, + amount: i64, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsTransactionRequest { + order_id: String, + checkout_id: String, + transactions: Transactions, +} + +impl From for GatewayStatusCode { + fn from(item: storage_enums::AttemptStatus) -> Self { + match item { + storage_enums::AttemptStatus::Pending => Self::Pending, + storage_enums::AttemptStatus::Failure => Self::Failure, + storage_enums::AttemptStatus::Charged => Self::Success, + _ => Self::Pending, + } + } +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for SignifydPaymentsTransactionRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + let currency = item.request.get_currency()?; + let transactions = Transactions { + amount: item.request.amount, + transaction_id: item.clone().payment_id, + gateway_status_code: GatewayStatusCode::from(item.status).to_string(), + payment_method: item.payment_method, + currency, + }; + Ok(Self { + order_id: item.attempt_id.clone(), + checkout_id: item.payment_id.clone(), + transactions, + }) + } +} + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, +)] +#[strum(serialize_all = "SCREAMING_SNAKE_CASE")] +pub enum GatewayStatusCode { + Success, + Failure, + #[default] + Pending, + Error, + Cancelled, + Expired, + SoftDecline, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsCheckoutRequest { + checkout_id: String, + order_id: String, + purchase: Purchase, +} + +impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmCheckoutRouterData) -> Result { + let products = item + .request + .get_order_details()? + .iter() + .map(|order_detail| Products { + item_name: order_detail.product_name.clone(), + item_price: order_detail.amount, + item_quantity: i32::from(order_detail.quantity), + }) + .collect::>(); + let ship_address = item.get_shipping_address()?; + let street_addr = ship_address.get_line1()?; + let city_addr = ship_address.get_city()?; + let zip_code_addr = ship_address.get_zip()?; + let country_code_addr = ship_address.get_country()?; + let _first_name_addr = ship_address.get_first_name()?; + let _last_name_addr = ship_address.get_last_name()?; + let address: Address = Address { + street_address: street_addr.clone(), + unit: None, + postal_code: zip_code_addr.clone(), + city: city_addr.clone(), + province_code: zip_code_addr.clone(), + country_code: country_code_addr.to_owned(), + }; + let destination: Destination = Destination { + full_name: ship_address.get_full_name().unwrap_or_default(), + organization: None, + email: None, + address, + }; + let created_at = common_utils::date_time::now(); + let order_channel = OrderChannel::Web; + let shipments: Shipments = Shipments { destination }; + let purchase = Purchase { + created_at, + order_channel, + total_price: item.request.amount, + products, + shipments, + }; + Ok(Self { + checkout_id: item.payment_id.clone(), + order_id: item.attempt_id.clone(), + purchase, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde(deny_unknown_fields)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydApiRequest { + pub order_id: String, + pub fulfillment_status: Option, + pub fulfillments: Vec, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub enum FulfillmentStatus { + PARTIAL, + COMPLETE, + REPLACEMENT, + CANCELED, +} + +#[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Fulfillments { + pub shipment_id: String, + pub products: Option>, + pub destination: Destination, +} + +#[derive(Default, Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct Product { + pub item_name: String, + pub item_quantity: i64, + pub item_id: String, +} + +impl From for FrmFullfillmentSignifydApiRequest { + fn from(req: FrmFulfillmentRequest) -> Self { + Self { + order_id: req.order_id, + fulfillment_status: req.fulfillment_status.map(FulfillmentStatus::from), + fulfillments: req + .fulfillments + .iter() + .map(|f| Fulfillments::from(f.clone())) + .collect(), + } + } +} + +impl From for FulfillmentStatus { + fn from(status: core_types::FulfillmentStatus) -> Self { + match status { + core_types::FulfillmentStatus::PARTIAL => Self::PARTIAL, + core_types::FulfillmentStatus::COMPLETE => Self::COMPLETE, + core_types::FulfillmentStatus::REPLACEMENT => Self::REPLACEMENT, + core_types::FulfillmentStatus::CANCELED => Self::CANCELED, + } + } +} + +impl From for Fulfillments { + fn from(fulfillment: core_types::Fulfillments) -> Self { + Self { + shipment_id: fulfillment.shipment_id, + products: fulfillment + .products + .map(|products| products.iter().map(|p| Product::from(p.clone())).collect()), + destination: Destination::from(fulfillment.destination), + } + } +} + +impl From for Product { + fn from(product: core_types::Product) -> Self { + Self { + item_name: product.item_name, + item_quantity: product.item_quantity, + item_id: product.item_id, + } + } +} + +impl From for Destination { + fn from(destination: core_types::Destination) -> Self { + Self { + full_name: destination.full_name, + organization: destination.organization, + email: destination.email, + address: Address::from(destination.address), + } + } +} + +impl From for Address { + fn from(address: core_types::Address) -> Self { + Self { + street_address: address.street_address, + unit: address.unit, + postal_code: address.postal_code, + city: address.city, + province_code: address.province_code, + country_code: address.country_code, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct FrmFullfillmentSignifydApiResponse { + pub order_id: String, + pub shipment_ids: Vec, +} + +impl + TryFrom< + ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + > + for types::RouterData< + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + Fulfillment, + FrmFullfillmentSignifydApiResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { + order_id: item.response.order_id, + shipment_ids: item.response.shipment_ids, + }), + ..item.data + }) + } +} + +#[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydRefund { + method: RefundMethod, + amount: String, + currency: storage_enums::Currency, +} + +#[derive(Debug, Serialize, Eq, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnRequest { + order_id: String, + return_id: String, + refund_transaction_id: Option, + refund: SignifydRefund, +} + +#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum RefundMethod { + StoreCredit, + OriginalPaymentInstrument, + NewPaymentInstrument, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde_with::skip_serializing_none] +#[serde(rename_all = "camelCase")] +pub struct SignifydPaymentsRecordReturnResponse { + return_id: String, + order_id: String, +} + +impl + TryFrom< + ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + SignifydPaymentsRecordReturnResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::RecordReturnResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order_id), + return_id: Some(item.response.return_id.to_string()), + connector_metadata: None, + }), + ..item.data + }) + } +} + +impl TryFrom<&frm_types::FrmRecordReturnRouterData> for SignifydPaymentsRecordReturnRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmRecordReturnRouterData) -> Result { + let currency = item.request.get_currency()?; + let refund = SignifydRefund { + method: item.request.refund_method.clone(), + amount: item.request.amount.to_string(), + currency, + }; + Ok(Self { + return_id: uuid::Uuid::new_v4().to_string(), + refund_transaction_id: item.request.refund_transaction_id.clone(), + refund, + order_id: item.attempt_id.clone(), + }) + } +} diff --git a/crates/router/src/connector/signifyd/transformers/auth.rs b/crates/router/src/connector/signifyd/transformers/auth.rs new file mode 100644 index 000000000000..cc5867aea366 --- /dev/null +++ b/crates/router/src/connector/signifyd/transformers/auth.rs @@ -0,0 +1,20 @@ +use error_stack; +use masking::Secret; + +use crate::{core::errors, types}; + +pub struct SignifydAuthType { + pub api_key: Secret, +} + +impl TryFrom<&types::ConnectorAuthType> for SignifydAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::HeaderKey { api_key } => Ok(Self { + api_key: api_key.to_owned(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 024124465cc5..5e772c03c078 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -17,6 +17,8 @@ use once_cell::sync::Lazy; use regex::Regex; use serde::Serializer; +#[cfg(feature = "frm")] +use crate::types::{fraud_check, storage::enums as storage_enums}; use crate::{ consts, core::{ @@ -25,9 +27,7 @@ use crate::{ }, pii::PeekInterface, types::{ - self, api, fraud_check, - storage::{enums as storage_enums, payment_attempt::PaymentAttemptExt}, - transformers::ForeignTryFrom, + self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, PaymentsCancelData, ResponseId, }, utils::{OptionExt, ValueExt}, @@ -1577,10 +1577,11 @@ pub fn validate_currency( Ok(()) } +#[cfg(feature = "frm")] pub trait FraudCheckSaleRequest { fn get_order_details(&self) -> Result, Error>; } - +#[cfg(feature = "frm")] impl FraudCheckSaleRequest for fraud_check::FraudCheckSaleData { fn get_order_details(&self) -> Result, Error> { self.order_details @@ -1589,9 +1590,11 @@ impl FraudCheckSaleRequest for fraud_check::FraudCheckSaleData { } } +#[cfg(feature = "frm")] pub trait FraudCheckCheckoutRequest { fn get_order_details(&self) -> Result, Error>; } +#[cfg(feature = "frm")] impl FraudCheckCheckoutRequest for fraud_check::FraudCheckCheckoutData { fn get_order_details(&self) -> Result, Error> { self.order_details @@ -1600,20 +1603,22 @@ impl FraudCheckCheckoutRequest for fraud_check::FraudCheckCheckoutData { } } +#[cfg(feature = "frm")] pub trait FraudCheckTransactionRequest { fn get_currency(&self) -> Result; } - +#[cfg(feature = "frm")] impl FraudCheckTransactionRequest for fraud_check::FraudCheckTransactionData { fn get_currency(&self) -> Result { self.currency.ok_or_else(missing_field_err("currency")) } } +#[cfg(feature = "frm")] pub trait FraudCheckRecordReturnRequest { fn get_currency(&self) -> Result; } - +#[cfg(feature = "frm")] impl FraudCheckRecordReturnRequest for fraud_check::FraudCheckRecordReturnData { fn get_currency(&self) -> Result { self.currency.ok_or_else(missing_field_err("currency")) diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs index 8cc486e17140..00f50d01a862 100644 --- a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs +++ b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs @@ -136,7 +136,7 @@ impl Domain for FraudCheckPre { #[instrument(skip_all)] async fn post_payment_frm<'a>( &'a self, - state_vas: &'a AppState, + state: &'a AppState, payment_data: &mut payments::PaymentData, frm_data: &mut FrmData, merchant_account: &domain::MerchantAccount, @@ -144,7 +144,7 @@ impl Domain for FraudCheckPre { key_store: domain::MerchantKeyStore, ) -> RouterResult> { let router_data = frm_core::call_frm_service::( - state_vas, + state, payment_data, frm_data.to_owned(), merchant_account, @@ -170,7 +170,7 @@ impl Domain for FraudCheckPre { async fn pre_payment_frm<'a>( &'a self, - state_vas: &'a AppState, + state: &'a AppState, payment_data: &mut payments::PaymentData, frm_data: &mut FrmData, merchant_account: &domain::MerchantAccount, @@ -178,7 +178,7 @@ impl Domain for FraudCheckPre { key_store: domain::MerchantKeyStore, ) -> RouterResult { let router_data = frm_core::call_frm_service::( - state_vas, + state, payment_data, frm_data.to_owned(), merchant_account, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index c8e9bfe925a8..f8c03aef1584 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -10,6 +10,8 @@ pub mod setup_mandate_flow; use async_trait::async_trait; +#[cfg(feature = "frm")] +use crate::types::fraud_check as frm_types; use crate::{ connector, core::{ @@ -18,7 +20,7 @@ use crate::{ }, routes::AppState, services, - types::{self, api, domain, fraud_check as frm_types}, + types::{self, api, domain}, }; #[async_trait] @@ -1663,6 +1665,7 @@ default_imp_for_fraud_check!( connector::Zen ); +#[cfg(feature = "frm")] macro_rules! default_imp_for_frm_sale { ($($path:ident::$connector:ident),*) => { $( @@ -1678,8 +1681,10 @@ macro_rules! default_imp_for_frm_sale { }; } +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl api::FraudCheckSale for connector::DummyConnector {} +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< @@ -1690,6 +1695,7 @@ impl { } +#[cfg(feature = "frm")] default_imp_for_frm_sale!( connector::Aci, connector::Adyen, @@ -1743,6 +1749,7 @@ default_imp_for_frm_sale!( connector::Zen ); +#[cfg(feature = "frm")] macro_rules! default_imp_for_frm_checkout { ($($path:ident::$connector:ident),*) => { $( @@ -1758,8 +1765,10 @@ macro_rules! default_imp_for_frm_checkout { }; } +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl api::FraudCheckCheckout for connector::DummyConnector {} +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< @@ -1770,6 +1779,7 @@ impl { } +#[cfg(feature = "frm")] default_imp_for_frm_checkout!( connector::Aci, connector::Adyen, @@ -1823,6 +1833,7 @@ default_imp_for_frm_checkout!( connector::Zen ); +#[cfg(feature = "frm")] macro_rules! default_imp_for_frm_transaction { ($($path:ident::$connector:ident),*) => { $( @@ -1838,8 +1849,10 @@ macro_rules! default_imp_for_frm_transaction { }; } +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl api::FraudCheckTransaction for connector::DummyConnector {} +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< @@ -1850,6 +1863,7 @@ impl { } +#[cfg(feature = "frm")] default_imp_for_frm_transaction!( connector::Aci, connector::Adyen, @@ -1903,6 +1917,7 @@ default_imp_for_frm_transaction!( connector::Zen ); +#[cfg(feature = "frm")] macro_rules! default_imp_for_frm_fulfillment { ($($path:ident::$connector:ident),*) => { $( @@ -1918,8 +1933,10 @@ macro_rules! default_imp_for_frm_fulfillment { }; } +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl api::FraudCheckFulfillment for connector::DummyConnector {} +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< @@ -1930,6 +1947,7 @@ impl { } +#[cfg(feature = "frm")] default_imp_for_frm_fulfillment!( connector::Aci, connector::Adyen, @@ -1983,6 +2001,7 @@ default_imp_for_frm_fulfillment!( connector::Zen ); +#[cfg(feature = "frm")] macro_rules! default_imp_for_frm_record_return { ($($path:ident::$connector:ident),*) => { $( @@ -1998,8 +2017,10 @@ macro_rules! default_imp_for_frm_record_return { }; } +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl api::FraudCheckRecordReturn for connector::DummyConnector {} +#[cfg(feature = "frm")] #[cfg(feature = "dummy_connector")] impl services::ConnectorIntegration< @@ -2010,6 +2031,7 @@ impl { } +#[cfg(feature = "frm")] default_imp_for_frm_record_return!( connector::Aci, connector::Adyen, diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index f3124692773b..d0097da733d7 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -28,11 +28,13 @@ use super::{cache::*, health::*}; use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*}; #[cfg(feature = "oltp")] use super::{ephemeral_key::*, payment_methods::*, webhooks::*}; +#[cfg(all(feature = "frm", feature = "oltp"))] +use crate::routes::fraud_check as frm_routes; use crate::{ configs::settings, db::{StorageImpl, StorageInterface}, events::{event_logger::EventLogger, EventHandler}, - routes::{cards_info::card_iin_info, fraud_check as frm_routes}, + routes::cards_info::card_iin_info, services::get_store, }; @@ -578,6 +580,7 @@ impl Webhooks { pub fn server(config: AppState) -> Scope { use api_models::webhooks as webhook_type; + #[allow(unused_mut)] let mut route = web::scope("/webhooks") .app_data(web::Data::new(config)) .service( diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 8c7f8a141abb..0d1a3e74e225 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -22,9 +22,11 @@ use std::{fmt::Debug, str::FromStr}; use api_models::payment_methods::{SurchargeDetailsResponse, SurchargeMetadata}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "frm")] +pub use self::fraud_check::*; pub use self::{ - admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, fraud_check::*, - payment_link::*, payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, + admin::*, api_keys::*, configs::*, customers::*, disputes::*, files::*, payment_link::*, + payment_methods::*, payments::*, payouts::*, refunds::*, webhooks::*, }; use super::ErrorResponse; use crate::{ @@ -405,6 +407,20 @@ impl ConnectorData { } } +#[cfg(feature = "frm")] +pub trait FraudCheck: + ConnectorCommon + + FraudCheckSale + + FraudCheckTransaction + + FraudCheckCheckout + + FraudCheckFulfillment + + FraudCheckRecordReturn +{ +} + +#[cfg(not(feature = "frm"))] +pub trait FraudCheck {} + #[cfg(test)] mod test { #![allow(clippy::unwrap_used)] diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs index 8597ae0702e4..7be60bfee952 100644 --- a/crates/router/src/types/api/fraud_check.rs +++ b/crates/router/src/types/api/fraud_check.rs @@ -4,7 +4,7 @@ use api_models::enums; use common_utils::errors::CustomResult; use error_stack::{IntoReport, ResultExt}; -use super::{BoxedConnector, ConnectorCommon, ConnectorData, SessionConnectorData}; +use super::{BoxedConnector, ConnectorData, SessionConnectorData}; use crate::{ connector, core::errors, @@ -15,16 +15,6 @@ use crate::{ }, }; -pub trait FraudCheck: - ConnectorCommon - + FraudCheckSale - + FraudCheckTransaction - + FraudCheckCheckout - + FraudCheckFulfillment - + FraudCheckRecordReturn -{ -} - #[derive(Debug, Clone)] pub struct Sale; diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index fd5331b30d1e..4878a152a3c0 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -256,7 +256,7 @@ pub enum Flow { /// Retrieve Decision Manager Config DecisionManagerRetrieveConfig, #[cfg(feature = "frm")] - /// Manual payment fullfilment acknowledgement + /// Manual payment fulfillment acknowledgement FrmFulfillment, } From 319a0ccbf94a988fc9eb6eaae20cfebcb29a6594 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 30 Nov 2023 15:29:23 +0530 Subject: [PATCH 3/5] refactor: move signifyd base_url to configs --- config/config.example.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/router/src/configs/settings.rs | 1 + crates/router/src/connector/signifyd.rs | 4 ++-- loadtest/config/development.toml | 1 + 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index 0cce6f731bcb..f71c1ecae1c7 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -215,6 +215,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" diff --git a/config/development.toml b/config/development.toml index 2defdbf1a451..24984f0b4f36 100644 --- a/config/development.toml +++ b/config/development.toml @@ -189,6 +189,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index cbd88b560072..f23e8ab77bc8 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -129,6 +129,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 56f488bf9193..e54c6aff0af2 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -609,6 +609,7 @@ pub struct Connectors { pub prophetpay: ConnectorParams, pub rapyd: ConnectorParams, pub shift4: ConnectorParams, + pub signifyd: ConnectorParams, pub square: ConnectorParams, pub stax: ConnectorParams, pub stripe: ConnectorParamsWithFileUploadUrl, diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs index eefce05ddbe2..5d9714e4d945 100644 --- a/crates/router/src/connector/signifyd.rs +++ b/crates/router/src/connector/signifyd.rs @@ -53,8 +53,8 @@ impl ConnectorCommon for Signifyd { fn common_get_content_type(&self) -> &'static str { "application/json" } - fn base_url<'a>(&self, _connectors: &'a settings::Connectors) -> &'a str { - "https://api.signifyd.com" + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.signifyd.base_url.as_ref() } fn get_auth_header( diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 3938c1f162a0..c9c41b384651 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -115,6 +115,7 @@ powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" shift4.base_url = "https://api.shift4.com/" +signifyd.base_url = "https://api.signifyd.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" stax.base_url = "https://apiprod.fattlabs.com/" From 1ddfd7f6911d4d99a63ce416ef5f4d9b8615aa2b Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 4 Dec 2023 20:03:54 +0530 Subject: [PATCH 4/5] refactor(frm): handle vec indexing for FrmConfigs --- config/config.example.toml | 2 +- config/development.toml | 2 +- config/docker_compose.toml | 2 +- crates/router/src/configs/settings.rs | 2 +- crates/router/src/core/fraud_check.rs | 46 ++++++++++-------------- crates/router/src/core/payments.rs | 2 +- crates/router/src/core/payments/flows.rs | 30 ++++++---------- crates/router/src/types/transformers.rs | 2 +- crates/router_env/Cargo.toml | 3 +- crates/router_env/src/logger/types.rs | 1 - loadtest/config/development.toml | 2 +- 11 files changed, 37 insertions(+), 57 deletions(-) diff --git a/config/config.example.toml b/config/config.example.toml index f71c1ecae1c7..c5ec4216e8be 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -480,4 +480,4 @@ connection_timeout = 10 # Timeout for database connection in seconds ttl = 900 [frm] -is_frm_enabled = true +enabled = true diff --git a/config/development.toml b/config/development.toml index 24984f0b4f36..ea85d1262821 100644 --- a/config/development.toml +++ b/config/development.toml @@ -478,7 +478,7 @@ delay_between_retries_in_milliseconds = 500 ttl = 900 # 15 * 60 seconds [frm] -is_frm_enabled = true +enabled = true [events] source = "logs" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index f23e8ab77bc8..df7d244e82ea 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -365,4 +365,4 @@ queue_strategy = "Fifo" ttl = 900 # 15 * 60 seconds [frm] -is_frm_enabled = true +enabled = true diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index e54c6aff0af2..059bdd1eee88 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -123,7 +123,7 @@ pub struct Settings { #[cfg(feature = "frm")] #[derive(Debug, Deserialize, Clone, Default)] pub struct Frm { - pub is_frm_enabled: bool, + pub enabled: bool, } #[derive(Debug, Deserialize, Clone)] diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index 0d2a10c7d25c..55bd22baeec4 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -167,7 +167,7 @@ where .parse_value("FrmConfigs") .change_context(errors::ApiErrorResponse::InvalidDataFormat { field_name: "frm_configs".to_string(), - expected_format: "[{ \"gateway\": \"stripe\", \"payment_methods\": [{ \"payment_method\": \"card\",\"payment_method_types\": [{\"payment_method_type\": \"credit\",\"card_networks\": [\"Visa\"],\"flow\": \"pre\",\"action\": \"cancel_txn\"}]}]}]".to_string(), + expected_format: r#"[{ "gateway": "stripe", "payment_methods": [{ "payment_method": "card","payment_method_types": [{"payment_method_type": "credit","card_networks": ["Visa"],"flow": "pre","action": "cancel_txn"}]}]}]"#.to_string(), }) }) .collect::, _>>()?; @@ -298,32 +298,24 @@ where // filtered_frm_config... // Panic Safety: we are first checking if the object is present... only if present, we try to fetch index 0 let frm_configs_object = FrmConfigsObject { - frm_enabled_gateway: if filtered_frm_config.is_empty() { - None - } else { - filtered_frm_config[0].gateway - }, - frm_enabled_pm: if filtered_payment_methods.is_empty() { - None - } else { - filtered_payment_methods[0].payment_method - }, - frm_enabled_pm_type: if filtered_payment_method_types.is_empty() { - None - } else { - filtered_payment_method_types[0].payment_method_type - }, - frm_action: if filtered_payment_method_types.is_empty() { - api_enums::FrmAction::ManualReview - } else { - filtered_payment_method_types[0].clone().action - }, - frm_preferred_flow_type: if filtered_payment_method_types.is_empty() - { - api_enums::FrmPreferredFlowTypes::Pre - } else { - filtered_payment_method_types[0].clone().flow - }, + frm_enabled_gateway: filtered_frm_config + .get(0) + .and_then(|c| c.gateway), + frm_enabled_pm: filtered_payment_methods + .get(0) + .and_then(|pm| pm.payment_method), + frm_enabled_pm_type: filtered_payment_method_types + .get(0) + .and_then(|pmt| pmt.payment_method_type), + frm_action: filtered_payment_method_types + // .clone() + .get(0) + .map(|pmt| pmt.action.clone()) + .unwrap_or(api_enums::FrmAction::ManualReview), + frm_preferred_flow_type: filtered_payment_method_types + .get(0) + .map(|pmt| pmt.flow.clone()) + .unwrap_or(api_enums::FrmPreferredFlowTypes::Pre), }; logger::debug!( "frm_routing_configs: {:?} {:?} {:?} {:?}", diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 993c2abf4328..c1b7c8deb501 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -185,7 +185,7 @@ where #[allow(unused_variables, unused_mut)] let mut should_continue_transaction: bool = true; #[cfg(feature = "frm")] - let frm_configs = if state.conf.frm.is_frm_enabled { + let frm_configs = if state.conf.frm.enabled { frm_core::call_frm_before_connector_call( db, &operation, diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 73ddf44f47d8..b606cc7b2be2 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -1679,11 +1679,9 @@ macro_rules! default_imp_for_frm_sale { }; } -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl api::FraudCheckSale for connector::DummyConnector {} -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl services::ConnectorIntegration< api::Sale, @@ -1763,11 +1761,9 @@ macro_rules! default_imp_for_frm_checkout { }; } -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl api::FraudCheckCheckout for connector::DummyConnector {} -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl services::ConnectorIntegration< api::Checkout, @@ -1847,11 +1843,9 @@ macro_rules! default_imp_for_frm_transaction { }; } -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl api::FraudCheckTransaction for connector::DummyConnector {} -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl services::ConnectorIntegration< api::Transaction, @@ -1931,11 +1925,9 @@ macro_rules! default_imp_for_frm_fulfillment { }; } -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl api::FraudCheckFulfillment for connector::DummyConnector {} -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl services::ConnectorIntegration< api::Fulfillment, @@ -2015,11 +2007,9 @@ macro_rules! default_imp_for_frm_record_return { }; } -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl api::FraudCheckRecordReturn for connector::DummyConnector {} -#[cfg(feature = "frm")] -#[cfg(feature = "dummy_connector")] +#[cfg(all(feature = "frm", feature = "dummy_connector"))] impl services::ConnectorIntegration< api::RecordReturn, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 99096864a000..abc658be8648 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -747,7 +747,7 @@ impl TryFrom for api_models::admin::MerchantCo .parse_value("FrmConfigs") .change_context(errors::ApiErrorResponse::InvalidDataFormat { field_name: "frm_configs".to_string(), - expected_format: "[{ \"gateway\": \"stripe\", \"payment_methods\": [{ \"payment_method\": \"card\",\"payment_method_types\": [{\"payment_method_type\": \"credit\",\"card_networks\": [\"Visa\"],\"flow\": \"pre\",\"action\": \"cancel_txn\"}]}]}]".to_string(), + expected_format: r#"[{ "gateway": "stripe", "payment_methods": [{ "payment_method": "card","payment_method_types": [{"payment_method_type": "credit","card_networks": ["Visa"],"flow": "pre","action": "cancel_txn"}]}]}]"#.to_string(), }) }) .collect::, _>>()?; diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index 614bec49d937..266baf0e3863 100644 --- a/crates/router_env/Cargo.toml +++ b/crates/router_env/Cargo.toml @@ -38,10 +38,9 @@ cargo_metadata = "0.15.4" vergen = { version = "8.2.1", features = ["cargo", "git", "git2", "rustc"], optional = true } [features] -default = ["actix_web", "payouts", "frm"] +default = ["actix_web", "payouts"] actix_web = ["tracing-actix-web"] log_custom_entries_to_extra = [] log_extra_implicit_fields = [] log_active_span_json = [] payouts = [] -frm = [] diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 1f31c10ed76e..4b2f0f60087d 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -257,7 +257,6 @@ pub enum Flow { DecisionManagerDeleteConfig, /// Retrieve Decision Manager Config DecisionManagerRetrieveConfig, - #[cfg(feature = "frm")] /// Manual payment fulfillment acknowledgement FrmFulfillment, /// Change password flow diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index c9c41b384651..fdce0fa34d16 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -265,4 +265,4 @@ connection_timeout = 10 ttl = 300 # 5 * 60 seconds [frm] -is_frm_enabled = true +enabled = true From 8f1b3c2c8b09afcff38cbb54c2781ec7834a2dd9 Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 4 Dec 2023 21:24:04 +0530 Subject: [PATCH 5/5] refactor(frm): handle matching of signifyd's status --- crates/router/src/connector/signifyd/transformers/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index 126d97ec9cba..1a1b09bd2880 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -175,7 +175,7 @@ impl From for storage_enums::FraudCheckStatus { SignifydPaymentStatus::Accept => Self::Legit, SignifydPaymentStatus::Reject => Self::Fraud, SignifydPaymentStatus::Hold => Self::ManualReview, - _ => Self::Pending, + SignifydPaymentStatus::Challenge | SignifydPaymentStatus::Credit => Self::Pending, } } }