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>,
/// 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

View File

@ -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(),

View File

@ -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,
})
}
}

View File

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

View File

@ -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,

View File

@ -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,

View File

@ -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),
})

View File

@ -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,
}])

View File

@ -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 {

View File

@ -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,
}]),

View File

@ -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,
}]),

View File

@ -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",