From 652ed1820a4820634fe9635306eabdd2d46b6b70 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 10 Oct 2023 12:09:22 +0530 Subject: [PATCH 01/46] add riskified frm connector --- crates/api_models/src/enums.rs | 1 + crates/router/src/configs/settings.rs | 1 + crates/router/src/core/admin.rs | 4 +++- crates/router/src/types/api.rs | 4 +++- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 33cde866eed1..ffa2b9ff1241 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -121,6 +121,7 @@ pub enum Connector { Zen, Signifyd, Plaid, + Riskified, } impl Connector { diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index fd4a398fd568..731bb657519b 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -543,6 +543,7 @@ pub struct Connectors { pub worldline: ConnectorParams, pub worldpay: ConnectorParams, pub zen: ConnectorParams, + pub riskified: ConnectorParams, } #[derive(Debug, Deserialize, Clone, Default, router_derive::ConfigValidate)] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index ec229bd8a564..4571d8a92968 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -1426,7 +1426,9 @@ pub(crate) fn validate_auth_type( zen::transformers::ZenAuthType::try_from(val)?; Ok(()) } - api_enums::Connector::Signifyd | api_enums::Connector::Plaid => { + api_enums::Connector::Signifyd + | api_enums::Connector::Plaid + | api_enums::Connector::Riskified => { Err(report!(errors::ConnectorError::InvalidConnectorName) .attach_printable(format!("invalid connector name: {connector_name}"))) } diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index 27d86db831fe..3e02bc85ee22 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -356,7 +356,9 @@ impl ConnectorData { enums::Connector::Trustpay => Ok(Box::new(&connector::Trustpay)), enums::Connector::Tsys => Ok(Box::new(&connector::Tsys)), enums::Connector::Zen => Ok(Box::new(&connector::Zen)), - enums::Connector::Signifyd | enums::Connector::Plaid => { + enums::Connector::Signifyd + | enums::Connector::Plaid + | enums::Connector::Riskified => { Err(report!(errors::ConnectorError::InvalidConnectorName) .attach_printable(format!("invalid connector name: {connector_name}"))) .change_context(errors::ApiErrorResponse::InternalServerError) From 5b250316c7eaeb76876590d15b4a9bbfdb5d5645 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 10 Oct 2023 16:53:31 +0530 Subject: [PATCH 02/46] add a constant content_length --- crates/router/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 738646b2964b..ba4a8f43d149 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -66,6 +66,7 @@ pub mod headers { pub const X_WEBHOOK_SIGNATURE: &str = "X-Webhook-Signature-512"; pub const X_REQUEST_ID: &str = "X-Request-Id"; pub const STRIPE_COMPATIBLE_WEBHOOK_SIGNATURE: &str = "Stripe-Signature"; + pub const CONTENT_LENGTH: &str = "Content-Length"; } pub mod pii { From e0b0ea4db8d73e671972d869df8a1d2cb1ab2b92 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:06:35 +0000 Subject: [PATCH 03/46] docs(openapi): re-generate OpenAPI specification --- openapi/openapi_spec.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 56632a68c39b..3456746b8901 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -3878,7 +3878,8 @@ "worldpay", "zen", "signifyd", - "plaid" + "plaid", + "riskified" ] }, "ConnectorMetadata": { From f76073315ae85e7de157903d90222714bd8b477e Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 17 Oct 2023 12:45:20 +0530 Subject: [PATCH 04/46] add frm_metadata --- config/config.example.toml | 1 + config/development.toml | 1 + config/docker_compose.toml | 1 + crates/api_models/src/payments.rs | 9 +++++++++ crates/router/src/core/payments.rs | 1 + crates/router/src/core/payments/helpers.rs | 1 + .../src/core/payments/operations/payment_approve.rs | 1 + .../src/core/payments/operations/payment_cancel.rs | 1 + .../src/core/payments/operations/payment_capture.rs | 2 ++ .../payments/operations/payment_complete_authorize.rs | 1 + .../src/core/payments/operations/payment_confirm.rs | 1 + .../src/core/payments/operations/payment_create.rs | 1 + .../core/payments/operations/payment_method_validate.rs | 1 + .../src/core/payments/operations/payment_reject.rs | 1 + .../src/core/payments/operations/payment_session.rs | 1 + .../router/src/core/payments/operations/payment_start.rs | 1 + .../src/core/payments/operations/payment_status.rs | 1 + .../src/core/payments/operations/payment_update.rs | 1 + crates/router/src/core/payments/transformers.rs | 2 ++ crates/router/src/core/utils.rs | 7 +++++++ crates/router/src/core/webhooks/utils.rs | 1 + crates/router/src/types.rs | 4 ++++ crates/router/tests/connectors/payme.rs | 4 ++++ crates/router/tests/connectors/zen.rs | 4 ++++ 24 files changed, 49 insertions(+) diff --git a/config/config.example.toml b/config/config.example.toml index b69519b6ac4a..c7644cabbf7b 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -200,6 +200,7 @@ paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" diff --git a/config/development.toml b/config/development.toml index 75a5a89ca9dc..bcfe5a5fbb9b 100644 --- a/config/development.toml +++ b/config/development.toml @@ -171,6 +171,7 @@ paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" diff --git a/config/docker_compose.toml b/config/docker_compose.toml index b1483327ee0c..acc30a88e0fd 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -115,6 +115,7 @@ paypal.base_url = "https://api-m.sandbox.paypal.com/" payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 01edef87a67a..9c64153f5113 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -302,6 +302,10 @@ pub struct PaymentsRequest { /// The type of the payment that differentiates between normal and various types of mandate payments #[schema(value_type = Option)] pub payment_type: Option, + + /// additional data related to some frm connectors + pub frm_metadata: Option, + } #[derive(Default, Debug, Clone, Copy)] @@ -2243,6 +2247,8 @@ pub struct OrderDetailsWithAmount { pub quantity: u16, /// the amount per quantity of product pub amount: i64, + // Does the order includes shipping + pub requires_shipping: bool, } #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] @@ -2253,6 +2259,9 @@ pub struct OrderDetails { /// The quantity of the product to be purchased #[schema(example = 1)] pub quantity: u16, + // Does the order include shipping + pub requires_shipping: bool, + } #[derive(Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index d65e53c95ba9..817c76c7e474 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1461,6 +1461,7 @@ where pub ephemeral_key: Option, pub redirect_response: Option, pub frm_message: Option, + pub frm_metadata: Option, } #[derive(Debug, Default, Clone)] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index a6e3b2f12785..92f8b594ab16 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2659,6 +2659,7 @@ pub fn router_data_type_conversion( connector_http_status_code: router_data.connector_http_status_code, external_latency: router_data.external_latency, apple_pay_flow: router_data.apple_pay_flow, + frm_metadata: router_data.frm_metadata, } } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index d7b3d743b959..876df511e692 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -252,6 +252,7 @@ impl multiple_capture_data: None, redirect_response, frm_message: frm_response.ok(), + frm_metadata: request.frm_metadata.clone(), }, Some(CustomerDetails { customer_id: request.customer_id.clone(), diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 72006946c207..5a96bb34b1df 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -172,6 +172,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: None, + frm_metadata: None, }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index bd64ebac6322..4cb5dea80944 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -230,6 +230,8 @@ impl multiple_capture_data, redirect_response: None, frm_message: None, + frm_metadata: None, + }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index db2c9e27c9b9..361c90be9b36 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -247,6 +247,7 @@ impl multiple_capture_data: None, redirect_response, frm_message: None, + frm_metadata: request.frm_metadata.clone(), }, Some(CustomerDetails { customer_id: request.customer_id.clone(), diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 761264e4798e..08a8d3dda97d 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -360,6 +360,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: None, + frm_metadata: request.frm_metadata.clone(), }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 479b0e2cceea..51890001009a 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -295,6 +295,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: None, + frm_metadata: request.frm_metadata.clone(), }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index 0ff49279f3c8..c30efcf8a119 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -196,6 +196,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: None, + frm_metadata: None, }, Some(payments::CustomerDetails { customer_id: request.customer_id.clone(), diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index c9a24b8fb840..22d0224d3519 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -158,6 +158,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: frm_response.ok(), + frm_metadata: None }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 261275296f14..31a1a91f0150 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -192,6 +192,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: None, + frm_metadata: None, }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 07015810039d..ad99ae1b0831 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -167,6 +167,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: None, + frm_metadata: None, }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 6e0ef2f4bac7..133f4084c7b1 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -403,6 +403,7 @@ async fn get_tracker_for_sync< multiple_capture_data, redirect_response: None, frm_message: frm_response.ok(), + frm_metadata: None, }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 9e0ef76d3e7f..2a8429c825f6 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -346,6 +346,7 @@ impl multiple_capture_data: None, redirect_response: None, frm_message: None, + frm_metadata: request.frm_metadata.clone(), }, Some(customer_details), )) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index e1ea8a063592..b212cb8055b1 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -163,6 +163,7 @@ where connector_http_status_code: None, external_latency: None, apple_pay_flow, + frm_metadata: None, }; Ok(router_data) @@ -912,6 +913,7 @@ pub fn change_order_details_to_new_type( product_name: order_details.product_name, quantity: order_details.quantity, amount: order_amount, + requires_shipping: order_details.requires_shipping, }]) } diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 908bd1438762..2f901771c5fe 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -189,6 +189,7 @@ pub async fn construct_payout_router_data<'a, F>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) @@ -329,6 +330,7 @@ pub async fn construct_refund_router_data<'a, F>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None }; Ok(router_data) @@ -557,6 +559,7 @@ pub async fn construct_accept_dispute_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } @@ -643,6 +646,7 @@ pub async fn construct_submit_evidence_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } @@ -734,6 +738,7 @@ pub async fn construct_upload_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None }; Ok(router_data) } @@ -823,6 +828,7 @@ pub async fn construct_defend_dispute_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None }; Ok(router_data) } @@ -905,6 +911,7 @@ pub async fn construct_retrieve_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None }; Ok(router_data) } diff --git a/crates/router/src/core/webhooks/utils.rs b/crates/router/src/core/webhooks/utils.rs index ac7e5081c823..320f6baf9057 100644 --- a/crates/router/src/core/webhooks/utils.rs +++ b/crates/router/src/core/webhooks/utils.rs @@ -120,6 +120,7 @@ pub async fn construct_webhook_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, }; Ok(router_data) } diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 560d706a8403..8fbcdc0f01b3 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -292,6 +292,8 @@ pub struct RouterData { pub external_latency: Option, /// Contains apple pay flow type simplified or manual pub apple_pay_flow: Option, + + pub frm_metadata: Option, } #[derive(Debug, Clone, serde::Deserialize)] @@ -1119,6 +1121,7 @@ impl From<(&RouterData, T2)> connector_http_status_code: data.connector_http_status_code, external_latency: data.external_latency, apple_pay_flow: data.apple_pay_flow.clone(), + frm_metadata: data.frm_metadata.clone(), } } } @@ -1174,6 +1177,7 @@ impl connector_http_status_code: data.connector_http_status_code, external_latency: data.external_latency, apple_pay_flow: None, + frm_metadata: None, } } } diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 67e7919caf75..880de1c44c79 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -76,6 +76,7 @@ fn payment_method_details() -> Option { product_name: "iphone 13".to_string(), quantity: 1, amount: 1000, + requires_shipping: false, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -370,6 +371,7 @@ async fn should_fail_payment_for_incorrect_cvc() { product_name: "iphone 13".to_string(), quantity: 1, amount: 100, + requires_shipping: false, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -402,6 +404,7 @@ async fn should_fail_payment_for_invalid_exp_month() { product_name: "iphone 13".to_string(), quantity: 1, amount: 100, + requires_shipping: false }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -434,6 +437,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { product_name: "iphone 13".to_string(), quantity: 1, amount: 100, + requires_shipping: false, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index ca71fd0c6221..bd65520ac346 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -313,6 +313,7 @@ async fn should_fail_payment_for_incorrect_card_number() { product_name: "test".to_string(), quantity: 1, amount: 1000, + requires_shipping: false, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -348,6 +349,7 @@ async fn should_fail_payment_for_incorrect_cvc() { product_name: "test".to_string(), quantity: 1, amount: 1000, + requires_shipping: false, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -383,6 +385,7 @@ async fn should_fail_payment_for_invalid_exp_month() { product_name: "test".to_string(), quantity: 1, amount: 1000, + requires_shipping: false }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -418,6 +421,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { product_name: "test".to_string(), quantity: 1, amount: 1000, + requires_shipping: false, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), From f9cd5123690faa67a8b01ac42ed5561e426e7f3f Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 17 Oct 2023 17:11:37 +0530 Subject: [PATCH 05/46] remove const CONTENT_LENGTH --- crates/router/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index ba4a8f43d149..738646b2964b 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -66,7 +66,6 @@ pub mod headers { pub const X_WEBHOOK_SIGNATURE: &str = "X-Webhook-Signature-512"; pub const X_REQUEST_ID: &str = "X-Request-Id"; pub const STRIPE_COMPATIBLE_WEBHOOK_SIGNATURE: &str = "Stripe-Signature"; - pub const CONTENT_LENGTH: &str = "Content-Length"; } pub mod pii { From 76ca6e5b3b08a656724ad83e28defd632938c54e Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 17 Oct 2023 17:42:31 +0530 Subject: [PATCH 06/46] resolve conflicts --- crates/api_models/src/payments.rs | 9 +-------- .../src/core/payments/operations/payment_capture.rs | 1 - .../src/core/payments/operations/payment_reject.rs | 2 +- crates/router/src/core/utils.rs | 8 ++++---- crates/router/tests/connectors/payme.rs | 4 ++++ crates/router/tests/connectors/zen.rs | 2 +- 6 files changed, 11 insertions(+), 15 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 83cd45c226e4..f8d3e07afd1c 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -309,16 +309,9 @@ pub struct PaymentsRequest { /// The type of the payment that differentiates between normal and various types of mandate payments #[schema(value_type = Option)] pub payment_type: Option, - + /// additional data related to some frm connectors pub frm_metadata: Option, - -} - -#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema)] -pub struct RequestSurchargeDetails { - pub surcharge_amount: i64, - pub tax_amount: Option, } #[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, Copy, ToSchema)] diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index cfdb30b406b8..a31fd08b9fcb 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -234,7 +234,6 @@ impl frm_message: None, payment_link_data: None, frm_metadata: None, - }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 443067747def..75ad4364acd3 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -160,7 +160,7 @@ impl surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, - frm_metadata: None + frm_metadata: None, }, None, )) diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index 9f85b6eedf4b..4a97e8619673 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -336,7 +336,7 @@ pub async fn construct_refund_router_data<'a, F>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, - frm_metadata: None + frm_metadata: None, }; Ok(router_data) @@ -744,7 +744,7 @@ pub async fn construct_upload_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, - frm_metadata: None + frm_metadata: None, }; Ok(router_data) } @@ -834,7 +834,7 @@ pub async fn construct_defend_dispute_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, - frm_metadata: None + frm_metadata: None, }; Ok(router_data) } @@ -917,7 +917,7 @@ pub async fn construct_retrieve_file_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, - frm_metadata: None + frm_metadata: None, }; Ok(router_data) } diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index ad722dd1bd91..e64904b158f4 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -406,6 +406,8 @@ async fn should_fail_payment_for_invalid_exp_month() { product_name: "iphone 13".to_string(), quantity: 1, amount: 100, + product_img_link: None, + requires_shipping: false, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -438,6 +440,8 @@ async fn should_fail_payment_for_incorrect_expiry_year() { product_name: "iphone 13".to_string(), quantity: 1, amount: 100, + product_img_link: None, + requires_shipping: false, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index dc4dd5d621ba..bc079479f190 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -388,7 +388,7 @@ async fn should_fail_payment_for_invalid_exp_month() { quantity: 1, amount: 1000, product_img_link: None, - requires_shipping: false + requires_shipping: false, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), From b96fcd8cb6597ddbd473bc979c196e61ea37c864 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 12:26:34 +0000 Subject: [PATCH 07/46] docs(openapi): re-generate OpenAPI specification --- openapi/openapi_spec.json | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index c2ec04add9db..62580e2beac4 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7323,7 +7323,8 @@ "type": "object", "required": [ "product_name", - "quantity" + "quantity", + "requires_shipping" ], "properties": { "product_name": { @@ -7339,6 +7340,9 @@ "example": 1, "minimum": 0 }, + "requires_shipping": { + "type": "boolean" + }, "product_img_link": { "type": "string", "description": "The image URL of the product", @@ -7351,7 +7355,8 @@ "required": [ "product_name", "quantity", - "amount" + "amount", + "requires_shipping" ], "properties": { "product_name": { @@ -7372,6 +7377,9 @@ "format": "int64", "description": "the amount per quantity of product" }, + "requires_shipping": { + "type": "boolean" + }, "product_img_link": { "type": "string", "description": "The image URL of the product", @@ -8974,6 +8982,10 @@ } ], "nullable": true + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", + "nullable": true } } }, @@ -9338,6 +9350,10 @@ } ], "nullable": true + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", + "nullable": true } } }, From 0a7025634de4469ee7b10bdce9dac51f154f2599 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 17 Oct 2023 20:00:03 +0530 Subject: [PATCH 08/46] fix clippy error --- crates/router/tests/connectors/aci.rs | 2 ++ crates/router/tests/connectors/utils.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 4fe36f36871c..5cb28d5d4bdc 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -69,6 +69,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { complete_authorize_url: None, customer_id: None, surcharge_details: None, + frm_metadata: None, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, @@ -152,6 +153,7 @@ fn construct_refund_router_data() -> types::RefundsRouterData { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, } } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index 1cb3b48f72d5..f0445008cf77 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -511,6 +511,7 @@ pub trait ConnectorActions: Connector { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, } } From c95511e648f84544d0902b145f7243ed87a5ea32 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 18 Oct 2023 14:34:51 +0000 Subject: [PATCH 09/46] chore(version): v1.61.0 --- CHANGELOG.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d519b8be9fee..3a0cc5458623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,74 @@ All notable changes to HyperSwitch will be documented here. - - - +## 1.61.0 (2023-10-18) + +### Features + +- **Connector:** [Paypal] add support for dispute webhooks for paypal connector ([#2353](https://github.com/juspay/hyperswitch/pull/2353)) ([`6cf8f05`](https://github.com/juspay/hyperswitch/commit/6cf8f0582cfa4f6a58c67a868cb67846970b3835)) +- **apple_pay:** Add support for decrypted apple pay token for checkout ([#2628](https://github.com/juspay/hyperswitch/pull/2628)) ([`794dbc6`](https://github.com/juspay/hyperswitch/commit/794dbc6a766d12ff3cdf0b782abb4c48b8fa77d0)) +- **connector:** + - [Aci] Update connector_response_reference_id with merchant reference ([#2551](https://github.com/juspay/hyperswitch/pull/2551)) ([`9e450b8`](https://github.com/juspay/hyperswitch/commit/9e450b81ca8bc4b1ddbbe2c1d732dbc58c61934e)) + - [Bambora] use connector_request_reference_id ([#2518](https://github.com/juspay/hyperswitch/pull/2518)) ([`73e9391`](https://github.com/juspay/hyperswitch/commit/73e93910cd3bd668d721b15edb86240adc18f46b)) + - [Tsys] Use connector_request_reference_id as reference to the connector ([#2631](https://github.com/juspay/hyperswitch/pull/2631)) ([`b145463`](https://github.com/juspay/hyperswitch/commit/b1454634259144d896716e5cef37d9b8491f55b9)) +- **core:** Replace temp locker with redis ([#2594](https://github.com/juspay/hyperswitch/pull/2594)) ([`2edbd61`](https://github.com/juspay/hyperswitch/commit/2edbd6123512a6f2f4d51d5c2d1ed8b6ee502813)) +- **events:** Add events for incoming API requests ([#2621](https://github.com/juspay/hyperswitch/pull/2621)) ([`7a76d6c`](https://github.com/juspay/hyperswitch/commit/7a76d6c01a0c6087c6429e58cc9dd6b4ea7fc0aa)) +- **merchant_account:** Add merchant account list endpoint ([#2560](https://github.com/juspay/hyperswitch/pull/2560)) ([`a1472c6`](https://github.com/juspay/hyperswitch/commit/a1472c6b78afa819cbe026a7db1e0c2b9016715e)) +- Update surcharge_amount and tax_amount in update_trackers of payment_confirm ([#2603](https://github.com/juspay/hyperswitch/pull/2603)) ([`2f9a355`](https://github.com/juspay/hyperswitch/commit/2f9a3557f63150bcd27e27c6510a799669706718)) + +### Bug Fixes + +- **connector:** + - [Authorizedotnet]fix error deserialization incase of authentication failure ([#2600](https://github.com/juspay/hyperswitch/pull/2600)) ([`4859b7d`](https://github.com/juspay/hyperswitch/commit/4859b7da73125c2da72f4754863ff4485bebce29)) + - [Paypal]fix error deserelization for source verification call ([#2611](https://github.com/juspay/hyperswitch/pull/2611)) ([`da77d13`](https://github.com/juspay/hyperswitch/commit/da77d1393b8f6ab658dd7f3c202dd6c7d15c0ebd)) +- **payments:** Fix payment update enum being inserted into kv ([#2612](https://github.com/juspay/hyperswitch/pull/2612)) ([`9aa1c75`](https://github.com/juspay/hyperswitch/commit/9aa1c75eca24caa14af5f4801173cd59f76d7e57)) + +### Refactors + +- **events:** Allow box dyn for event handler ([#2629](https://github.com/juspay/hyperswitch/pull/2629)) ([`01410bb`](https://github.com/juspay/hyperswitch/commit/01410bb9f233637e98f27ebe509e859c7dad2cf4)) +- **payment_connector:** Allow connector label to be updated ([#2622](https://github.com/juspay/hyperswitch/pull/2622)) ([`c86ac9b`](https://github.com/juspay/hyperswitch/commit/c86ac9b1fe5388666463aa16c899427a2bf442fb)) +- **router:** Remove unnecessary function from Refunds Validate Flow ([#2609](https://github.com/juspay/hyperswitch/pull/2609)) ([`3399328`](https://github.com/juspay/hyperswitch/commit/3399328ae7f525fb72e0751182cf32d0b2470594)) +- Refactor connector auth type failure to 4xx ([#2616](https://github.com/juspay/hyperswitch/pull/2616)) ([`1dad745`](https://github.com/juspay/hyperswitch/commit/1dad7455c4ae8d26d52c44d90f5b8d815d85d205)) + +### Testing + +- **postman:** Update postman collection files ([`d899025`](https://github.com/juspay/hyperswitch/commit/d89902507486b8b97011fb63ed0343f727255ca2)) + +### Documentation + +- **postman:** Rewrite postman documentation to help devs develop tests for their features ([#2613](https://github.com/juspay/hyperswitch/pull/2613)) ([`1548ee6`](https://github.com/juspay/hyperswitch/commit/1548ee62b661200fcb9d439d16c072a66dbfa718)) + +### Miscellaneous Tasks + +- **scripts:** Add connector script changes ([#2620](https://github.com/juspay/hyperswitch/pull/2620)) ([`373a10b`](https://github.com/juspay/hyperswitch/commit/373a10beffc7cddef6ff76f5c8fff91ca3618581)) + +**Full Changelog:** [`v1.60.0...v1.61.0`](https://github.com/juspay/hyperswitch/compare/v1.60.0...v1.61.0) + +- - - + + +## 1.60.0 (2023-10-17) + +### Features + +- **compatibility:** Added support to connector txn id ([#2606](https://github.com/juspay/hyperswitch/pull/2606)) ([`82980a8`](https://github.com/juspay/hyperswitch/commit/82980a86ad7966c6645d26a4abec85c8c7e3bdad)) +- **router:** Better UI payment link and order details product image and merchant config support ([#2583](https://github.com/juspay/hyperswitch/pull/2583)) ([`fdd9580`](https://github.com/juspay/hyperswitch/commit/fdd95800127bb79fe2a9eeca1b7e0e158b6d2783)) +- Add updated_by to tracker tables ([#2604](https://github.com/juspay/hyperswitch/pull/2604)) ([`6a74e8c`](https://github.com/juspay/hyperswitch/commit/6a74e8cba9078529fd9662d29ac7b941a191fbf4)) + +### Bug Fixes + +- Make push to drainer generic and add application metrics for KV ([#2563](https://github.com/juspay/hyperswitch/pull/2563)) ([`274a783`](https://github.com/juspay/hyperswitch/commit/274a78343e5e3de614cfb1476570b5c449ee0c1e)) + +### Refactors + +- **connector:** [Nuvei] remove default case handling ([#2584](https://github.com/juspay/hyperswitch/pull/2584)) ([`3807601`](https://github.com/juspay/hyperswitch/commit/3807601ee1c140310abf7a7e6ee4b83d44de9558)) +- **router:** Throw bad request error on applepay verification failure ([#2607](https://github.com/juspay/hyperswitch/pull/2607)) ([`cecea87`](https://github.com/juspay/hyperswitch/commit/cecea8718a48b4e896b2bafce0f909ef8d9a6e8a)) + +**Full Changelog:** [`v1.59.0...v1.60.0`](https://github.com/juspay/hyperswitch/compare/v1.59.0...v1.60.0) + +- - - + + ## 1.59.0 (2023-10-16) ### Features From f83bf8f66357d3aea47117c77e820bd605850e41 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 20 Oct 2023 11:30:01 +0530 Subject: [PATCH 10/46] add extra field v1 --- crates/api_models/src/payments.rs | 34 +++++++++++++++++-- crates/common_utils/src/request.rs | 4 ++- .../router/src/core/payments/transformers.rs | 4 +++ crates/router/tests/connectors/payme.rs | 6 +++- 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 1d076674c29a..1648249d39d6 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2428,11 +2428,32 @@ pub struct OrderDetailsWithAmount { /// the amount per quantity of product pub amount: i64, // Does the order includes shipping - pub requires_shipping: bool, + pub requires_shipping: Option, /// The image URL of the product pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, } +#[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProductType { + #[default] + Physical, + Digital, + Travel, + Ride, + Event, + Accommodation +} + + #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct OrderDetails { /// Name of the product that is being purchased @@ -2442,10 +2463,17 @@ pub struct OrderDetails { #[schema(example = 1)] pub quantity: u16, // Does the order include shipping - pub requires_shipping: bool, - + pub requires_shipping: Option, /// The image URL of the product pub product_img_link: Option, + /// ID of the product that is being purchased + pub product_id: Option, + /// Category of the product that is being purchased + pub category: Option, + /// Brand of the product that is being purchased + pub brand: Option, + /// Type of the product that is being purchased + pub product_type: Option, } #[derive(Default, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] diff --git a/crates/common_utils/src/request.rs b/crates/common_utils/src/request.rs index 64bce8649d97..911514949bfc 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -198,7 +198,9 @@ impl RequestBody { { #[cfg(feature = "logs")] logger::info!(connector_request_body=?body); - Ok(Self(Secret::new(encoder(body)?))) + let encoder = encoder(body)?; + logger::debug!("aaaaaaaaaaaaaaaaaa {:?}", encoder); + Ok(Self(Secret::new(encoder))) } pub fn get_inner_value(request_body: Self) -> Secret { request_body.0 diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 6ca0318772cf..7cc0ca657c68 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -927,6 +927,10 @@ pub fn change_order_details_to_new_type( amount: order_amount, product_img_link: order_details.product_img_link, requires_shipping: order_details.requires_shipping, + product_id: order_details.product_id, + category: order_details.category, + brand: order_details.brand, + product_type: order_details.product_type, }]) } diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index e64904b158f4..127ecdd6f4af 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -77,7 +77,11 @@ fn payment_method_details() -> Option { quantity: 1, amount: 1000, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), From 309db041b327ea26f6be121ef206e1317942675c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 12:02:01 +0000 Subject: [PATCH 11/46] chore: run formatter --- crates/api_models/src/payments.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 1648249d39d6..e0d127bb1693 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2450,10 +2450,9 @@ pub enum ProductType { Travel, Ride, Event, - Accommodation + Accommodation, } - #[derive(Debug, Default, Eq, PartialEq, serde::Deserialize, serde::Serialize, Clone, ToSchema)] pub struct OrderDetails { /// Name of the product that is being purchased From 43ffa94eafdbae3d5c47681973a4e3fdfbdad9b8 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Thu, 26 Oct 2023 12:00:19 +0530 Subject: [PATCH 12/46] fix clippy errors --- crates/common_utils/src/request.rs | 4 +- crates/router/tests/connectors/aci.rs | 2 +- crates/router/tests/connectors/payme.rs | 18 ++++++-- crates/router/tests/connectors/zen.rs | 24 ++++++++-- openapi/openapi_spec.json | 58 ++++++++++++++++++++++--- 5 files changed, 89 insertions(+), 17 deletions(-) diff --git a/crates/common_utils/src/request.rs b/crates/common_utils/src/request.rs index 911514949bfc..64bce8649d97 100644 --- a/crates/common_utils/src/request.rs +++ b/crates/common_utils/src/request.rs @@ -198,9 +198,7 @@ impl RequestBody { { #[cfg(feature = "logs")] logger::info!(connector_request_body=?body); - let encoder = encoder(body)?; - logger::debug!("aaaaaaaaaaaaaaaaaa {:?}", encoder); - Ok(Self(Secret::new(encoder))) + Ok(Self(Secret::new(encoder(body)?))) } pub fn get_inner_value(request_body: Self) -> Secret { request_body.0 diff --git a/crates/router/tests/connectors/aci.rs b/crates/router/tests/connectors/aci.rs index 5cb28d5d4bdc..69c777d0c74f 100644 --- a/crates/router/tests/connectors/aci.rs +++ b/crates/router/tests/connectors/aci.rs @@ -69,7 +69,6 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { complete_authorize_url: None, customer_id: None, surcharge_details: None, - frm_metadata: None, }, response: Err(types::ErrorResponse::default()), payment_method_id: None, @@ -95,6 +94,7 @@ fn construct_payment_router_data() -> types::PaymentsAuthorizeRouterData { connector_http_status_code: None, apple_pay_flow: None, external_latency: None, + frm_metadata: None, } } diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 127ecdd6f4af..3c3ab938ddf1 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -377,7 +377,11 @@ async fn should_fail_payment_for_incorrect_cvc() { quantity: 1, amount: 100, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -411,7 +415,11 @@ async fn should_fail_payment_for_invalid_exp_month() { quantity: 1, amount: 100, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), @@ -445,7 +453,11 @@ async fn should_fail_payment_for_incorrect_expiry_year() { quantity: 1, amount: 100, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), router_return_url: Some("https://hyperswitch.io".to_string()), webhook_url: Some("https://hyperswitch.io".to_string()), diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index bc079479f190..ce6bcd754bc8 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -314,7 +314,11 @@ async fn should_fail_payment_for_incorrect_card_number() { quantity: 1, amount: 1000, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -351,7 +355,11 @@ async fn should_fail_payment_for_incorrect_cvc() { quantity: 1, amount: 1000, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -388,7 +396,11 @@ async fn should_fail_payment_for_invalid_exp_month() { quantity: 1, amount: 1000, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), @@ -425,7 +437,11 @@ async fn should_fail_payment_for_incorrect_expiry_year() { quantity: 1, amount: 1000, product_img_link: None, - requires_shipping: false, + requires_shipping: None, + product_id: None, + category: None, + brand: None, + product_type: None, }]), email: Some(Email::from_str("test@gmail.com").unwrap()), webhook_url: Some("https://1635-116-74-253-164.ngrok-free.app".to_string()), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 90758efb7bf3..5fce571c27b8 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -7340,8 +7340,7 @@ "type": "object", "required": [ "product_name", - "quantity", - "requires_shipping" + "quantity" ], "properties": { "product_name": { @@ -7358,12 +7357,36 @@ "minimum": 0 }, "requires_shipping": { - "type": "boolean" + "type": "boolean", + "nullable": true }, "product_img_link": { "type": "string", "description": "The image URL of the product", "nullable": true + }, + "product_id": { + "type": "string", + "description": "ID of the product that is being purchased", + "nullable": true + }, + "category": { + "type": "string", + "description": "Category of the product that is being purchased", + "nullable": true + }, + "brand": { + "type": "string", + "description": "Brand of the product that is being purchased", + "nullable": true + }, + "product_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ProductType" + } + ], + "nullable": true } } }, @@ -7372,8 +7395,7 @@ "required": [ "product_name", "quantity", - "amount", - "requires_shipping" + "amount" ], "properties": { "product_name": { @@ -7395,12 +7417,36 @@ "description": "the amount per quantity of product" }, "requires_shipping": { - "type": "boolean" + "type": "boolean", + "nullable": true }, "product_img_link": { "type": "string", "description": "The image URL of the product", "nullable": true + }, + "product_id": { + "type": "string", + "description": "ID of the product that is being purchased", + "nullable": true + }, + "category": { + "type": "string", + "description": "Category of the product that is being purchased", + "nullable": true + }, + "brand": { + "type": "string", + "description": "Brand of the product that is being purchased", + "nullable": true + }, + "product_type": { + "allOf": [ + { + "$ref": "#/components/schemas/ProductType" + } + ], + "nullable": true } } }, From 34b32709e8b4c19edf0d7298d50727048aa2bfb7 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 10 Nov 2023 18:27:24 +0530 Subject: [PATCH 13/46] resolve conflict --- crates/router/src/types/transformers.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 1cd016de18e6..bd54798c2158 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -237,6 +237,12 @@ impl ForeignTryFrom for api_enums::RoutableConnectors { }) .into_report()? } + api_enums::Connector::Riskified => { + Err(common_utils::errors::ValidationError::InvalidValue { + message: "riskified is not a routable connector".to_string(), + }) + .into_report()? + } api_enums::Connector::Square => Self::Square, api_enums::Connector::Stax => Self::Stax, api_enums::Connector::Stripe => Self::Stripe, From 466a244f656d086fd57d63e7b8a7e17155b19366 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Thu, 16 Nov 2023 13:05:21 +0530 Subject: [PATCH 14/46] generate openapi spec --- crates/router/src/openapi.rs | 1 + openapi/openapi_spec.json | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/crates/router/src/openapi.rs b/crates/router/src/openapi.rs index dbcd8cbe4ce2..bad6c22c86a8 100644 --- a/crates/router/src/openapi.rs +++ b/crates/router/src/openapi.rs @@ -254,6 +254,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::SecretInfoToInitiateSdk, api_models::payments::ApplePayPaymentRequest, api_models::payments::AmountInfo, + api_models::payments::ProductType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 894ad863d85e..2aaf21a21f5e 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -10474,6 +10474,17 @@ } } }, + "ProductType": { + "type": "string", + "enum": [ + "physical", + "digital", + "travel", + "ride", + "event", + "accommodation" + ] + }, "ReceiverDetails": { "type": "object", "required": [ From b8a7eea7db1ba8e41fc3b240b071042a5d526443 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 21 Nov 2023 11:39:53 +0530 Subject: [PATCH 15/46] update frm_metadata of complete auth to none --- .../src/core/payments/operations/payment_complete_authorize.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index cfc5393dfa8e..3c4703688f08 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -237,7 +237,7 @@ impl surcharge_details: None, frm_message: None, payment_link_data: None, - frm_metadata: request.frm_metadata.clone(), + frm_metadata: None, }, Some(CustomerDetails { customer_id: request.customer_id.clone(), From 49b0d0ee284f1007f1ef4bfe0bd54066f3caabc9 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Tue, 21 Nov 2023 12:04:11 +0530 Subject: [PATCH 16/46] resolve conflicts --- .../operations/payment_complete_authorize.rs | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 92248b5523ba..a1fff6a1ad9c 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -198,51 +198,6 @@ impl // The operation merges mandate data from both request and payment_attempt let setup_mandate = setup_mandate.map(Into::into); - Ok(( - Box::new(self), - PaymentData { - flow: PhantomData, - payment_intent, - payment_attempt, - currency, - amount, - email: request.email.clone(), - mandate_id: None, - mandate_connector, - setup_mandate, - token, - address: PaymentAddress { - shipping: shipping_address.as_ref().map(|a| a.into()), - billing: billing_address.as_ref().map(|a| a.into()), - }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), - force_sync: None, - refunds: vec![], - disputes: vec![], - attempts: None, - sessions_token: vec![], - card_cvc: request.card_cvc.clone(), - creds_identifier: None, - pm_token: None, - connector_customer_id: None, - recurring_mandate_payment_data, - ephemeral_key: None, - multiple_capture_data: None, - redirect_response, - surcharge_details: None, - frm_message: None, - payment_link_data: None, - frm_metadata: None, - }, - Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }), - )) let profile_id = payment_intent .profile_id .as_ref() @@ -453,4 +408,4 @@ impl ValidateRequest Date: Tue, 21 Nov 2023 06:34:56 +0000 Subject: [PATCH 17/46] chore: run formatter --- .../src/core/payments/operations/payment_complete_authorize.rs | 2 +- crates/router/src/core/payments/operations/payment_create.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index a1fff6a1ad9c..74c95b9aca69 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -408,4 +408,4 @@ impl ValidateRequest frm_message: None, payment_link_data, frm_metadata: request.frm_metadata.clone(), - }; let get_trackers_response = operations::GetTrackerResponse { From 8be82f2af250b5cfac67d75084e64d1d61d97150 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 23 Nov 2023 17:22:28 +0530 Subject: [PATCH 18/46] 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 19/46] 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 c887820ba3b64b409af0005dbdc552bdfa2c688b Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Wed, 29 Nov 2023 13:24:27 +0530 Subject: [PATCH 20/46] update --- crates/router/src/connector/utils.rs | 58 +++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 803c511f3a6b..364a5cc141d9 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -4,6 +4,9 @@ use api_models::{ enums::{CanadaStatesAbbreviation, UsStatesAbbreviation}, payments::{self, BankDebitBilling, OrderDetailsWithAmount}, }; + +use data_models::payments::payment_attempt::PaymentAttempt; + use base64::Engine; use common_utils::{ date_time, @@ -20,13 +23,13 @@ use serde::Serializer; use crate::{ consts, core::{ - errors::{self, CustomResult}, + errors::{self, CustomResult, ApiErrorResponse}, payments::PaymentData, }, pii::PeekInterface, types::{ self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, - PaymentsCancelData, ResponseId, + PaymentsCancelData, ResponseId, BrowserInformation, }, utils::{OptionExt, ValueExt}, }; @@ -65,9 +68,12 @@ pub trait RouterData { fn get_return_url(&self) -> Result; fn get_billing_address(&self) -> Result<&api::AddressDetails, Error>; fn get_shipping_address(&self) -> Result<&api::AddressDetails, Error>; + fn get_billing_address_with_phone_number(&self) -> Result<&api::Address, Error>; + fn get_shipping_address_with_phone_number(&self) -> Result<&api::Address, Error>; fn get_connector_meta(&self) -> Result; fn get_session_token(&self) -> Result; fn to_connector_meta(&self) -> Result + where T: serde::de::DeserializeOwned; fn is_three_ds(&self) -> bool; @@ -174,6 +180,13 @@ impl RouterData for types::RouterData Result<&api::Address, Error> { + self.address + .billing + .as_ref() + .ok_or_else(missing_field_err("billing")) + } fn get_connector_meta(&self) -> Result { self.connector_meta_data .clone() @@ -209,6 +222,14 @@ impl RouterData for types::RouterData Result<&api::Address, Error> { + self.address + .shipping + .as_ref() + .ok_or_else(missing_field_err("shipping")) + } + fn get_payment_method_token(&self) -> Result { self.payment_method_token .clone() @@ -1575,3 +1596,36 @@ pub fn validate_currency( } Ok(()) } + +pub trait AccessPaymentAttemptInfo { + fn get_browser_info(&self) -> Result, error_stack::Report>; + +} + +impl AccessPaymentAttemptInfo for PaymentAttempt { + fn get_browser_info(&self) -> Result, error_stack::Report> { + self + .browser_info + .clone() + .map(|b| b.parse_value("BrowserInformation")) + .transpose() + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + }) + } +} + + +pub trait PaymentsAttemptData { + fn get_browser_info(&self) -> Result; +} + +impl PaymentsAttemptData for PaymentAttempt { + fn get_browser_info(&self) -> Result { + self.browser_info + .clone() + .ok_or_else(missing_field_err("browser_info")) + } + + +} \ No newline at end of file From 53a6ead814bfe45af3860d6c3079c7738158fb4e Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Wed, 29 Nov 2023 15:00:53 +0530 Subject: [PATCH 21/46] add PaymentsAttemptData trait --- crates/router/src/connector/utils.rs | 68 +++++++++++++++------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 364a5cc141d9..1b04a2dcd2e2 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -23,13 +23,13 @@ use serde::Serializer; use crate::{ consts, core::{ - errors::{self, CustomResult, ApiErrorResponse}, + errors::{self, ApiErrorResponse, CustomResult}, payments::PaymentData, }, pii::PeekInterface, types::{ self, api, storage::payment_attempt::PaymentAttemptExt, transformers::ForeignTryFrom, - PaymentsCancelData, ResponseId, BrowserInformation, + BrowserInformation, PaymentsCancelData, ResponseId, }, utils::{OptionExt, ValueExt}, }; @@ -73,7 +73,6 @@ pub trait RouterData { fn get_connector_meta(&self) -> Result; fn get_session_token(&self) -> Result; fn to_connector_meta(&self) -> Result - where T: serde::de::DeserializeOwned; fn is_three_ds(&self) -> bool; @@ -186,7 +185,7 @@ impl RouterData for types::RouterData Result { self.connector_meta_data .clone() @@ -273,7 +272,7 @@ pub trait PaymentsPreProcessingData { fn get_order_details(&self) -> Result, Error>; fn get_webhook_url(&self) -> Result; fn get_return_url(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { @@ -313,7 +312,7 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { .clone() .ok_or_else(missing_field_err("return_url")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -322,14 +321,14 @@ impl PaymentsPreProcessingData for types::PaymentsPreProcessingData { pub trait PaymentsCaptureRequestData { fn is_multiple_capture(&self) -> bool; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsCaptureRequestData for types::PaymentsCaptureData { fn is_multiple_capture(&self) -> bool { self.multiple_capture_data.is_some() } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -337,12 +336,12 @@ impl PaymentsCaptureRequestData for types::PaymentsCaptureData { } pub trait PaymentsSetupMandateRequestData { - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; fn get_email(&self) -> Result; } impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -354,7 +353,7 @@ impl PaymentsSetupMandateRequestData for types::SetupMandateRequestData { pub trait PaymentsAuthorizeRequestData { fn is_auto_capture(&self) -> Result; fn get_email(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; fn get_order_details(&self) -> Result, Error>; fn get_card(&self) -> Result; fn get_return_url(&self) -> Result; @@ -371,11 +370,11 @@ pub trait PaymentsAuthorizeRequestData { } pub trait PaymentMethodTokenizationRequestData { - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentMethodTokenizationRequestData for types::PaymentMethodTokenizationData { - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -393,7 +392,7 @@ impl PaymentsAuthorizeRequestData for types::PaymentsAuthorizeData { fn get_email(&self) -> Result { self.email.clone().ok_or_else(missing_field_err("email")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -499,7 +498,7 @@ pub trait BrowserInformationData { fn get_ip_address(&self) -> Result, Error>; } -impl BrowserInformationData for types::BrowserInformation { +impl BrowserInformationData for BrowserInformation { fn get_ip_address(&self) -> Result, Error> { let ip_address = self .ip_address @@ -607,7 +606,7 @@ pub trait PaymentsCancelRequestData { fn get_amount(&self) -> Result; fn get_currency(&self) -> Result; fn get_cancellation_reason(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl PaymentsCancelRequestData for PaymentsCancelData { @@ -622,7 +621,7 @@ impl PaymentsCancelRequestData for PaymentsCancelData { .clone() .ok_or_else(missing_field_err("cancellation_reason")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -632,7 +631,7 @@ impl PaymentsCancelRequestData for PaymentsCancelData { pub trait RefundsRequestData { fn get_connector_refund_id(&self) -> Result; fn get_webhook_url(&self) -> Result; - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result; } impl RefundsRequestData for types::RefundsData { @@ -648,7 +647,7 @@ impl RefundsRequestData for types::RefundsData { .clone() .ok_or_else(missing_field_err("webhook_url")) } - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result { self.browser_info .clone() .ok_or_else(missing_field_err("browser_info")) @@ -1598,14 +1597,16 @@ pub fn validate_currency( } pub trait AccessPaymentAttemptInfo { - fn get_browser_info(&self) -> Result, error_stack::Report>; - + fn get_browser_info( + &self, + ) -> Result, error_stack::Report>; } impl AccessPaymentAttemptInfo for PaymentAttempt { - fn get_browser_info(&self) -> Result, error_stack::Report> { - self - .browser_info + fn get_browser_info( + &self, + ) -> Result, error_stack::Report> { + self.browser_info .clone() .map(|b| b.parse_value("BrowserInformation")) .transpose() @@ -1615,17 +1616,20 @@ impl AccessPaymentAttemptInfo for PaymentAttempt { } } - pub trait PaymentsAttemptData { - fn get_browser_info(&self) -> Result; + fn get_browser_info(&self) -> Result>; } impl PaymentsAttemptData for PaymentAttempt { - fn get_browser_info(&self) -> Result { + fn get_browser_info(&self) -> Result> { self.browser_info - .clone() - .ok_or_else(missing_field_err("browser_info")) + .clone() + .ok_or(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + })? + .parse_value::("BrowserInformation") + .change_context(ApiErrorResponse::InvalidDataValue { + field_name: "browser_info", + }) } - - -} \ No newline at end of file +} From 3f1971bb32ae7e23eeefb62cf34f33f3b48e2dd5 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Wed, 29 Nov 2023 09:31:48 +0000 Subject: [PATCH 22/46] chore: run formatter --- crates/router/src/connector/utils.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 1b04a2dcd2e2..093e4f0552a5 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -4,15 +4,13 @@ use api_models::{ enums::{CanadaStatesAbbreviation, UsStatesAbbreviation}, payments::{self, BankDebitBilling, OrderDetailsWithAmount}, }; - -use data_models::payments::payment_attempt::PaymentAttempt; - use base64::Engine; use common_utils::{ date_time, errors::ReportSwitchExt, pii::{self, Email, IpAddress}, }; +use data_models::payments::payment_attempt::PaymentAttempt; use diesel_models::enums; use error_stack::{report, IntoReport, ResultExt}; use masking::{ExposeInterface, Secret}; @@ -1617,13 +1615,16 @@ impl AccessPaymentAttemptInfo for PaymentAttempt { } pub trait PaymentsAttemptData { - fn get_browser_info(&self) -> Result>; + fn get_browser_info(&self) + -> Result>; } impl PaymentsAttemptData for PaymentAttempt { - fn get_browser_info(&self) -> Result> { + fn get_browser_info( + &self, + ) -> Result> { self.browser_info - .clone() + .clone() .ok_or(ApiErrorResponse::InvalidDataValue { field_name: "browser_info", })? From 319a0ccbf94a988fc9eb6eaae20cfebcb29a6594 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 30 Nov 2023 15:29:23 +0530 Subject: [PATCH 23/46] 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 4d20657302ef53d8d9f409d39e770f8bff2ae2c0 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 11:41:27 +0530 Subject: [PATCH 24/46] add riskified in oss --- crates/api_models/src/enums.rs | 2 + crates/common_enums/src/enums.rs | 1 + crates/router/src/connector.rs | 7 +- crates/router/src/connector/riskified.rs | 550 ++++++++++++++++ .../src/connector/riskified/transformers.rs | 7 + .../connector/riskified/transformers/api.rs | 602 ++++++++++++++++++ .../connector/riskified/transformers/auth.rs | 24 + crates/router/src/connector/signifyd.rs | 4 +- .../connector/signifyd/transformers/api.rs | 30 +- crates/router/src/connector/utils.rs | 39 ++ crates/router/src/core/fraud_check.rs | 19 +- .../core/fraud_check/flows/checkout_flow.rs | 25 +- .../fraud_check/flows/fulfillment_flow.rs | 7 +- .../core/fraud_check/flows/record_return.rs | 1 + .../src/core/fraud_check/flows/sale_flow.rs | 1 + .../fraud_check/flows/transaction_flow.rs | 4 + .../fraud_check/operation/fraud_check_post.rs | 7 +- .../fraud_check/operation/fraud_check_pre.rs | 13 +- crates/router/src/core/fraud_check/types.rs | 10 + crates/router/src/core/payments/flows.rs | 19 + crates/router/src/types/api/fraud_check.rs | 1 + crates/router/src/types/fraud_check.rs | 16 +- loadtest/config/development.toml | 1 + 23 files changed, 1356 insertions(+), 34 deletions(-) create mode 100644 crates/router/src/connector/riskified.rs create mode 100644 crates/router/src/connector/riskified/transformers.rs create mode 100644 crates/router/src/connector/riskified/transformers/api.rs create mode 100644 crates/router/src/connector/riskified/transformers/auth.rs diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index de3538938213..3f51595cbd70 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -198,6 +198,7 @@ impl From for RoutableConnectors { pub enum FrmConnectors { /// Signifyd Risk Manager. Official docs: https://docs.signifyd.com/ Signifyd, + Riskified, } #[cfg(feature = "frm")] @@ -205,6 +206,7 @@ impl From for RoutableConnectors { fn from(value: FrmConnectors) -> Self { match value { FrmConnectors::Signifyd => Self::Signifyd, + FrmConnectors::Riskified => Self::Riskified, } } } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 857c1a0085d9..ca5950fbf467 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -144,6 +144,7 @@ pub enum RoutableConnectors { Powertranz, Prophetpay, Rapyd, + Riskified, Shift4, Signifyd, Square, diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 55c61442591d..750e7bfd7ad8 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -39,6 +39,7 @@ pub mod payu; pub mod powertranz; pub mod prophetpay; pub mod rapyd; +pub mod riskified; pub mod shift4; pub mod signifyd; pub mod square; @@ -64,7 +65,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, signifyd::Signifyd, square::Square, - stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, - worldline::Worldline, worldpay::Worldpay, zen::Zen, + prophetpay::Prophetpay, rapyd::Rapyd, riskified::Riskified, 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/riskified.rs b/crates/router/src/connector/riskified.rs new file mode 100644 index 000000000000..9fed012c202b --- /dev/null +++ b/crates/router/src/connector/riskified.rs @@ -0,0 +1,550 @@ +pub mod transformers; +use std::fmt::Debug; + +use error_stack::{IntoReport, ResultExt}; +use masking::{ExposeInterface, PeekInterface}; +use ring::hmac; +use transformers as riskified; + +use super::utils::FrmTransactionRouterDataRequest; +use crate::{ + configs::settings, + core::errors::{self, CustomResult}, + headers, + services::{request, ConnectorIntegration, ConnectorValidation}, + types::{ + self, + api::{ + self, Checkout, ConnectorCommon, ConnectorCommonExt, Fulfillment, RecordReturn, Sale, + Transaction, + }, + }, +}; +#[cfg(feature = "frm")] +use crate::{ + services, + types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, + utils::{self, BytesExt}, +}; + +#[derive(Debug, Clone)] +pub struct Riskified; + +impl Riskified { + pub fn generate_authorization_signature( + &self, + auth: &riskified::RiskifiedAuthType, + payload: &str, + ) -> CustomResult { + let key = hmac::Key::new( + hmac::HMAC_SHA256, + auth.secret_token.clone().expose().as_bytes(), + ); + + let signature_value = hmac::sign(&key, payload.as_bytes()); + + let digest = signature_value.as_ref(); + + Ok(hex::encode(digest)) + } +} + +impl ConnectorCommonExt for Riskified +where + Self: ConnectorIntegration, +{ + fn build_headers( + &self, + req: &types::RouterData, + connectors: &settings::Connectors, + ) -> CustomResult)>, errors::ConnectorError> { + let auth: riskified::RiskifiedAuthType = + riskified::RiskifiedAuthType::try_from(&req.connector_auth_type)?; + + let riskified_req = self + .get_request_body(req, connectors)? + .ok_or(errors::ConnectorError::RequestEncodingFailed)?; + + let binding = types::RequestBody::get_inner_value(riskified_req); + let payload = binding.peek(); + + let digest = self + .generate_authorization_signature(&auth, payload) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let header = vec![ + ( + headers::CONTENT_TYPE.to_string(), + self.get_content_type().to_string().into(), + ), + ( + "X-RISKIFIED-SHOP-DOMAIN".to_string(), + auth.domain_name.clone().into(), + ), + ( + "X-RISKIFIED-HMAC-SHA256".to_string(), + request::Mask::into_masked(digest), + ), + ( + "Accept".to_string(), + "application/vnd.riskified.com; version=2".into(), + ), + ]; + + Ok(header) + } +} + +impl ConnectorCommon for Riskified { + fn id(&self) -> &'static str { + "riskified" + } + + fn common_get_content_type(&self) -> &'static str { + "application/json" + } + fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { + connectors.riskified.base_url.as_ref() + } + fn build_error_response( + &self, + res: Response, + ) -> CustomResult { + let response: riskified::ErrorResponse = res + .response + .parse_struct("ErrorResponse") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + Ok(ErrorResponse { + status_code: res.status_code, + attempt_status: None, + code: crate::consts::NO_ERROR_CODE.to_string(), + message: response.error.message.clone(), + reason: None, + connector_transaction_id: None, + }) + } +} + +impl + ConnectorIntegration< + Checkout, + frm_types::FraudCheckCheckoutData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + 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), "/decide")) + } + + fn get_request_body( + &self, + req: &frm_types::FrmCheckoutRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = riskified::RiskifiedPaymentsCheckoutRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_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: riskified::RiskifiedPaymentsResponse = res + .response + .parse_struct("RiskifiedPaymentsResponse 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 api::Payment for Riskified {} +impl api::PaymentAuthorize for Riskified {} +impl api::PaymentSync for Riskified {} +impl api::PaymentVoid for Riskified {} +impl api::PaymentCapture for Riskified {} +impl api::MandateSetup for Riskified {} +impl api::ConnectorAccessToken for Riskified {} +impl api::PaymentToken for Riskified {} +impl api::Refund for Riskified {} +impl api::RefundExecute for Riskified {} +impl api::RefundSync for Riskified {} +impl ConnectorValidation for Riskified {} + +impl ConnectorIntegration + for Riskified +{ +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + Transaction, + frm_types::FraudCheckTransactionData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + 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 { + match req.is_payment_successful() { + Some(false) => Ok(format!( + "{}{}", + self.base_url(connectors), + "/checkout_denied" + )), + Some(true) => Ok(format!("{}{}", self.base_url(connectors), "/decision")), + None => Err(errors::ConnectorError::FlowNotSupported { + flow: "Transaction".to_owned(), + connector: req.connector.to_string(), + })?, + } + } + + fn get_request_body( + &self, + req: &frm_types::FrmTransactionRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + match req.is_payment_successful() { + Some(false) => { + let req_obj = riskified::TransactionFailedRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_req)) + } + Some(true) => { + let req_obj = riskified::TransactionSuccessRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_req)) + } + None => Err(errors::ConnectorError::FlowNotSupported { + flow: "Transaction".to_owned(), + connector: req.connector.to_owned(), + })?, + } + } + + 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: riskified::RiskifiedTransactionResponse = res + .response + .parse_struct("RiskifiedPaymentsResponse Transaction") + .change_context(errors::ConnectorError::ResponseDeserializationFailed)?; + + match response { + riskified::RiskifiedTransactionResponse::FailedResponse(response_data) => { + ::try_from(types::ResponseRouterData { + response: response_data, + data: data.clone(), + http_code: res.status_code, + }) + } + riskified::RiskifiedTransactionResponse::SucceessResponse(response_data) => { + ::try_from(types::ResponseRouterData { + response: response_data, + data: data.clone(), + http_code: res.status_code, + }) + } + } + } + fn get_error_response( + &self, + res: Response, + ) -> CustomResult { + self.build_error_response(res) + } +} + +#[cfg(feature = "frm")] +impl + ConnectorIntegration< + Fulfillment, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + > for Riskified +{ + 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), "/fulfill")) + } + + fn get_request_body( + &self, + req: &frm_types::FrmFulfillmentRouterData, + _connectors: &settings::Connectors, + ) -> CustomResult, errors::ConnectorError> { + let req_obj = riskified::RiskifiedFullfillmentRequest::try_from(req)?; + let riskified_req = types::RequestBody::log_and_get_request_body( + &req_obj, + utils::Encode::::encode_to_string_of_json, + ) + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + Ok(Some(riskified_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: riskified::RiskifiedFulfilmentResponse = res + .response + .parse_struct("RiskifiedFulfilmentResponse fulfilment") + .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< + RecordReturn, + frm_types::FraudCheckRecordReturnData, + frm_types::FraudCheckResponseData, + > for Riskified +{ +} + +impl + ConnectorIntegration< + api::PaymentMethodToken, + types::PaymentMethodTokenizationData, + types::PaymentsResponseData, + > for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl + ConnectorIntegration< + api::SetupMandate, + types::SetupMandateRequestData, + types::PaymentsResponseData, + > for Riskified +{ +} + +impl api::PaymentSession for Riskified {} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +impl ConnectorIntegration + for Riskified +{ +} + +#[cfg(feature = "frm")] +impl api::FraudCheck for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckSale for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckCheckout for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckTransaction for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckFulfillment for Riskified {} +#[cfg(feature = "frm")] +impl frm_api::FraudCheckRecordReturn for Riskified {} + +#[async_trait::async_trait] +impl api::IncomingWebhook for Riskified { + 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/riskified/transformers.rs b/crates/router/src/connector/riskified/transformers.rs new file mode 100644 index 000000000000..4f155f341f6d --- /dev/null +++ b/crates/router/src/connector/riskified/transformers.rs @@ -0,0 +1,7 @@ +#[cfg(feature = "frm")] +pub mod api; +pub mod auth; + +#[cfg(feature = "frm")] +pub use self::api::*; +pub use self::auth::*; diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs new file mode 100644 index 000000000000..e4bb731679b1 --- /dev/null +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -0,0 +1,602 @@ +use api_models::payments::AdditionalPaymentData; +use common_utils::ext_traits::ValueExt; +use error_stack::{self, ResultExt}; +use masking::Secret; +use serde::{Deserialize, Serialize}; +use time::PrimitiveDateTime; + +use crate::{ + connector::utils::{ + AddressDetailsData, FraudCheckCheckoutRequest, + FraudCheckTransactionRequest, RouterData, + }, + core::{errors, fraud_check::types as core_types}, + types::{ + self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, + ResponseId, ResponseRouterData, + }, +}; + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedPaymentsCheckoutRequest { + order: CheckoutRequest, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct CheckoutRequest { + id: String, + note: Option, + email: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + currency: Option, + #[serde(with = "common_utils::custom_serde::iso8601")] + updated_at: PrimitiveDateTime, + gateway: Option, + browser_ip: Option, + total_price: i64, + total_discounts: i64, + cart_token: String, + referring_site: String, + line_items: Vec, + discount_codes: Vec, + shipping_lines: Vec, + payment_details: Option, + customer: RiskifiedCustomer, + billing_address: Option, + shipping_address: Option, + source: Source, + client_details: ClientDetails, + vendor_name: String, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct PaymentDetails { + credit_card_bin: Option, + credit_card_number: Option, + credit_card_company: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ShippingLines { + price: i64, + title: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct DiscountCodes { + amount: i64, + code: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ClientDetails { + user_agent: Option, + accept_language: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedCustomer { + email: Option, + first_name: Option>, + last_name: Option>, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + verified_email: bool, + id: String, + account_type: CustomerAccountType, + orders_count: i32, + phone: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum CustomerAccountType { + Guest, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderAddress { + first_name: Option>, + last_name: Option>, + address1: Option>, + country_code: Option, + city: Option, + province: Option>, + phone: Option>, + zip: Option>, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct LineItem { + price: i64, + quantity: i32, + title: String, + product_type: Option, + requires_shipping: Option, + product_id: Option, + category: Option, + brand: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "snake_case")] +pub enum Source { + DesktopWeb, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedMetadata { + vendor_name: String, + shipping_lines: Vec, +} + +impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutRequest { + type Error = error_stack::Report; + fn try_from(payment_data: &frm_types::FrmCheckoutRouterData) -> Result { + let metadata: RiskifiedMetadata = payment_data + .frm_metadata + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "frm_metadata", + })? + .parse_value("Riskified Metadata") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; + + let billing_address = payment_data.get_billing_address_with_phone_number()?; + let shipping_address = payment_data.get_shipping_address_with_phone_number()?; + let address = payment_data.get_billing_address()?; + + Ok(Self { + order: CheckoutRequest { + id: payment_data.attempt_id.clone(), + email: payment_data.request.email.clone(), + created_at: common_utils::date_time::now(), + updated_at: common_utils::date_time::now(), + gateway: payment_data.request.gateway.clone(), + total_price: payment_data.request.amount, + cart_token: payment_data.attempt_id.clone(), + line_items: payment_data + .request + .get_order_details()? + .iter() + .map(|order_detail| LineItem { + price: order_detail.amount, + quantity: i32::from(order_detail.quantity), + title: order_detail.product_name.clone(), + product_type: order_detail.product_type.clone(), + requires_shipping: order_detail.requires_shipping, + product_id: order_detail.product_id.clone(), + category: order_detail.category.clone(), + brand: order_detail.brand.clone(), + }) + .collect::>(), + source: Source::DesktopWeb, + billing_address: OrderAddress::try_from(billing_address).ok(), + shipping_address: OrderAddress::try_from(shipping_address).ok(), + total_discounts: 0, + currency: payment_data.request.currency, + referring_site: "hyperswitch.io".to_owned(), + discount_codes: [DiscountCodes { + amount: 0, + code: Some("ABC".to_owned()), + }] + .to_vec(), + shipping_lines: metadata.shipping_lines, + customer: RiskifiedCustomer { + email: payment_data.request.email.clone(), + + first_name: address.get_first_name().ok().cloned(), + last_name: address.get_last_name().ok().cloned(), + created_at: common_utils::date_time::now(), + verified_email: false, + id: payment_data.get_customer_id()?, + account_type: CustomerAccountType::Guest, + orders_count: 0, + phone: billing_address + .clone() + .phone + .and_then(|phone_data| phone_data.number), + }, + browser_ip: payment_data + .request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.ip_address), + client_details: ClientDetails { + user_agent: payment_data + .request + .browser_info + .as_ref() + .and_then(|browser_info| browser_info.user_agent.clone()), + accept_language: payment_data.request.browser_info.as_ref().and_then( + |browser_info: &types::BrowserInformation| browser_info.language.clone(), + ), + }, + note: payment_data.description.clone(), + vendor_name: metadata.vendor_name, + payment_details: match payment_data.request.payment_method_data.as_ref() { + Some(AdditionalPaymentData::Card(card_info)) => Some(PaymentDetails { + credit_card_bin: card_info.card_isin.clone(), + credit_card_number: card_info + .last4 + .clone() + .map(|last_four| format!("XXXX-XXXX-XXXX-{}", last_four)), + credit_card_company: card_info.card_network.clone(), + }), + Some(_) | None => None, + }, + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedPaymentsResponse { + order: OrderResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderResponse { + id: String, + status: PaymentStatus, + description: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFulfilmentResponse { + order: OrderFulfilmentResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderFulfilmentResponse { + id: String, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FulfilmentStatus { + Fulfilled, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum PaymentStatus { + Captured, + Created, + Submitted, + Approved, + Declined, + Processing, +} + +impl + TryFrom> + for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + RiskifiedPaymentsResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.order.id), + status: storage_enums::FraudCheckStatus::from(item.response.order.status), + connector_metadata: None, + score: None, + reason: item.response.order.description.map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +impl From for storage_enums::FraudCheckStatus { + fn from(item: PaymentStatus) -> Self { + match item { + PaymentStatus::Approved => Self::Legit, + PaymentStatus::Declined => Self::Fraud, + _ => Self::Pending, + } + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionFailedRequest { + checkout: FailedTransactionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct FailedTransactionData { + id: String, + payment_details: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct DeclinedPaymentDetails { + authorization_error: AuthorizationError, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct AuthorizationError { + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + error_code: Option, + message: Option, +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionFailedRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + Ok(Self { + checkout: FailedTransactionData { + id: item.attempt_id.clone(), + payment_details: [DeclinedPaymentDetails { + authorization_error: AuthorizationError { + created_at: common_utils::date_time::now(), + error_code: item.request.error_code.clone(), + message: item.request.error_message.clone(), + }, + }] + .to_vec(), + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFailedTransactionResponse { + checkout: OrderResponse, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(untagged)] +pub enum RiskifiedTransactionResponse { + FailedResponse(RiskifiedFailedTransactionResponse), + SucceessResponse(RiskifiedPaymentsResponse), +} + +impl + TryFrom< + ResponseRouterData< + F, + RiskifiedFailedTransactionResponse, + T, + frm_types::FraudCheckResponseData, + >, + > for types::RouterData +{ + type Error = error_stack::Report; + fn try_from( + item: ResponseRouterData< + F, + RiskifiedFailedTransactionResponse, + T, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::TransactionResponse { + resource_id: ResponseId::ConnectorTransactionId(item.response.checkout.id), + status: storage_enums::FraudCheckStatus::from(item.response.checkout.status), + connector_metadata: None, + score: None, + reason: item + .response + .checkout + .description + .map(serde_json::Value::from), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionSuccessRequest { + order: SuccessfulTransactionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct SuccessfulTransactionData { + id: String, + decision: TransactionDecisionData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionDecisionData { + external_status: TransactionStatus, + reason: Option, + amount: i64, + currency: storage_enums::Currency, + #[serde(with = "common_utils::custom_serde::iso8601")] + decided_at: PrimitiveDateTime, + payment_details: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct TransactionPaymentDetails { + authorization_id: Option, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum TransactionStatus { + Approved, +} + +impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionSuccessRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { + Ok(Self { + order: SuccessfulTransactionData { + id: item.attempt_id.clone(), + decision: TransactionDecisionData { + external_status: TransactionStatus::Approved, + reason: None, + amount: item.request.amount, + currency: item.request.get_currency()?, + decided_at: common_utils::date_time::now(), + payment_details: [TransactionPaymentDetails { + authorization_id: item.request.connector_transaction_id.clone(), + }] + .to_vec(), + }, + }, + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct RiskifiedFullfillmentRequest { + order: OrderFullfillment, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum FulfillmentRequestStatus { + Success, + Cancelled, + Error, + Failure, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct OrderFullfillment { + id: String, + fulfillments: FullfilmentData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct FullfilmentData { + fulfillment_id: String, + #[serde(with = "common_utils::custom_serde::iso8601")] + created_at: PrimitiveDateTime, + status: Option, + tracking_company: String, + tracking_numbers: String, + tracking_urls: Option, +} + +impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmFulfillmentRouterData) -> Result { + Ok(Self { + order: OrderFullfillment { + id: item.attempt_id.clone(), + fulfillments: FullfilmentData { + fulfillment_id: item.payment_id.clone(), + created_at: common_utils::date_time::now(), + status: item + .request + .fulfillment_req + .fulfillment_status + .clone() + .and_then(get_fulfillment_status), + tracking_company: item + .request + .fulfillment_req + .tracking_company + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "tracking_company", + })?, + tracking_numbers: item + .request + .fulfillment_req + .tracking_numbers + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "tracking_numbers", + })?, + tracking_urls: item.request.fulfillment_req.tracking_urls.clone(), + }, + }, + }) + } +} + +impl + TryFrom< + ResponseRouterData< + Fulfillment, + RiskifiedFulfilmentResponse, + 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, + RiskifiedFulfilmentResponse, + frm_types::FraudCheckFulfillmentData, + frm_types::FraudCheckResponseData, + >, + ) -> Result { + Ok(Self { + response: Ok(frm_types::FraudCheckResponseData::FulfillmentResponse { + order_id: item.response.order.id, + shipment_ids: Vec::new(), + }), + ..item.data + }) + } +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ErrorResponse { + pub error: ErrorData, +} + +#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +pub struct ErrorData { + pub message: String, +} + +impl TryFrom<&api_models::payments::Address> for OrderAddress { + type Error = error_stack::Report; + fn try_from(address_info: &api_models::payments::Address) -> Result { + let address = + address_info + .clone() + .address + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "address", + })?; + Ok(Self { + first_name: address.first_name.clone(), + last_name: address.last_name.clone(), + address1: address.line1.clone(), + country_code: address.country, + city: address.city.clone(), + province: address.state.clone(), + zip: address.zip.clone(), + phone: address_info + .phone + .clone() + .and_then(|phone_data| phone_data.number), + }) + } +} + +fn get_fulfillment_status( + status: core_types::FulfillmentStatus, +) -> Option { + match status { + core_types::FulfillmentStatus::COMPLETE => Some(FulfillmentRequestStatus::Success), + core_types::FulfillmentStatus::CANCELED => Some(FulfillmentRequestStatus::Cancelled), + core_types::FulfillmentStatus::PARTIAL | core_types::FulfillmentStatus::REPLACEMENT => None, + } +} diff --git a/crates/router/src/connector/riskified/transformers/auth.rs b/crates/router/src/connector/riskified/transformers/auth.rs new file mode 100644 index 000000000000..49784c85817c --- /dev/null +++ b/crates/router/src/connector/riskified/transformers/auth.rs @@ -0,0 +1,24 @@ +use error_stack; +use masking::{ExposeInterface, Secret}; + +use crate::{core::errors, types}; + +// #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] +// #[serde(rename_all = "camelCase")] +pub struct RiskifiedAuthType { + pub secret_token: Secret, + pub domain_name: String, +} + +impl TryFrom<&types::ConnectorAuthType> for RiskifiedAuthType { + type Error = error_stack::Report; + fn try_from(auth_type: &types::ConnectorAuthType) -> Result { + match auth_type { + types::ConnectorAuthType::BodyKey { api_key, key1 } => Ok(Self { + secret_token: api_key.to_owned(), + domain_name: key1.to_owned().expose(), + }), + _ => Err(errors::ConnectorError::FailedToObtainAuthType.into()), + } + } +} diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs index eefce05ddbe2..48b486432fbd 100644 --- a/crates/router/src/connector/signifyd.rs +++ b/crates/router/src/connector/signifyd.rs @@ -478,10 +478,10 @@ impl req: &frm_types::FrmFulfillmentRouterData, _connectors: &settings::Connectors, ) -> CustomResult, errors::ConnectorError> { - let req_obj = &req.request.fulfillment_request; + let req_obj = signifyd::FrmFullfillmentSignifydRequest::try_from(req)?; let signifyd_req = types::RequestBody::log_and_get_request_body( &req_obj, - utils::Encode::::encode_to_string_of_json, + utils::Encode::::encode_to_string_of_json, ) .change_context(errors::ConnectorError::RequestEncodingFailed)?; Ok(Some(signifyd_req)) diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index 126d97ec9cba..2af6f02af796 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -13,7 +13,7 @@ use crate::{ }, core::{ errors, - fraud_check::types::{self as core_types, FrmFulfillmentRequest}, + fraud_check::types as core_types }, types::{ self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, @@ -356,7 +356,7 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ #[serde(deny_unknown_fields)] #[serde_with::skip_serializing_none] #[serde(rename_all = "camelCase")] -pub struct FrmFullfillmentSignifydApiRequest { +pub struct FrmFullfillmentSignifydRequest { pub order_id: String, pub fulfillment_status: Option, pub fulfillments: Vec, @@ -391,22 +391,30 @@ pub struct Product { 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 +impl TryFrom<&frm_types::FrmFulfillmentRouterData> for FrmFullfillmentSignifydRequest { + type Error = error_stack::Report; + fn try_from(item: &frm_types::FrmFulfillmentRouterData) -> Result { + Ok(Self { + order_id: item.request.fulfillment_req.order_id.clone(), + fulfillment_status: item + .request + .fulfillment_req + .fulfillment_status + .clone() + .map(|fulfillment_status| FulfillmentStatus::from(&fulfillment_status)), + fulfillments: item + .request + .fulfillment_req .fulfillments .iter() .map(|f| Fulfillments::from(f.clone())) .collect(), - } + }) } } -impl From for FulfillmentStatus { - fn from(status: core_types::FulfillmentStatus) -> Self { +impl From<&core_types::FulfillmentStatus> for FulfillmentStatus { + fn from(status: &core_types::FulfillmentStatus) -> Self { match status { core_types::FulfillmentStatus::PARTIAL => Self::PARTIAL, core_types::FulfillmentStatus::COMPLETE => Self::COMPLETE, diff --git a/crates/router/src/connector/utils.rs b/crates/router/src/connector/utils.rs index 3df3bef6f3bf..4e1760409a00 100644 --- a/crates/router/src/connector/utils.rs +++ b/crates/router/src/connector/utils.rs @@ -1684,3 +1684,42 @@ impl PaymentsAttemptData for PaymentAttempt { }) } } + +#[cfg(feature = "frm")] +pub trait FrmTransactionRouterDataRequest { + fn is_payment_successful(&self) -> Option; +} + +#[cfg(feature = "frm")] +impl FrmTransactionRouterDataRequest for fraud_check::FrmTransactionRouterData { + fn is_payment_successful(&self) -> Option { + match self.status { + storage_enums::AttemptStatus::AuthenticationFailed + | storage_enums::AttemptStatus::RouterDeclined + | storage_enums::AttemptStatus::AuthorizationFailed + | storage_enums::AttemptStatus::Voided + | storage_enums::AttemptStatus::CaptureFailed + | storage_enums::AttemptStatus::Failure + | storage_enums::AttemptStatus::AutoRefunded => Some(false), + + storage_enums::AttemptStatus::AuthenticationSuccessful + | storage_enums::AttemptStatus::PartialChargedAndChargeable + | storage_enums::AttemptStatus::Authorized + | storage_enums::AttemptStatus::Charged => Some(true), + + storage_enums::AttemptStatus::Started + | storage_enums::AttemptStatus::AuthenticationPending + | storage_enums::AttemptStatus::Authorizing + | storage_enums::AttemptStatus::CodInitiated + | storage_enums::AttemptStatus::VoidInitiated + | storage_enums::AttemptStatus::CaptureInitiated + | storage_enums::AttemptStatus::VoidFailed + | storage_enums::AttemptStatus::PartialCharged + | storage_enums::AttemptStatus::Unresolved + | storage_enums::AttemptStatus::Pending + | storage_enums::AttemptStatus::PaymentMethodAwaited + | storage_enums::AttemptStatus::ConfirmationAwaited + | storage_enums::AttemptStatus::DeviceDataCollectionPending => None, + } + } +} diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index 0d2a10c7d25c..e8277e56a596 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -17,7 +17,6 @@ use self::{ }; use super::errors::{ConnectorErrorExt, RouterResponse}; use crate::{ - connector::signifyd::transformers::FrmFullfillmentSignifydApiRequest, core::{ errors::{self, RouterResult}, payments::{ @@ -52,7 +51,7 @@ pub mod types; pub async fn call_frm_service( state: &AppState, payment_data: &mut payments::PaymentData, - frm_data: FrmData, + frm_data: &mut FrmData, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, customer: &Option, @@ -78,7 +77,12 @@ where ) .await?; - let router_data = frm_data + frm_data.payment_attempt.connector_transaction_id = payment_data + .payment_attempt + .connector_transaction_id + .clone(); + + let mut router_data = frm_data .construct_router_data( state, &frm_data.connector_details.connector_name, @@ -88,6 +92,9 @@ where &merchant_connector_account, ) .await?; + + router_data.status = payment_data.payment_attempt.status; + let connector = FraudCheckConnectorData::get_connector_by_name(&frm_data.connector_details.connector_name)?; let router_data_res = router_data @@ -405,6 +412,7 @@ where address: payment_data.address.clone(), connector_details: frm_connector_details.clone(), order_details, + frm_metadata: payment_data.frm_metadata.clone(), }; let fraud_check_operation: operation::BoxedFraudCheckOperation = @@ -730,15 +738,14 @@ pub async fn make_fulfillment_api_call( 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, + fraud_check.frm_name.clone(), + req, ) .await?; let response = services::execute_connector_processing_step( diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 47a29d657484..cd39cbd3bcfe 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -1,9 +1,11 @@ use async_trait::async_trait; use common_utils::ext_traits::ValueExt; use error_stack::ResultExt; +use masking::ExposeInterface; use super::{ConstructFlowSpecificData, FeatureFrm}; use crate::{ + connector::utils::PaymentsAttemptData, core::{ errors::{ConnectorErrorExt, RouterResult}, fraud_check::types::FrmData, @@ -15,7 +17,7 @@ use crate::{ domain, fraud_check::{FraudCheckCheckoutData, FraudCheckResponseData, FrmCheckoutRouterData}, storage::enums as storage_enums, - ConnectorAuthType, ResponseId, RouterData, + BrowserInformation, ConnectorAuthType, ResponseId, RouterData, }, AppState, }; @@ -43,6 +45,7 @@ impl ConstructFlowSpecificData = self.payment_attempt.get_browser_info().ok(); let customer_id = customer.to_owned().map(|customer| customer.customer_id); let router_data = RouterData { @@ -68,6 +71,25 @@ impl ConstructFlowSpecificData( + "AdditionalPaymentData", + ) + }) + .transpose() + .unwrap_or_default(), + email: customer.clone().and_then(|customer_data| { + customer_data.email.map(|email| email.into_inner().expose()) + }), + gateway: self.payment_attempt.connector.clone(), }, // self.order_details response: Ok(FraudCheckResponseData::TransactionResponse { resource_id: ResponseId::ConnectorTransactionId("".to_string()), @@ -94,6 +116,7 @@ impl ConstructFlowSpecificData( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, connector: String, - fulfillment_request: FrmFullfillmentSignifydApiRequest, + fulfillment_request: FrmFulfillmentRequest, ) -> RouterResult { let profile_id = core_utils::get_profile_id_from_business_details( payment_intent.business_country, @@ -79,7 +79,7 @@ pub async fn construct_fulfillment_router_data<'a>( request: FraudCheckFulfillmentData { amount: payment_attempt.amount, order_details: payment_intent.order_details.clone(), - fulfillment_request, + fulfillment_req: fulfillment_request, }, response: Err(ErrorResponse::default()), access_token: None, @@ -105,6 +105,7 @@ pub async fn construct_fulfillment_router_data<'a>( connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: 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 index eaefdbefcc77..bd0ba3e4f7f4 100644 --- a/crates/router/src/core/fraud_check/flows/record_return.rs +++ b/crates/router/src/core/fraud_check/flows/record_return.rs @@ -96,6 +96,7 @@ impl ConstructFlowSpecificData for FraudCheckPost { connector_details: payment_data.connector_details, order_details: payment_data.order_details, refund: None, + frm_metadata: payment_data.frm_metadata, }; Ok(Some(frm_data)) } @@ -152,7 +153,7 @@ impl Domain for FraudCheckPost { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -219,7 +220,7 @@ impl Domain for FraudCheckPost { let _router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -243,7 +244,7 @@ impl Domain for FraudCheckPost { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, 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 00f50d01a862..b92df3d3ef9f 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 @@ -120,6 +120,7 @@ impl GetTracker for FraudCheckPre { connector_details: payment_data.connector_details, order_details: payment_data.order_details, refund: None, + frm_metadata: payment_data.frm_metadata, }; Ok(Some(frm_data)) } @@ -146,7 +147,7 @@ impl Domain for FraudCheckPre { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -163,6 +164,9 @@ impl Domain for FraudCheckPre { order_details: router_data.request.order_details, currency: router_data.request.currency, payment_method: Some(router_data.payment_method), + error_code: router_data.request.error_code, + error_message: router_data.request.error_message, + connector_transaction_id: router_data.request.connector_transaction_id, }), response: FrmResponse::Transaction(router_data.response), })) @@ -180,7 +184,7 @@ impl Domain for FraudCheckPre { let router_data = frm_core::call_frm_service::( state, payment_data, - frm_data.to_owned(), + &mut frm_data.to_owned(), merchant_account, &key_store, customer, @@ -195,6 +199,11 @@ impl Domain for FraudCheckPre { request: FrmRequest::Checkout(FraudCheckCheckoutData { amount: router_data.request.amount, order_details: router_data.request.order_details, + currency: router_data.request.currency, + browser_info: router_data.request.browser_info, + payment_method_data: router_data.request.payment_method_data, + email: router_data.request.email, + gateway: router_data.request.gateway, }), response: FrmResponse::Checkout(router_data.response), }) diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs index 1d6e7cb45a58..c948e4dba2ce 100644 --- a/crates/router/src/core/fraud_check/types.rs +++ b/crates/router/src/core/fraud_check/types.rs @@ -56,6 +56,7 @@ pub struct FrmData { pub connector_details: ConnectorDetailsCore, pub order_details: Option>, pub refund: Option, + pub frm_metadata: Option, } #[derive(Debug)] @@ -79,6 +80,7 @@ pub struct PaymentToFrmData { pub address: PaymentAddress, pub connector_details: ConnectorDetailsCore, pub order_details: Option>, + pub frm_metadata: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -123,6 +125,14 @@ pub struct FrmFulfillmentRequest { ///contains details of the fulfillment #[schema(value_type = Vec)] pub fulfillments: Vec, + //name of the tracking Company + #[schema(max_length = 255, example = "fedex")] + pub tracking_company: Option, + //tracking ID of the product + #[schema(max_length = 255, example = "track_8327446667")] + pub tracking_numbers: Option, + //tracking_url for tracking the product + pub tracking_urls: Option, } #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 73ddf44f47d8..b7bfb498c868 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -171,6 +171,7 @@ default_imp_for_complete_authorize!( connector::Payeezy, connector::Payu, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -249,6 +250,7 @@ default_imp_for_webhook_source_verification!( connector::Prophetpay, connector::Rapyd, connector::Shift4, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -328,6 +330,7 @@ default_imp_for_create_customer!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -397,6 +400,7 @@ default_imp_for_connector_redirect_response!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -457,6 +461,7 @@ default_imp_for_connector_request_id!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -540,6 +545,7 @@ default_imp_for_accept_dispute!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -641,6 +647,7 @@ default_imp_for_file_upload!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -720,6 +727,7 @@ default_imp_for_submit_evidence!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -799,6 +807,7 @@ default_imp_for_defend_dispute!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -877,6 +886,7 @@ default_imp_for_pre_processing_steps!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Shift4, connector::Signifyd, connector::Square, @@ -939,6 +949,7 @@ default_imp_for_payouts!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1019,6 +1030,7 @@ default_imp_for_payouts_create!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1102,6 +1114,7 @@ default_imp_for_payouts_eligibility!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1182,6 +1195,7 @@ default_imp_for_payouts_fulfill!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1262,6 +1276,7 @@ default_imp_for_payouts_cancel!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1343,6 +1358,7 @@ default_imp_for_payouts_quote!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1424,6 +1440,7 @@ default_imp_for_payouts_recipient!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1504,6 +1521,7 @@ default_imp_for_approve!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, @@ -1585,6 +1603,7 @@ default_imp_for_reject!( connector::Powertranz, connector::Prophetpay, connector::Rapyd, + connector::Riskified, connector::Signifyd, connector::Square, connector::Stax, diff --git a/crates/router/src/types/api/fraud_check.rs b/crates/router/src/types/api/fraud_check.rs index 7be60bfee952..e871cbc5d330 100644 --- a/crates/router/src/types/api/fraud_check.rs +++ b/crates/router/src/types/api/fraud_check.rs @@ -86,6 +86,7 @@ impl FraudCheckConnectorData { ) -> CustomResult { match connector_name { enums::FrmConnectors::Signifyd => Ok(Box::new(&connector::Signifyd)), + enums::FrmConnectors::Riskified => Ok(Box::new(&connector::Riskified)), } } } diff --git a/crates/router/src/types/fraud_check.rs b/crates/router/src/types/fraud_check.rs index 4bbba8ac4dca..788f4bc31d52 100644 --- a/crates/router/src/types/fraud_check.rs +++ b/crates/router/src/types/fraud_check.rs @@ -1,9 +1,11 @@ use crate::{ - connector::signifyd::transformers::{FrmFullfillmentSignifydApiRequest, RefundMethod}, + connector::signifyd::transformers::RefundMethod, + core::fraud_check::types::FrmFulfillmentRequest, pii::Serialize, services, - types::{api, storage_enums, ErrorResponse, ResponseId, RouterData}, + types::{self, api, storage_enums, ErrorResponse, ResponseId, RouterData}, }; + pub type FrmSaleRouterData = RouterData; pub type FrmSaleType = @@ -74,6 +76,11 @@ pub type FrmCheckoutType = dyn services::ConnectorIntegration< pub struct FraudCheckCheckoutData { pub amount: i64, pub order_details: Option>, + pub currency: Option, + pub browser_info: Option, + pub payment_method_data: Option, + pub email: Option, + pub gateway: Option, } pub type FrmTransactionRouterData = @@ -91,6 +98,9 @@ pub struct FraudCheckTransactionData { pub order_details: Option>, pub currency: Option, pub payment_method: Option, + pub error_code: Option, + pub error_message: Option, + pub connector_transaction_id: Option, } pub type FrmFulfillmentRouterData = @@ -114,7 +124,7 @@ pub type FrmRecordReturnType = dyn services::ConnectorIntegration< pub struct FraudCheckFulfillmentData { pub amount: i64, pub order_details: Option>>, - pub fulfillment_request: FrmFullfillmentSignifydApiRequest, + pub fulfillment_req: FrmFulfillmentRequest, } #[derive(Debug, Clone)] diff --git a/loadtest/config/development.toml b/loadtest/config/development.toml index 3938c1f162a0..d11c764c7f89 100644 --- a/loadtest/config/development.toml +++ b/loadtest/config/development.toml @@ -114,6 +114,7 @@ payu.base_url = "https://secure.snd.payu.com/" powertranz.base_url = "https://staging.ptranz.com/api/" prophetpay.base_url = "https://ccm-thirdparty.cps.golf/" rapyd.base_url = "https://sandboxapi.rapyd.net" +riskified.base_url = "https://sandbox.riskified.com/api" shift4.base_url = "https://api.shift4.com/" square.base_url = "https://connect.squareupsandbox.com/" square.secondary_base_url = "https://pci-connect.squareupsandbox.com/" From aef6bda147c7cd01c232c88847f443734dc9073b Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 11:56:47 +0530 Subject: [PATCH 25/46] fix spell check --- crates/router/src/connector/riskified.rs | 2 +- .../src/connector/riskified/transformers/api.rs | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index 9fed012c202b..78e0bcc55f55 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -337,7 +337,7 @@ impl http_code: res.status_code, }) } - riskified::RiskifiedTransactionResponse::SucceessResponse(response_data) => { + riskified::RiskifiedTransactionResponse::SuccessResponse(response_data) => { ::try_from(types::ResponseRouterData { response: response_data, data: data.clone(), diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index e4bb731679b1..6fe947b0404d 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -7,8 +7,7 @@ use time::PrimitiveDateTime; use crate::{ connector::utils::{ - AddressDetailsData, FraudCheckCheckoutRequest, - FraudCheckTransactionRequest, RouterData, + AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckTransactionRequest, RouterData, }, core::{errors, fraud_check::types as core_types}, types::{ @@ -358,7 +357,7 @@ pub struct RiskifiedFailedTransactionResponse { #[serde(untagged)] pub enum RiskifiedTransactionResponse { FailedResponse(RiskifiedFailedTransactionResponse), - SucceessResponse(RiskifiedPaymentsResponse), + SuccessResponse(RiskifiedPaymentsResponse), } impl @@ -469,11 +468,11 @@ pub enum FulfillmentRequestStatus { #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct OrderFullfillment { id: String, - fulfillments: FullfilmentData, + fulfillments: FulfilmentData, } #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] -pub struct FullfilmentData { +pub struct FulfilmentData { fulfillment_id: String, #[serde(with = "common_utils::custom_serde::iso8601")] created_at: PrimitiveDateTime, @@ -489,7 +488,7 @@ impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequ Ok(Self { order: OrderFullfillment { id: item.attempt_id.clone(), - fulfillments: FullfilmentData { + fulfillments: FulfilmentData { fulfillment_id: item.payment_id.clone(), created_at: common_utils::date_time::now(), status: item From 25978e37a760be978749cfe5c74d56aabf4e3e0f Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 11:57:47 +0530 Subject: [PATCH 26/46] fix formatting --- crates/router/src/connector/signifyd/transformers/api.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index 2af6f02af796..7df6319057cc 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -11,10 +11,7 @@ use crate::{ AddressDetailsData, FraudCheckCheckoutRequest, FraudCheckRecordReturnRequest, FraudCheckSaleRequest, FraudCheckTransactionRequest, RouterData, }, - core::{ - errors, - fraud_check::types as core_types - }, + core::{errors, fraud_check::types as core_types}, types::{ self, api::Fulfillment, fraud_check as frm_types, storage::enums as storage_enums, ResponseId, ResponseRouterData, From d0980a474a7b06897370c2e052299d88c29e2074 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 12:22:13 +0530 Subject: [PATCH 27/46] fix openapi_spec errors --- .../router/src/types/api/verify_connector.rs | 1 + openapi/openapi_spec.json | 18 ++++++++---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/crates/router/src/types/api/verify_connector.rs b/crates/router/src/types/api/verify_connector.rs index 74b15f911b9a..04a9d76632b0 100644 --- a/crates/router/src/types/api/verify_connector.rs +++ b/crates/router/src/types/api/verify_connector.rs @@ -100,6 +100,7 @@ impl VerifyConnectorData { connector_http_status_code: None, external_latency: None, apple_pay_flow: None, + frm_metadata: None, } } } diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index e7dae5d10b27..17c2f1ae220a 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9777,14 +9777,13 @@ ], "nullable": true }, -<<<<<<< HEAD - "frm_metadata": { - "description": "additional data related to some frm connectors", -======= "request_incremental_authorization": { "type": "boolean", "description": "Request for an incremental authorization", ->>>>>>> frm_oss + "nullable": true + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", "nullable": true } } @@ -10151,14 +10150,13 @@ ], "nullable": true }, -<<<<<<< HEAD - "frm_metadata": { - "description": "additional data related to some frm connectors", -======= "request_incremental_authorization": { "type": "boolean", "description": "Request for an incremental authorization", ->>>>>>> frm_oss + "nullable": true + }, + "frm_metadata": { + "description": "additional data related to some frm connectors", "nullable": true } } From 0202f23acab156ca36a9990095fda352d719f5f2 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 13:09:31 +0530 Subject: [PATCH 28/46] fix cargo hack --- crates/router/src/connector/riskified.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index 78e0bcc55f55..bc5a941a05d1 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -6,7 +6,10 @@ use masking::{ExposeInterface, PeekInterface}; use ring::hmac; use transformers as riskified; +#[cfg(feature = "frm")] use super::utils::FrmTransactionRouterDataRequest; + +#[cfg(feature = "frm")] use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -49,6 +52,7 @@ impl Riskified { } } +#[cfg(feature = "frm")] impl ConnectorCommonExt for Riskified where Self: ConnectorIntegration, @@ -125,6 +129,7 @@ impl ConnectorCommon for Riskified { } } +#[cfg(feature = "frm")] impl ConnectorIntegration< Checkout, @@ -222,6 +227,7 @@ impl api::RefundExecute for Riskified {} impl api::RefundSync for Riskified {} impl ConnectorValidation for Riskified {} +#[cfg(feature = "frm")] impl ConnectorIntegration for Riskified { @@ -443,6 +449,7 @@ impl } } +#[cfg(feature = "frm")] impl ConnectorIntegration< RecordReturn, From 77a71b0c7c6fb07c4b4671c4ff750ef7f23e624c Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Fri, 1 Dec 2023 07:40:24 +0000 Subject: [PATCH 29/46] chore: run formatter --- crates/router/src/connector/riskified.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index bc5a941a05d1..f6984c811654 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -8,7 +8,6 @@ use transformers as riskified; #[cfg(feature = "frm")] use super::utils::FrmTransactionRouterDataRequest; - #[cfg(feature = "frm")] use crate::{ configs::settings, From 7cf532a8d5d67d721221e0658b9b798cd83979ea Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 13:42:23 +0530 Subject: [PATCH 30/46] fix hack --- crates/router/src/connector/riskified.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index f6984c811654..f7883e5b2c0b 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -8,7 +8,7 @@ use transformers as riskified; #[cfg(feature = "frm")] use super::utils::FrmTransactionRouterDataRequest; -#[cfg(feature = "frm")] + use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -16,10 +16,7 @@ use crate::{ services::{request, ConnectorIntegration, ConnectorValidation}, types::{ self, - api::{ - self, Checkout, ConnectorCommon, ConnectorCommonExt, Fulfillment, RecordReturn, Sale, - Transaction, - }, + api::{self, ConnectorCommon, ConnectorCommonExt}, }, }; #[cfg(feature = "frm")] @@ -131,7 +128,7 @@ impl ConnectorCommon for Riskified { #[cfg(feature = "frm")] impl ConnectorIntegration< - Checkout, + frm_api::Checkout, frm_types::FraudCheckCheckoutData, frm_types::FraudCheckResponseData, > for Riskified @@ -227,7 +224,7 @@ impl api::RefundSync for Riskified {} impl ConnectorValidation for Riskified {} #[cfg(feature = "frm")] -impl ConnectorIntegration +impl ConnectorIntegration for Riskified { } @@ -235,7 +232,7 @@ impl ConnectorIntegration for Riskified @@ -362,7 +359,7 @@ impl #[cfg(feature = "frm")] impl ConnectorIntegration< - Fulfillment, + frm_api::Fulfillment, frm_types::FraudCheckFulfillmentData, frm_types::FraudCheckResponseData, > for Riskified @@ -451,7 +448,7 @@ impl #[cfg(feature = "frm")] impl ConnectorIntegration< - RecordReturn, + frm_api::RecordReturn, frm_types::FraudCheckRecordReturnData, frm_types::FraudCheckResponseData, > for Riskified From 1b85ef49ef967d2d07b73aaf694b41c4d53a0c19 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 13:42:52 +0530 Subject: [PATCH 31/46] fix formatting --- crates/router/src/connector/riskified.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index f7883e5b2c0b..a93873acbcf1 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -8,7 +8,6 @@ use transformers as riskified; #[cfg(feature = "frm")] use super::utils::FrmTransactionRouterDataRequest; - use crate::{ configs::settings, core::errors::{self, CustomResult}, @@ -128,7 +127,7 @@ impl ConnectorCommon for Riskified { #[cfg(feature = "frm")] impl ConnectorIntegration< - frm_api::Checkout, + frm_api::Checkout, frm_types::FraudCheckCheckoutData, frm_types::FraudCheckResponseData, > for Riskified @@ -224,15 +223,19 @@ impl api::RefundSync for Riskified {} impl ConnectorValidation for Riskified {} #[cfg(feature = "frm")] -impl ConnectorIntegration - for Riskified +impl + ConnectorIntegration< + frm_api::Sale, + frm_types::FraudCheckSaleData, + frm_types::FraudCheckResponseData, + > for Riskified { } #[cfg(feature = "frm")] impl ConnectorIntegration< - frm_api::Transaction, + frm_api::Transaction, frm_types::FraudCheckTransactionData, frm_types::FraudCheckResponseData, > for Riskified @@ -359,7 +362,7 @@ impl #[cfg(feature = "frm")] impl ConnectorIntegration< - frm_api::Fulfillment, + frm_api::Fulfillment, frm_types::FraudCheckFulfillmentData, frm_types::FraudCheckResponseData, > for Riskified @@ -448,7 +451,7 @@ impl #[cfg(feature = "frm")] impl ConnectorIntegration< - frm_api::RecordReturn, + frm_api::RecordReturn, frm_types::FraudCheckRecordReturnData, frm_types::FraudCheckResponseData, > for Riskified From da9f47d7451b76fe2426ce550586d1384b61f546 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Fri, 1 Dec 2023 14:44:52 +0530 Subject: [PATCH 32/46] chore: fix cargo hack errors --- crates/router/src/connector/riskified.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index a93873acbcf1..ddc0d6e83f7e 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -16,12 +16,13 @@ use crate::{ types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, + ErrorResponse, Response, }, }; #[cfg(feature = "frm")] use crate::{ services, - types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, + types::{api::fraud_check as frm_api, fraud_check as frm_types}, utils::{self, BytesExt}, }; @@ -105,6 +106,8 @@ impl ConnectorCommon for Riskified { fn base_url<'a>(&self, connectors: &'a settings::Connectors) -> &'a str { connectors.riskified.base_url.as_ref() } + + #[cfg(feature = "frm")] fn build_error_response( &self, res: Response, From fc2ec74d53b6f312c3076e771e45f0a5ae1344de Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:34:07 +0530 Subject: [PATCH 33/46] Update crates/router/src/connector/riskified/transformers/api.rs Co-authored-by: Jagan --- crates/router/src/connector/riskified/transformers/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index 6fe947b0404d..e9d48529ebfe 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -25,7 +25,7 @@ pub struct RiskifiedPaymentsCheckoutRequest { pub struct CheckoutRequest { id: String, note: Option, - email: Option, + email: Option, #[serde(with = "common_utils::custom_serde::iso8601")] created_at: PrimitiveDateTime, currency: Option, From c56d0e9daba6e4f2fd870730432a1ddabf6a3f94 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:37:10 +0530 Subject: [PATCH 34/46] Update crates/router/src/connector/riskified/transformers/api.rs Co-authored-by: Jagan --- crates/router/src/connector/riskified/transformers/api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index e9d48529ebfe..9fd4ec06448b 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -51,8 +51,8 @@ pub struct CheckoutRequest { #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct PaymentDetails { - credit_card_bin: Option, - credit_card_number: Option, + credit_card_bin: Option>, + credit_card_number: Option>, credit_card_company: Option, } From fdda6338a5a6001bef1463de5523406cd68c4ab8 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Fri, 1 Dec 2023 19:53:31 +0530 Subject: [PATCH 35/46] Update crates/router/src/connector/riskified/transformers/api.rs Co-authored-by: Jagan --- crates/router/src/connector/riskified/transformers/api.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index 9fd4ec06448b..945c0544349b 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -76,7 +76,7 @@ pub struct ClientDetails { #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct RiskifiedCustomer { - email: Option, + email: Option, first_name: Option>, last_name: Option>, #[serde(with = "common_utils::custom_serde::iso8601")] From fccde407b792551f591075186be428736ce1ee65 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:10:31 +0530 Subject: [PATCH 36/46] Update crates/router/src/connector/riskified/transformers/auth.rs Co-authored-by: Jagan --- crates/router/src/connector/riskified/transformers/auth.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/router/src/connector/riskified/transformers/auth.rs b/crates/router/src/connector/riskified/transformers/auth.rs index 49784c85817c..6968bb55a59c 100644 --- a/crates/router/src/connector/riskified/transformers/auth.rs +++ b/crates/router/src/connector/riskified/transformers/auth.rs @@ -3,8 +3,6 @@ use masking::{ExposeInterface, Secret}; use crate::{core::errors, types}; -// #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] -// #[serde(rename_all = "camelCase")] pub struct RiskifiedAuthType { pub secret_token: Secret, pub domain_name: String, From 92ddef090684ea1dc32a9a8722f05c46ff3d945e Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:10:57 +0530 Subject: [PATCH 37/46] Update crates/router/src/core/fraud_check/types.rs Co-authored-by: Jagan --- crates/router/src/core/fraud_check/types.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs index c948e4dba2ce..e60458646f3e 100644 --- a/crates/router/src/core/fraud_check/types.rs +++ b/crates/router/src/core/fraud_check/types.rs @@ -130,9 +130,9 @@ pub struct FrmFulfillmentRequest { pub tracking_company: Option, //tracking ID of the product #[schema(max_length = 255, example = "track_8327446667")] - pub tracking_numbers: Option, + pub tracking_number: Option, //tracking_url for tracking the product - pub tracking_urls: Option, + pub tracking_url: Option, } #[derive(Eq, PartialEq, Clone, Debug, Deserialize, Serialize, ToSchema)] From dc54bfc4ce01279365e1eaf0db8ff6db544c3a0c Mon Sep 17 00:00:00 2001 From: AkshayaFoiger <131388445+AkshayaFoiger@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:11:32 +0530 Subject: [PATCH 38/46] Update crates/router/src/core/fraud_check/flows/checkout_flow.rs Co-authored-by: Jagan --- crates/router/src/core/fraud_check/flows/checkout_flow.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index cd39cbd3bcfe..59b249b44e29 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -86,9 +86,7 @@ impl ConstructFlowSpecificData Date: Fri, 1 Dec 2023 20:11:43 +0530 Subject: [PATCH 39/46] Update crates/router/src/types/fraud_check.rs Co-authored-by: Jagan --- crates/router/src/types/fraud_check.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/router/src/types/fraud_check.rs b/crates/router/src/types/fraud_check.rs index 788f4bc31d52..18f4b9e145d1 100644 --- a/crates/router/src/types/fraud_check.rs +++ b/crates/router/src/types/fraud_check.rs @@ -79,7 +79,7 @@ pub struct FraudCheckCheckoutData { pub currency: Option, pub browser_info: Option, pub payment_method_data: Option, - pub email: Option, + pub email: Option, pub gateway: Option, } From 5e8e922d93185a571e929e45cd7d7877bc3b38dd Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Sat, 2 Dec 2023 12:52:06 +0530 Subject: [PATCH 40/46] chore: fix openAPI Spec --- .../connector/riskified/transformers/api.rs | 26 +++++++------------ .../core/fraud_check/flows/checkout_flow.rs | 8 ++++-- crates/router/src/types/fraud_check.rs | 2 ++ 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index 945c0544349b..854d4a4fd1b8 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -1,5 +1,5 @@ use api_models::payments::AdditionalPaymentData; -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, pii::Email}; use error_stack::{self, ResultExt}; use masking::Secret; use serde::{Deserialize, Serialize}; @@ -176,11 +176,7 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutReq total_discounts: 0, currency: payment_data.request.currency, referring_site: "hyperswitch.io".to_owned(), - discount_codes: [DiscountCodes { - amount: 0, - code: Some("ABC".to_owned()), - }] - .to_vec(), + discount_codes: Vec::new(), shipping_lines: metadata.shipping_lines, customer: RiskifiedCustomer { email: payment_data.request.email.clone(), @@ -216,11 +212,12 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutReq vendor_name: metadata.vendor_name, payment_details: match payment_data.request.payment_method_data.as_ref() { Some(AdditionalPaymentData::Card(card_info)) => Some(PaymentDetails { - credit_card_bin: card_info.card_isin.clone(), + credit_card_bin: card_info.card_isin.clone().map(Secret::new), credit_card_number: card_info .last4 .clone() - .map(|last_four| format!("XXXX-XXXX-XXXX-{}", last_four)), + .map(|last_four| format!("XXXX-XXXX-XXXX-{}", last_four)) + .map(Secret::new), credit_card_company: card_info.card_network.clone(), }), Some(_) | None => None, @@ -505,15 +502,12 @@ impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequ .ok_or(errors::ConnectorError::MissingRequiredField { field_name: "tracking_company", })?, - tracking_numbers: item - .request - .fulfillment_req - .tracking_numbers - .clone() - .ok_or(errors::ConnectorError::MissingRequiredField { + tracking_numbers: item.request.fulfillment_req.tracking_number.clone().ok_or( + errors::ConnectorError::MissingRequiredField { field_name: "tracking_numbers", - })?, - tracking_urls: item.request.fulfillment_req.tracking_urls.clone(), + }, + )?, + tracking_urls: item.request.fulfillment_req.tracking_url.clone(), }, }, }) diff --git a/crates/router/src/core/fraud_check/flows/checkout_flow.rs b/crates/router/src/core/fraud_check/flows/checkout_flow.rs index 59b249b44e29..7f8993af5270 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -1,5 +1,5 @@ use async_trait::async_trait; -use common_utils::ext_traits::ValueExt; +use common_utils::{ext_traits::ValueExt, pii::Email}; use error_stack::ResultExt; use masking::ExposeInterface; @@ -86,7 +86,11 @@ impl ConstructFlowSpecificData Date: Sun, 3 Dec 2023 12:43:31 +0530 Subject: [PATCH 41/46] chore: fix cargo hack --- crates/router/src/connector/riskified.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/router/src/connector/riskified.rs b/crates/router/src/connector/riskified.rs index ddc0d6e83f7e..e34d12def02a 100644 --- a/crates/router/src/connector/riskified.rs +++ b/crates/router/src/connector/riskified.rs @@ -16,13 +16,12 @@ use crate::{ types::{ self, api::{self, ConnectorCommon, ConnectorCommonExt}, - ErrorResponse, Response, }, }; #[cfg(feature = "frm")] use crate::{ services, - types::{api::fraud_check as frm_api, fraud_check as frm_types}, + types::{api::fraud_check as frm_api, fraud_check as frm_types, ErrorResponse, Response}, utils::{self, BytesExt}, }; @@ -48,7 +47,6 @@ impl Riskified { } } -#[cfg(feature = "frm")] impl ConnectorCommonExt for Riskified where Self: ConnectorIntegration, From 36b2584399c1fb7f43b02b2a035c11f7379972c8 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Mon, 4 Dec 2023 14:39:25 +0530 Subject: [PATCH 42/46] make Error a global variable --- .../connector/riskified/transformers/api.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index 854d4a4fd1b8..6490ee3f7e02 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -16,6 +16,8 @@ use crate::{ }, }; +type Error = error_stack::Report; + #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] pub struct RiskifiedPaymentsCheckoutRequest { order: CheckoutRequest, @@ -131,7 +133,7 @@ pub struct RiskifiedMetadata { } impl TryFrom<&frm_types::FrmCheckoutRouterData> for RiskifiedPaymentsCheckoutRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(payment_data: &frm_types::FrmCheckoutRouterData) -> Result { let metadata: RiskifiedMetadata = payment_data .frm_metadata @@ -270,7 +272,7 @@ impl TryFrom> for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: ResponseRouterData< F, @@ -327,7 +329,7 @@ pub struct AuthorizationError { } impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionFailedRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { Ok(Self { checkout: FailedTransactionData { @@ -367,7 +369,7 @@ impl >, > for types::RouterData { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: ResponseRouterData< F, @@ -427,7 +429,7 @@ pub enum TransactionStatus { } impl TryFrom<&frm_types::FrmTransactionRouterData> for TransactionSuccessRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &frm_types::FrmTransactionRouterData) -> Result { Ok(Self { order: SuccessfulTransactionData { @@ -480,7 +482,7 @@ pub struct FulfilmentData { } impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequest { - type Error = error_stack::Report; + type Error = Error; fn try_from(item: &frm_types::FrmFulfillmentRouterData) -> Result { Ok(Self { order: OrderFullfillment { @@ -529,7 +531,7 @@ impl frm_types::FraudCheckResponseData, > { - type Error = error_stack::Report; + type Error = Error; fn try_from( item: ResponseRouterData< Fulfillment, @@ -559,7 +561,7 @@ pub struct ErrorData { } impl TryFrom<&api_models::payments::Address> for OrderAddress { - type Error = error_stack::Report; + type Error = Error; fn try_from(address_info: &api_models::payments::Address) -> Result { let address = address_info From faaad3a3464a2a8524e87129a95c063163a7fc98 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Wed, 6 Dec 2023 21:51:21 +0530 Subject: [PATCH 43/46] resolve merge conflict --- crates/router/src/core/admin.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index d8f8baa0e1fb..0675fa9d263b 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -13,7 +13,6 @@ use common_utils::{ use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; -use pm_auth::connector::plaid::transformers::PlaidAuthType; use uuid::Uuid; use crate::{ From 55e6c11bf04c803e67d62da989d6a8224ae1f231 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Thu, 7 Dec 2023 13:31:32 +0530 Subject: [PATCH 44/46] resolve comments --- crates/router/src/connector/signifyd/transformers/api.rs | 2 +- crates/router_env/Cargo.toml | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index e83f5c4225ae..66d6f0e48cd5 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -172,7 +172,7 @@ impl From for storage_enums::FraudCheckStatus { SignifydPaymentStatus::Accept => Self::Legit, SignifydPaymentStatus::Reject => Self::Fraud, SignifydPaymentStatus::Hold => Self::ManualReview, - SignifydPaymentStatus::Credit | SignifydPaymentStatus::Challenge => Self::Pending, + SignifydPaymentStatus::Challenge | SignifydPaymentStatus::Credit => Self::Pending, } } } diff --git a/crates/router_env/Cargo.toml b/crates/router_env/Cargo.toml index 614bec49d937..3df15ba75a21 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 = [] +payouts = [] \ No newline at end of file From 10ec670185117f6f6ad8525694df320f358fdf17 Mon Sep 17 00:00:00 2001 From: AkshayaFoiger Date: Thu, 7 Dec 2023 16:08:05 +0530 Subject: [PATCH 45/46] update field name --- .../router/src/connector/riskified/transformers/api.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/router/src/connector/riskified/transformers/api.rs b/crates/router/src/connector/riskified/transformers/api.rs index 6490ee3f7e02..de8884f03909 100644 --- a/crates/router/src/connector/riskified/transformers/api.rs +++ b/crates/router/src/connector/riskified/transformers/api.rs @@ -477,8 +477,8 @@ pub struct FulfilmentData { created_at: PrimitiveDateTime, status: Option, tracking_company: String, - tracking_numbers: String, - tracking_urls: Option, + tracking_number: String, + tracking_url: Option, } impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequest { @@ -504,12 +504,12 @@ impl TryFrom<&frm_types::FrmFulfillmentRouterData> for RiskifiedFullfillmentRequ .ok_or(errors::ConnectorError::MissingRequiredField { field_name: "tracking_company", })?, - tracking_numbers: item.request.fulfillment_req.tracking_number.clone().ok_or( + tracking_number: item.request.fulfillment_req.tracking_number.clone().ok_or( errors::ConnectorError::MissingRequiredField { - field_name: "tracking_numbers", + field_name: "tracking_number", }, )?, - tracking_urls: item.request.fulfillment_req.tracking_url.clone(), + tracking_url: item.request.fulfillment_req.tracking_url.clone(), }, }, }) From 550bc595822945b3f805548a1c3f1a93e7a08b24 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:44:09 +0000 Subject: [PATCH 46/46] chore: run formatter --- crates/router/src/connector.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/router/src/connector.rs b/crates/router/src/connector.rs index 59d105f9f8fe..de6e250842c7 100644 --- a/crates/router/src/connector.rs +++ b/crates/router/src/connector.rs @@ -66,7 +66,8 @@ 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, placetopay::Placetopay, - powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, riskified::Riskified, shift4::Shift4, - signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, trustpay::Trustpay, tsys::Tsys, - volt::Volt, wise::Wise, worldline::Worldline, worldpay::Worldpay, zen::Zen, + powertranz::Powertranz, prophetpay::Prophetpay, rapyd::Rapyd, riskified::Riskified, + shift4::Shift4, signifyd::Signifyd, square::Square, stax::Stax, stripe::Stripe, + trustpay::Trustpay, tsys::Tsys, volt::Volt, wise::Wise, worldline::Worldline, + worldpay::Worldpay, zen::Zen, };