mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 17:47:54 +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