mirror of
				https://github.com/juspay/hyperswitch.git
				synced 2025-10-31 10:06:32 +08:00 
			
		
		
		
	feat(FRM): add missing fields in Signifyd payment request (#4554)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
		| @ -3776,6 +3776,8 @@ pub struct OrderDetailsWithAmount { | ||||
|     pub product_id: Option<String>, | ||||
|     /// Category of the product that is being purchased | ||||
|     pub category: Option<String>, | ||||
|     /// Sub category of the product that is being purchased | ||||
|     pub sub_category: Option<String>, | ||||
|     /// Brand of the product that is being purchased | ||||
|     pub brand: Option<String>, | ||||
|     /// Type of the product that is being purchased | ||||
| @ -3810,6 +3812,8 @@ pub struct OrderDetails { | ||||
|     pub product_id: Option<String>, | ||||
|     /// Category of the product that is being purchased | ||||
|     pub category: Option<String>, | ||||
|     /// Sub category of the product that is being purchased | ||||
|     pub sub_category: Option<String>, | ||||
|     /// Brand of the product that is being purchased | ||||
|     pub brand: Option<String>, | ||||
|     /// Type of the product that is being purchased | ||||
|  | ||||
| @ -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<Vec<(String, request::Maskable<String>)>, 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(), | ||||
|  | ||||
| @ -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<Products>, | ||||
|     shipments: Shipments, | ||||
|     currency: Option<common_enums::Currency>, | ||||
|     total_shipping_cost: Option<i64>, | ||||
|     confirmation_email: Option<Email>, | ||||
|     confirmation_phone: Option<Secret<String>>, | ||||
| } | ||||
|  | ||||
| #[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<String>, | ||||
|     item_category: Option<String>, | ||||
|     item_sub_category: Option<String>, | ||||
|     item_is_digital: Option<bool>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Eq, PartialEq, Clone)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Shipments { | ||||
|     destination: Destination, | ||||
|     fulfillment_method: Option<FulfillmentMethod>, | ||||
| } | ||||
|  | ||||
| #[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<CoverageRequests>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Serialize, Eq, PartialEq, Deserialize, Clone)] | ||||
| #[serde(rename_all(serialize = "camelCase", deserialize = "snake_case"))] | ||||
| pub struct SignifydFrmMetadata { | ||||
|     pub total_shipping_cost: Option<i64>, | ||||
|     pub fulfillment_method: Option<FulfillmentMethod>, | ||||
|     pub coverage_request: Option<CoverageRequests>, | ||||
|     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::<Vec<_>>(); | ||||
|         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<CoverageRequests>, | ||||
| } | ||||
|  | ||||
| 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::<Vec<_>>(); | ||||
|         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, | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -86,11 +86,17 @@ impl ConstructFlowSpecificData<frm_api::Checkout, FraudCheckCheckoutData, FraudC | ||||
|                     }) | ||||
|                     .transpose() | ||||
|                     .unwrap_or_default(), | ||||
|                 email: customer.clone().and_then(|customer_data| { | ||||
|                 email: customer | ||||
|                     .clone() | ||||
|                     .and_then(|customer_data| { | ||||
|                         customer_data | ||||
|                             .email | ||||
|                         .and_then(|email| Email::try_from(email.into_inner().expose()).ok()) | ||||
|                 }), | ||||
|                             .map(|email| Email::try_from(email.into_inner().expose())) | ||||
|                     }) | ||||
|                     .transpose() | ||||
|                     .change_context(errors::ApiErrorResponse::InvalidDataValue { | ||||
|                         field_name: "customer.customer_data.email", | ||||
|                     })?, | ||||
|                 gateway: self.payment_attempt.connector.clone(), | ||||
|             }, // self.order_details | ||||
|             response: Ok(FraudCheckResponseData::TransactionResponse { | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| 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; | ||||
|  | ||||
| use crate::{ | ||||
|     core::{ | ||||
| @ -65,6 +66,18 @@ impl ConstructFlowSpecificData<frm_api::Sale, FraudCheckSaleData, FraudCheckResp | ||||
|             request: FraudCheckSaleData { | ||||
|                 amount: self.payment_attempt.amount, | ||||
|                 order_details: self.order_details.clone(), | ||||
|                 currency: self.payment_attempt.currency, | ||||
|                 email: customer | ||||
|                     .clone() | ||||
|                     .and_then(|customer_data| { | ||||
|                         customer_data | ||||
|                             .email | ||||
|                             .map(|email| Email::try_from(email.into_inner().expose())) | ||||
|                     }) | ||||
|                     .transpose() | ||||
|                     .change_context(errors::ApiErrorResponse::InvalidDataValue { | ||||
|                         field_name: "customer.customer_data.email", | ||||
|                     })?, | ||||
|             }, | ||||
|             response: Ok(FraudCheckResponseData::TransactionResponse { | ||||
|                 resource_id: ResponseId::ConnectorTransactionId("".to_string()), | ||||
| @ -92,7 +105,7 @@ impl ConstructFlowSpecificData<frm_api::Sale, FraudCheckSaleData, FraudCheckResp | ||||
|             external_latency: None, | ||||
|             connector_api_version: None, | ||||
|             apple_pay_flow: None, | ||||
|             frm_metadata: None, | ||||
|             frm_metadata: self.frm_metadata.clone(), | ||||
|             refund_id: None, | ||||
|             dispute_id: None, | ||||
|             connector_response: None, | ||||
|  | ||||
| @ -109,7 +109,7 @@ impl | ||||
|             connector_api_version: None, | ||||
|             payment_method_status: None, | ||||
|             apple_pay_flow: None, | ||||
|             frm_metadata: None, | ||||
|             frm_metadata: self.frm_metadata.clone(), | ||||
|             refund_id: None, | ||||
|             dispute_id: None, | ||||
|             connector_response: None, | ||||
|  | ||||
| @ -173,6 +173,8 @@ impl<F: Send + Clone> Domain<F> 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<F: Send + Clone> Domain<F> 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), | ||||
|         }) | ||||
|  | ||||
| @ -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, | ||||
|     }]) | ||||
|  | ||||
| @ -17,6 +17,8 @@ pub type FrmSaleType = | ||||
| pub struct FraudCheckSaleData { | ||||
|     pub amount: i64, | ||||
|     pub order_details: Option<Vec<api_models::payments::OrderDetailsWithAmount>>, | ||||
|     pub currency: Option<common_enums::Currency>, | ||||
|     pub email: Option<Email>, | ||||
| } | ||||
| #[derive(Debug, Clone)] | ||||
| pub struct FrmRouterData { | ||||
|  | ||||
| @ -84,6 +84,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> { | ||||
|             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, | ||||
|                 }]), | ||||
|  | ||||
| @ -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, | ||||
|                 }]), | ||||
|  | ||||
| @ -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", | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 chikke srujan
					chikke srujan