diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 819e67a73b..8270ddaf3f 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -3776,6 +3776,8 @@ pub struct OrderDetailsWithAmount { pub product_id: Option, /// Category of the product that is being purchased pub category: Option, + /// Sub category of the product that is being purchased + pub sub_category: Option, /// Brand of the product that is being purchased pub brand: Option, /// Type of the product that is being purchased @@ -3810,6 +3812,8 @@ pub struct OrderDetails { pub product_id: Option, /// Category of the product that is being purchased pub category: Option, + /// Sub category of the product that is being purchased + pub sub_category: Option, /// Brand of the product that is being purchased pub brand: Option, /// Type of the product that is being purchased diff --git a/crates/router/src/connector/signifyd.rs b/crates/router/src/connector/signifyd.rs index c28fbc0550..550e1ae30a 100644 --- a/crates/router/src/connector/signifyd.rs +++ b/crates/router/src/connector/signifyd.rs @@ -1,6 +1,7 @@ pub mod transformers; use std::fmt::Debug; +use base64::Engine; #[cfg(feature = "frm")] use common_utils::request::RequestContent; use error_stack::{report, ResultExt}; @@ -9,6 +10,7 @@ use transformers as signifyd; use crate::{ configs::settings, + consts, core::errors::{self, CustomResult}, headers, services::{self, request, ConnectorIntegration, ConnectorValidation}, @@ -65,7 +67,10 @@ impl ConnectorCommon for Signifyd { ) -> 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()); + let auth_api_key = format!( + "Basic {}", + consts::BASE64_ENGINE.encode(auth.api_key.peek()) + ); Ok(vec![( headers::AUTHORIZATION.to_string(), diff --git a/crates/router/src/connector/signifyd/transformers/api.rs b/crates/router/src/connector/signifyd/transformers/api.rs index 684c789be6..b56c7f7011 100644 --- a/crates/router/src/connector/signifyd/transformers/api.rs +++ b/crates/router/src/connector/signifyd/transformers/api.rs @@ -1,6 +1,6 @@ use bigdecimal::ToPrimitive; -use common_utils::pii::Email; -use error_stack; +use common_utils::{ext_traits::ValueExt, pii::Email}; +use error_stack::{self, ResultExt}; use masking::Secret; use serde::{Deserialize, Serialize}; use time::PrimitiveDateTime; @@ -35,10 +35,14 @@ pub struct Purchase { total_price: i64, products: Vec, shipments: Shipments, + currency: Option, + total_shipping_cost: Option, + confirmation_email: Option, + confirmation_phone: Option>, } #[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] -#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE", deserialize = "snake_case"))] pub enum OrderChannel { Web, Phone, @@ -51,17 +55,36 @@ pub enum OrderChannel { Mit, } +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE", deserialize = "snake_case"))] +pub enum FulfillmentMethod { + Delivery, + CounterPickup, + CubsidePickup, + LockerPickup, + StandardShipping, + ExpeditedShipping, + GasPickup, + ScheduledDelivery, +} + #[derive(Debug, Serialize, Eq, PartialEq, Clone)] #[serde(rename_all = "camelCase")] pub struct Products { item_name: String, item_price: i64, item_quantity: i32, + item_id: Option, + item_category: Option, + item_sub_category: Option, + item_is_digital: Option, } #[derive(Debug, Serialize, Eq, PartialEq, Clone)] +#[serde(rename_all = "camelCase")] pub struct Shipments { destination: Destination, + fulfillment_method: Option, } #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] @@ -84,12 +107,32 @@ pub struct Address { country_code: common_enums::CountryAlpha2, } +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE", deserialize = "snake_case"))] +pub enum CoverageRequests { + Fraud, // use when you need a financial guarantee for Payment Fraud. + Inr, // use when you need a financial guarantee for Item Not Received. + Snad, // use when you need a financial guarantee for fraud alleging items are Significantly Not As Described. + All, // use when you need a financial guarantee on all chargebacks. + None, // use when you do not need a financial guarantee. Suggested actions in decision.checkpointAction are recommendations. +} + #[derive(Debug, Serialize, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SignifydPaymentsSaleRequest { order_id: String, purchase: Purchase, decision_delivery: DecisionDelivery, + coverage_requests: Option, +} + +#[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] +#[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))] +pub struct SignifydFrmMetadata { + pub total_shipping_cost: Option, + pub fulfillment_method: Option, + pub coverage_request: Option, + pub order_channel: OrderChannel, } impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { @@ -103,9 +146,25 @@ impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { item_name: order_detail.product_name.clone(), item_price: order_detail.amount, item_quantity: i32::from(order_detail.quantity), + item_id: order_detail.product_id.clone(), + item_category: order_detail.category.clone(), + item_sub_category: order_detail.sub_category.clone(), + item_is_digital: order_detail + .product_type + .as_ref() + .map(|product| (product == &api_models::payments::ProductType::Digital)), }) .collect::>(); + let metadata: SignifydFrmMetadata = item + .frm_metadata + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "frm_metadata", + })? + .parse_value("Signifyd Frm Metadata") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; let ship_address = item.get_shipping_address()?; + let billing_address = item.get_billing()?; let street_addr = ship_address.get_line1()?; let city_addr = ship_address.get_city()?; let zip_code_addr = ship_address.get_zip()?; @@ -128,19 +187,30 @@ impl TryFrom<&frm_types::FrmSaleRouterData> for SignifydPaymentsSaleRequest { }; let created_at = common_utils::date_time::now(); - let order_channel = OrderChannel::Web; - let shipments = Shipments { destination }; + let order_channel = metadata.order_channel; + let shipments = Shipments { + destination, + fulfillment_method: metadata.fulfillment_method, + }; let purchase = Purchase { created_at, order_channel, total_price: item.request.amount, products, shipments, + currency: item.request.currency, + total_shipping_cost: metadata.total_shipping_cost, + confirmation_email: item.request.email.clone(), + confirmation_phone: billing_address + .clone() + .phone + .and_then(|phone_data| phone_data.number), }; Ok(Self { order_id: item.attempt_id.clone(), purchase, - decision_delivery: DecisionDelivery::Sync, + decision_delivery: DecisionDelivery::Sync, // Specify SYNC if you require the Response to contain a decision field. If you have registered for a webhook associated with this checkpoint, then the webhook will also be sent when SYNC is specified. If ASYNC_ONLY is specified, then the decision field in the response will be null, and you will require a Webhook integration to receive Signifyd's final decision + coverage_requests: metadata.coverage_request, }) } } @@ -297,6 +367,7 @@ pub struct SignifydPaymentsCheckoutRequest { checkout_id: String, order_id: String, purchase: Purchase, + coverage_requests: Option, } impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequest { @@ -310,8 +381,23 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ item_name: order_detail.product_name.clone(), item_price: order_detail.amount, item_quantity: i32::from(order_detail.quantity), + item_id: order_detail.product_id.clone(), + item_category: order_detail.category.clone(), + item_sub_category: order_detail.sub_category.clone(), + item_is_digital: order_detail + .product_type + .as_ref() + .map(|product| (product == &api_models::payments::ProductType::Digital)), }) .collect::>(); + let metadata: SignifydFrmMetadata = item + .frm_metadata + .clone() + .ok_or(errors::ConnectorError::MissingRequiredField { + field_name: "frm_metadata", + })? + .parse_value("Signifyd Frm Metadata") + .change_context(errors::ConnectorError::RequestEncodingFailed)?; let ship_address = item.get_shipping_address()?; let street_addr = ship_address.get_line1()?; let city_addr = ship_address.get_city()?; @@ -319,6 +405,7 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ 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 billing_address = item.get_billing()?; let address: Address = Address { street_address: street_addr.clone(), unit: None, @@ -334,19 +421,30 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ address, }; let created_at = common_utils::date_time::now(); - let order_channel = OrderChannel::Web; - let shipments: Shipments = Shipments { destination }; + let order_channel = metadata.order_channel; + let shipments: Shipments = Shipments { + destination, + fulfillment_method: metadata.fulfillment_method, + }; let purchase = Purchase { created_at, order_channel, total_price: item.request.amount, products, shipments, + currency: item.request.currency, + total_shipping_cost: metadata.total_shipping_cost, + confirmation_email: item.request.email.clone(), + confirmation_phone: billing_address + .clone() + .phone + .and_then(|phone_data| phone_data.number), }; Ok(Self { checkout_id: item.payment_id.clone(), order_id: item.attempt_id.clone(), purchase, + coverage_requests: metadata.coverage_request, }) } } 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 58e82ec21c..d0e5376b2c 100644 --- a/crates/router/src/core/fraud_check/flows/checkout_flow.rs +++ b/crates/router/src/core/fraud_check/flows/checkout_flow.rs @@ -86,11 +86,17 @@ impl ConstructFlowSpecificData Domain for FraudCheckPost { request: FrmRequest::Sale(FraudCheckSaleData { amount: router_data.request.amount, order_details: router_data.request.order_details, + currency: router_data.request.currency, + email: router_data.request.email, }), response: FrmResponse::Sale(router_data.response), })) @@ -318,6 +320,8 @@ impl Domain for FraudCheckPost { request: FrmRequest::Sale(FraudCheckSaleData { amount: router_data.request.amount, order_details: router_data.request.order_details, + currency: router_data.request.currency, + email: router_data.request.email, }), response: FrmResponse::Sale(router_data.response), }) diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index d459f632d8..56874e5a9f 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1014,6 +1014,7 @@ pub fn change_order_details_to_new_type( requires_shipping: order_details.requires_shipping, product_id: order_details.product_id, category: order_details.category, + sub_category: order_details.sub_category, brand: order_details.brand, product_type: order_details.product_type, }]) diff --git a/crates/router/src/types/fraud_check.rs b/crates/router/src/types/fraud_check.rs index b6f1d10888..12ab7e4a06 100644 --- a/crates/router/src/types/fraud_check.rs +++ b/crates/router/src/types/fraud_check.rs @@ -17,6 +17,8 @@ pub type FrmSaleType = pub struct FraudCheckSaleData { pub amount: i64, pub order_details: Option>, + pub currency: Option, + pub email: Option, } #[derive(Debug, Clone)] pub struct FrmRouterData { diff --git a/crates/router/tests/connectors/payme.rs b/crates/router/tests/connectors/payme.rs index 5e9531a4f7..a168200148 100644 --- a/crates/router/tests/connectors/payme.rs +++ b/crates/router/tests/connectors/payme.rs @@ -84,6 +84,7 @@ fn payment_method_details() -> Option { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), @@ -383,6 +384,7 @@ async fn should_fail_payment_for_incorrect_cvc() { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), @@ -421,6 +423,7 @@ async fn should_fail_payment_for_invalid_exp_month() { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), @@ -459,6 +462,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), diff --git a/crates/router/tests/connectors/zen.rs b/crates/router/tests/connectors/zen.rs index 95017e7da0..e076aee42c 100644 --- a/crates/router/tests/connectors/zen.rs +++ b/crates/router/tests/connectors/zen.rs @@ -322,6 +322,7 @@ async fn should_fail_payment_for_incorrect_card_number() { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), @@ -363,6 +364,7 @@ async fn should_fail_payment_for_incorrect_cvc() { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), @@ -404,6 +406,7 @@ async fn should_fail_payment_for_invalid_exp_month() { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), @@ -445,6 +448,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() { requires_shipping: None, product_id: None, category: None, + sub_category: None, brand: None, product_type: None, }]), diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 586fd1c111..70e4715183 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -11658,6 +11658,11 @@ "description": "Category of the product that is being purchased", "nullable": true }, + "sub_category": { + "type": "string", + "description": "Sub category of the product that is being purchased", + "nullable": true + }, "brand": { "type": "string", "description": "Brand of the product that is being purchased", @@ -11718,6 +11723,11 @@ "description": "Category of the product that is being purchased", "nullable": true }, + "sub_category": { + "type": "string", + "description": "Sub category of the product that is being purchased", + "nullable": true + }, "brand": { "type": "string", "description": "Brand of the product that is being purchased",