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:
chikke srujan
2024-05-07 11:28:04 +05:30
committed by GitHub
parent 76b76eccc6
commit df2c2ca22d
12 changed files with 168 additions and 17 deletions

View File

@ -3776,6 +3776,8 @@ pub struct OrderDetailsWithAmount {
pub product_id: Option<String>, pub product_id: Option<String>,
/// Category of the product that is being purchased /// Category of the product that is being purchased
pub category: Option<String>, 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 /// Brand of the product that is being purchased
pub brand: Option<String>, pub brand: Option<String>,
/// Type of the product that is being purchased /// Type of the product that is being purchased
@ -3810,6 +3812,8 @@ pub struct OrderDetails {
pub product_id: Option<String>, pub product_id: Option<String>,
/// Category of the product that is being purchased /// Category of the product that is being purchased
pub category: Option<String>, 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 /// Brand of the product that is being purchased
pub brand: Option<String>, pub brand: Option<String>,
/// Type of the product that is being purchased /// Type of the product that is being purchased

View File

@ -1,6 +1,7 @@
pub mod transformers; pub mod transformers;
use std::fmt::Debug; use std::fmt::Debug;
use base64::Engine;
#[cfg(feature = "frm")] #[cfg(feature = "frm")]
use common_utils::request::RequestContent; use common_utils::request::RequestContent;
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
@ -9,6 +10,7 @@ use transformers as signifyd;
use crate::{ use crate::{
configs::settings, configs::settings,
consts,
core::errors::{self, CustomResult}, core::errors::{self, CustomResult},
headers, headers,
services::{self, request, ConnectorIntegration, ConnectorValidation}, services::{self, request, ConnectorIntegration, ConnectorValidation},
@ -65,7 +67,10 @@ impl ConnectorCommon for Signifyd {
) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> { ) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
let auth = signifyd::SignifydAuthType::try_from(auth_type) let auth = signifyd::SignifydAuthType::try_from(auth_type)
.change_context(errors::ConnectorError::FailedToObtainAuthType)?; .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![( Ok(vec![(
headers::AUTHORIZATION.to_string(), headers::AUTHORIZATION.to_string(),

View File

@ -1,6 +1,6 @@
use bigdecimal::ToPrimitive; use bigdecimal::ToPrimitive;
use common_utils::pii::Email; use common_utils::{ext_traits::ValueExt, pii::Email};
use error_stack; use error_stack::{self, ResultExt};
use masking::Secret; use masking::Secret;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
@ -35,10 +35,14 @@ pub struct Purchase {
total_price: i64, total_price: i64,
products: Vec<Products>, products: Vec<Products>,
shipments: Shipments, 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)] #[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 { pub enum OrderChannel {
Web, Web,
Phone, Phone,
@ -51,17 +55,36 @@ pub enum OrderChannel {
Mit, 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)] #[derive(Debug, Serialize, Eq, PartialEq, Clone)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Products { pub struct Products {
item_name: String, item_name: String,
item_price: i64, item_price: i64,
item_quantity: i32, 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)] #[derive(Debug, Serialize, Eq, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Shipments { pub struct Shipments {
destination: Destination, destination: Destination,
fulfillment_method: Option<FulfillmentMethod>,
} }
#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)] #[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
@ -84,12 +107,32 @@ pub struct Address {
country_code: common_enums::CountryAlpha2, 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)] #[derive(Debug, Serialize, Eq, PartialEq)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SignifydPaymentsSaleRequest { pub struct SignifydPaymentsSaleRequest {
order_id: String, order_id: String,
purchase: Purchase, purchase: Purchase,
decision_delivery: DecisionDelivery, 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 { 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_name: order_detail.product_name.clone(),
item_price: order_detail.amount, item_price: order_detail.amount,
item_quantity: i32::from(order_detail.quantity), 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<_>>(); .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 ship_address = item.get_shipping_address()?;
let billing_address = item.get_billing()?;
let street_addr = ship_address.get_line1()?; let street_addr = ship_address.get_line1()?;
let city_addr = ship_address.get_city()?; let city_addr = ship_address.get_city()?;
let zip_code_addr = ship_address.get_zip()?; 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 created_at = common_utils::date_time::now();
let order_channel = OrderChannel::Web; let order_channel = metadata.order_channel;
let shipments = Shipments { destination }; let shipments = Shipments {
destination,
fulfillment_method: metadata.fulfillment_method,
};
let purchase = Purchase { let purchase = Purchase {
created_at, created_at,
order_channel, order_channel,
total_price: item.request.amount, total_price: item.request.amount,
products, products,
shipments, 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 { Ok(Self {
order_id: item.attempt_id.clone(), order_id: item.attempt_id.clone(),
purchase, 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, checkout_id: String,
order_id: String, order_id: String,
purchase: Purchase, purchase: Purchase,
coverage_requests: Option<CoverageRequests>,
} }
impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequest { 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_name: order_detail.product_name.clone(),
item_price: order_detail.amount, item_price: order_detail.amount,
item_quantity: i32::from(order_detail.quantity), 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<_>>(); .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 ship_address = item.get_shipping_address()?;
let street_addr = ship_address.get_line1()?; let street_addr = ship_address.get_line1()?;
let city_addr = ship_address.get_city()?; 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 country_code_addr = ship_address.get_country()?;
let _first_name_addr = ship_address.get_first_name()?; let _first_name_addr = ship_address.get_first_name()?;
let _last_name_addr = ship_address.get_last_name()?; let _last_name_addr = ship_address.get_last_name()?;
let billing_address = item.get_billing()?;
let address: Address = Address { let address: Address = Address {
street_address: street_addr.clone(), street_address: street_addr.clone(),
unit: None, unit: None,
@ -334,19 +421,30 @@ impl TryFrom<&frm_types::FrmCheckoutRouterData> for SignifydPaymentsCheckoutRequ
address, address,
}; };
let created_at = common_utils::date_time::now(); let created_at = common_utils::date_time::now();
let order_channel = OrderChannel::Web; let order_channel = metadata.order_channel;
let shipments: Shipments = Shipments { destination }; let shipments: Shipments = Shipments {
destination,
fulfillment_method: metadata.fulfillment_method,
};
let purchase = Purchase { let purchase = Purchase {
created_at, created_at,
order_channel, order_channel,
total_price: item.request.amount, total_price: item.request.amount,
products, products,
shipments, 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 { Ok(Self {
checkout_id: item.payment_id.clone(), checkout_id: item.payment_id.clone(),
order_id: item.attempt_id.clone(), order_id: item.attempt_id.clone(),
purchase, purchase,
coverage_requests: metadata.coverage_request,
}) })
} }
} }

View File

@ -86,11 +86,17 @@ impl ConstructFlowSpecificData<frm_api::Checkout, FraudCheckCheckoutData, FraudC
}) })
.transpose() .transpose()
.unwrap_or_default(), .unwrap_or_default(),
email: customer.clone().and_then(|customer_data| { email: customer
customer_data .clone()
.email .and_then(|customer_data| {
.and_then(|email| Email::try_from(email.into_inner().expose()).ok()) customer_data
}), .email
.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(), gateway: self.payment_attempt.connector.clone(),
}, // self.order_details }, // self.order_details
response: Ok(FraudCheckResponseData::TransactionResponse { response: Ok(FraudCheckResponseData::TransactionResponse {

View File

@ -1,6 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use common_utils::ext_traits::ValueExt; use common_utils::{ext_traits::ValueExt, pii::Email};
use error_stack::ResultExt; use error_stack::ResultExt;
use masking::ExposeInterface;
use crate::{ use crate::{
core::{ core::{
@ -65,6 +66,18 @@ impl ConstructFlowSpecificData<frm_api::Sale, FraudCheckSaleData, FraudCheckResp
request: FraudCheckSaleData { request: FraudCheckSaleData {
amount: self.payment_attempt.amount, amount: self.payment_attempt.amount,
order_details: self.order_details.clone(), 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 { response: Ok(FraudCheckResponseData::TransactionResponse {
resource_id: ResponseId::ConnectorTransactionId("".to_string()), resource_id: ResponseId::ConnectorTransactionId("".to_string()),
@ -92,7 +105,7 @@ impl ConstructFlowSpecificData<frm_api::Sale, FraudCheckSaleData, FraudCheckResp
external_latency: None, external_latency: None,
connector_api_version: None, connector_api_version: None,
apple_pay_flow: None, apple_pay_flow: None,
frm_metadata: None, frm_metadata: self.frm_metadata.clone(),
refund_id: None, refund_id: None,
dispute_id: None, dispute_id: None,
connector_response: None, connector_response: None,

View File

@ -109,7 +109,7 @@ impl
connector_api_version: None, connector_api_version: None,
payment_method_status: None, payment_method_status: None,
apple_pay_flow: None, apple_pay_flow: None,
frm_metadata: None, frm_metadata: self.frm_metadata.clone(),
refund_id: None, refund_id: None,
dispute_id: None, dispute_id: None,
connector_response: None, connector_response: None,

View File

@ -173,6 +173,8 @@ impl<F: Send + Clone> Domain<F> for FraudCheckPost {
request: FrmRequest::Sale(FraudCheckSaleData { request: FrmRequest::Sale(FraudCheckSaleData {
amount: router_data.request.amount, amount: router_data.request.amount,
order_details: router_data.request.order_details, order_details: router_data.request.order_details,
currency: router_data.request.currency,
email: router_data.request.email,
}), }),
response: FrmResponse::Sale(router_data.response), response: FrmResponse::Sale(router_data.response),
})) }))
@ -318,6 +320,8 @@ impl<F: Send + Clone> Domain<F> for FraudCheckPost {
request: FrmRequest::Sale(FraudCheckSaleData { request: FrmRequest::Sale(FraudCheckSaleData {
amount: router_data.request.amount, amount: router_data.request.amount,
order_details: router_data.request.order_details, order_details: router_data.request.order_details,
currency: router_data.request.currency,
email: router_data.request.email,
}), }),
response: FrmResponse::Sale(router_data.response), response: FrmResponse::Sale(router_data.response),
}) })

View File

@ -1014,6 +1014,7 @@ pub fn change_order_details_to_new_type(
requires_shipping: order_details.requires_shipping, requires_shipping: order_details.requires_shipping,
product_id: order_details.product_id, product_id: order_details.product_id,
category: order_details.category, category: order_details.category,
sub_category: order_details.sub_category,
brand: order_details.brand, brand: order_details.brand,
product_type: order_details.product_type, product_type: order_details.product_type,
}]) }])

View File

@ -17,6 +17,8 @@ pub type FrmSaleType =
pub struct FraudCheckSaleData { pub struct FraudCheckSaleData {
pub amount: i64, pub amount: i64,
pub order_details: Option<Vec<api_models::payments::OrderDetailsWithAmount>>, pub order_details: Option<Vec<api_models::payments::OrderDetailsWithAmount>>,
pub currency: Option<common_enums::Currency>,
pub email: Option<Email>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FrmRouterData { pub struct FrmRouterData {

View File

@ -84,6 +84,7 @@ fn payment_method_details() -> Option<types::PaymentsAuthorizeData> {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),
@ -383,6 +384,7 @@ async fn should_fail_payment_for_incorrect_cvc() {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),
@ -421,6 +423,7 @@ async fn should_fail_payment_for_invalid_exp_month() {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),
@ -459,6 +462,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),

View File

@ -322,6 +322,7 @@ async fn should_fail_payment_for_incorrect_card_number() {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),
@ -363,6 +364,7 @@ async fn should_fail_payment_for_incorrect_cvc() {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),
@ -404,6 +406,7 @@ async fn should_fail_payment_for_invalid_exp_month() {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),
@ -445,6 +448,7 @@ async fn should_fail_payment_for_incorrect_expiry_year() {
requires_shipping: None, requires_shipping: None,
product_id: None, product_id: None,
category: None, category: None,
sub_category: None,
brand: None, brand: None,
product_type: None, product_type: None,
}]), }]),

View File

@ -11658,6 +11658,11 @@
"description": "Category of the product that is being purchased", "description": "Category of the product that is being purchased",
"nullable": true "nullable": true
}, },
"sub_category": {
"type": "string",
"description": "Sub category of the product that is being purchased",
"nullable": true
},
"brand": { "brand": {
"type": "string", "type": "string",
"description": "Brand of the product that is being purchased", "description": "Brand of the product that is being purchased",
@ -11718,6 +11723,11 @@
"description": "Category of the product that is being purchased", "description": "Category of the product that is being purchased",
"nullable": true "nullable": true
}, },
"sub_category": {
"type": "string",
"description": "Sub category of the product that is being purchased",
"nullable": true
},
"brand": { "brand": {
"type": "string", "type": "string",
"description": "Brand of the product that is being purchased", "description": "Brand of the product that is being purchased",