mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
feat(FRM): Revise post FRM core flows (#4394)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -2249,7 +2249,7 @@ pub enum FrmSuggestion {
|
||||
#[default]
|
||||
FrmCancelTransaction,
|
||||
FrmManualReview,
|
||||
FrmAutoRefund,
|
||||
FrmAuthorizeTransaction, // When manual capture payment which was marked fraud and held, when approved needs to be authorized.
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
||||
@ -303,6 +303,7 @@ pub enum PaymentAttemptUpdate {
|
||||
currency: storage_enums::Currency,
|
||||
status: storage_enums::AttemptStatus,
|
||||
authentication_type: Option<storage_enums::AuthenticationType>,
|
||||
capture_method: Option<storage_enums::CaptureMethod>,
|
||||
payment_method: Option<storage_enums::PaymentMethod>,
|
||||
browser_info: Option<serde_json::Value>,
|
||||
connector: Option<String>,
|
||||
|
||||
@ -182,6 +182,7 @@ pub enum PaymentIntentUpdate {
|
||||
updated_by: String,
|
||||
},
|
||||
ApproveUpdate {
|
||||
status: storage_enums::IntentStatus,
|
||||
merchant_decision: Option<String>,
|
||||
updated_by: String,
|
||||
},
|
||||
@ -382,9 +383,11 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
|
||||
..Default::default()
|
||||
},
|
||||
PaymentIntentUpdate::ApproveUpdate {
|
||||
status,
|
||||
merchant_decision,
|
||||
updated_by,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
merchant_decision,
|
||||
updated_by,
|
||||
..Default::default()
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
use common_enums as storage_enums;
|
||||
use diesel::{AsChangeset, Identifiable, Insertable, Queryable};
|
||||
use masking::{Deserialize, Serialize};
|
||||
use time::PrimitiveDateTime;
|
||||
@ -25,6 +26,7 @@ pub struct FraudCheck {
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub modified_at: PrimitiveDateTime,
|
||||
pub last_step: FraudCheckLastStep,
|
||||
pub payment_capture_method: Option<storage_enums::CaptureMethod>, // In postFrm, we are updating capture method from automatic to manual. To store the merchant actual capture method, we are storing the actual capture method in payment_capture_method. It will be useful while approving the FRM decision.
|
||||
}
|
||||
|
||||
#[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
|
||||
@ -46,6 +48,7 @@ pub struct FraudCheckNew {
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub modified_at: PrimitiveDateTime,
|
||||
pub last_step: FraudCheckLastStep,
|
||||
pub payment_capture_method: Option<storage_enums::CaptureMethod>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@ -59,6 +62,7 @@ pub enum FraudCheckUpdate {
|
||||
metadata: Option<serde_json::Value>,
|
||||
modified_at: PrimitiveDateTime,
|
||||
last_step: FraudCheckLastStep,
|
||||
payment_capture_method: Option<storage_enums::CaptureMethod>,
|
||||
},
|
||||
ErrorUpdate {
|
||||
status: FraudCheckStatus,
|
||||
@ -76,6 +80,7 @@ pub struct FraudCheckUpdateInternal {
|
||||
frm_error: Option<Option<String>>,
|
||||
metadata: Option<serde_json::Value>,
|
||||
last_step: FraudCheckLastStep,
|
||||
payment_capture_method: Option<storage_enums::CaptureMethod>,
|
||||
}
|
||||
|
||||
impl From<FraudCheckUpdate> for FraudCheckUpdateInternal {
|
||||
@ -89,6 +94,7 @@ impl From<FraudCheckUpdate> for FraudCheckUpdateInternal {
|
||||
metadata,
|
||||
modified_at: _,
|
||||
last_step,
|
||||
payment_capture_method,
|
||||
} => Self {
|
||||
frm_status: Some(frm_status),
|
||||
frm_transaction_id,
|
||||
@ -96,6 +102,7 @@ impl From<FraudCheckUpdate> for FraudCheckUpdateInternal {
|
||||
frm_score,
|
||||
metadata,
|
||||
last_step,
|
||||
payment_capture_method,
|
||||
..Default::default()
|
||||
},
|
||||
FraudCheckUpdate::ErrorUpdate {
|
||||
|
||||
@ -209,6 +209,7 @@ pub enum PaymentAttemptUpdate {
|
||||
currency: storage_enums::Currency,
|
||||
status: storage_enums::AttemptStatus,
|
||||
authentication_type: Option<storage_enums::AuthenticationType>,
|
||||
capture_method: Option<storage_enums::CaptureMethod>,
|
||||
payment_method: Option<storage_enums::PaymentMethod>,
|
||||
browser_info: Option<serde_json::Value>,
|
||||
connector: Option<String>,
|
||||
@ -559,6 +560,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
amount,
|
||||
currency,
|
||||
authentication_type,
|
||||
capture_method,
|
||||
status,
|
||||
payment_method,
|
||||
browser_info,
|
||||
@ -610,6 +612,7 @@ impl From<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
|
||||
payment_method_billing_address_id,
|
||||
fingerprint_id,
|
||||
payment_method_id,
|
||||
capture_method,
|
||||
..Default::default()
|
||||
},
|
||||
PaymentAttemptUpdate::VoidUpdate {
|
||||
|
||||
@ -180,6 +180,7 @@ pub enum PaymentIntentUpdate {
|
||||
updated_by: String,
|
||||
},
|
||||
ApproveUpdate {
|
||||
status: storage_enums::IntentStatus,
|
||||
merchant_decision: Option<String>,
|
||||
updated_by: String,
|
||||
},
|
||||
@ -456,9 +457,11 @@ impl From<PaymentIntentUpdate> for PaymentIntentUpdateInternal {
|
||||
..Default::default()
|
||||
},
|
||||
PaymentIntentUpdate::ApproveUpdate {
|
||||
status,
|
||||
merchant_decision,
|
||||
updated_by,
|
||||
} => Self {
|
||||
status: Some(status),
|
||||
merchant_decision,
|
||||
updated_by,
|
||||
..Default::default()
|
||||
|
||||
@ -450,6 +450,7 @@ diesel::table! {
|
||||
modified_at -> Timestamp,
|
||||
#[max_length = 64]
|
||||
last_step -> Varchar,
|
||||
payment_capture_method -> Nullable<CaptureMethod>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use api_models::{admin::FrmConfigs, enums as api_enums, payments::AdditionalPaymentData};
|
||||
use common_enums::CaptureMethod;
|
||||
use error_stack::ResultExt;
|
||||
use masking::{ExposeInterface, PeekInterface};
|
||||
use router_env::{
|
||||
@ -26,11 +27,14 @@ use crate::{
|
||||
utils as core_utils,
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::AppState,
|
||||
routes::{app::ReqState, AppState},
|
||||
services,
|
||||
types::{
|
||||
self as oss_types,
|
||||
api::{routing::FrmRoutingAlgorithm, Connector, FraudCheckConnectorData, Fulfillment},
|
||||
api::{
|
||||
fraud_check as frm_api, routing::FrmRoutingAlgorithm, Connector,
|
||||
FraudCheckConnectorData, Fulfillment,
|
||||
},
|
||||
domain, fraud_check as frm_types,
|
||||
storage::{
|
||||
enums::{
|
||||
@ -94,6 +98,15 @@ where
|
||||
.await?;
|
||||
|
||||
router_data.status = payment_data.payment_attempt.status;
|
||||
if matches!(
|
||||
frm_data.fraud_check.frm_transaction_type,
|
||||
FraudCheckType::PreFrm
|
||||
) && matches!(
|
||||
frm_data.fraud_check.last_step,
|
||||
FraudCheckLastStep::CheckoutOrSale
|
||||
) {
|
||||
frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund
|
||||
}
|
||||
|
||||
let connector =
|
||||
FraudCheckConnectorData::get_connector_by_name(&frm_data.connector_details.connector_name)?;
|
||||
@ -295,7 +308,7 @@ where
|
||||
let is_frm_enabled =
|
||||
is_frm_connector_enabled && is_frm_pm_enabled && is_frm_pmt_enabled;
|
||||
logger::debug!(
|
||||
"frm_configs {:?} {:?} {:?} {:?}",
|
||||
"is_frm_connector_enabled {:?}, is_frm_pm_enabled: {:?},is_frm_pmt_enabled : {:?}, is_frm_enabled :{:?}",
|
||||
is_frm_connector_enabled,
|
||||
is_frm_pm_enabled,
|
||||
is_frm_pmt_enabled,
|
||||
@ -423,7 +436,7 @@ where
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn pre_payment_frm_core<'a, F>(
|
||||
pub async fn pre_payment_frm_core<'a, F, Req, Ctx>(
|
||||
state: &AppState,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
payment_data: &mut payments::PaymentData<F>,
|
||||
@ -433,11 +446,14 @@ pub async fn pre_payment_frm_core<'a, F>(
|
||||
should_continue_transaction: &mut bool,
|
||||
should_continue_capture: &mut bool,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
operation: &BoxedOperation<'_, F, Req, Ctx>,
|
||||
) -> RouterResult<Option<FrmData>>
|
||||
where
|
||||
F: Send + Clone,
|
||||
{
|
||||
if let Some(frm_data) = &mut frm_info.frm_data {
|
||||
let mut frm_data = None;
|
||||
if is_operation_allowed(operation) {
|
||||
frm_data = if let Some(frm_data) = &mut frm_info.frm_data {
|
||||
if matches!(
|
||||
frm_configs.frm_preferred_flow_type,
|
||||
api_enums::FrmPreferredFlowTypes::Pre
|
||||
@ -452,7 +468,16 @@ where
|
||||
frm_data,
|
||||
merchant_account,
|
||||
customer,
|
||||
key_store,
|
||||
key_store.clone(),
|
||||
)
|
||||
.await?;
|
||||
let _router_data = call_frm_service::<F, frm_api::Transaction, _>(
|
||||
state,
|
||||
payment_data,
|
||||
frm_data,
|
||||
merchant_account,
|
||||
&key_store,
|
||||
customer,
|
||||
)
|
||||
.await?;
|
||||
let frm_data_updated = fraud_check_operation
|
||||
@ -468,8 +493,8 @@ where
|
||||
let frm_fraud_check = frm_data_updated.fraud_check.clone();
|
||||
payment_data.frm_message = Some(frm_fraud_check.clone());
|
||||
if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) {
|
||||
if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) {
|
||||
*should_continue_transaction = false;
|
||||
if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) {
|
||||
frm_info.suggested_action = Some(FrmSuggestion::FrmCancelTransaction);
|
||||
} else if matches!(frm_configs.frm_action, api_enums::FrmAction::ManualReview) {
|
||||
*should_continue_capture = false;
|
||||
@ -481,37 +506,48 @@ where
|
||||
frm_info.fraud_check_operation,
|
||||
frm_info.suggested_action
|
||||
);
|
||||
Ok(Some(frm_data_updated))
|
||||
Some(frm_data_updated)
|
||||
} else if matches!(
|
||||
frm_configs.frm_preferred_flow_type,
|
||||
api_enums::FrmPreferredFlowTypes::Post
|
||||
) {
|
||||
*should_continue_capture = false;
|
||||
Some(frm_data.to_owned())
|
||||
} else {
|
||||
Ok(Some(frm_data.to_owned()))
|
||||
Some(frm_data.to_owned())
|
||||
}
|
||||
} else {
|
||||
Ok(None)
|
||||
None
|
||||
};
|
||||
}
|
||||
Ok(frm_data)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub async fn post_payment_frm_core<'a, F>(
|
||||
state: &AppState,
|
||||
req_state: ReqState,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
payment_data: &mut payments::PaymentData<F>,
|
||||
frm_info: &mut FrmInfo<F>,
|
||||
frm_configs: FrmConfigsObject,
|
||||
customer: &Option<domain::Customer>,
|
||||
key_store: domain::MerchantKeyStore,
|
||||
should_continue_capture: &mut bool,
|
||||
) -> RouterResult<Option<FrmData>>
|
||||
where
|
||||
F: Send + Clone,
|
||||
{
|
||||
if let Some(frm_data) = &mut frm_info.frm_data {
|
||||
// Allow the Post flow only if the payment is succeeded,
|
||||
// Allow the Post flow only if the payment is authorized,
|
||||
// this logic has to be removed if we are going to call /sale or /transaction after failed transaction
|
||||
let fraud_check_operation = &mut frm_info.fraud_check_operation;
|
||||
if payment_data.payment_attempt.status == AttemptStatus::Charged {
|
||||
if payment_data.payment_attempt.status == AttemptStatus::Authorized {
|
||||
let frm_router_data_opt = fraud_check_operation
|
||||
.to_domain()?
|
||||
.post_payment_frm(
|
||||
state,
|
||||
req_state.clone(),
|
||||
payment_data,
|
||||
frm_data,
|
||||
merchant_account,
|
||||
@ -530,18 +566,23 @@ where
|
||||
frm_router_data.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
payment_data.frm_message = Some(frm_data.fraud_check.clone());
|
||||
logger::debug!(
|
||||
"frm_updated_data: {:?} {:?}",
|
||||
frm_data,
|
||||
payment_data.frm_message
|
||||
);
|
||||
let frm_fraud_check = frm_data.fraud_check.clone();
|
||||
let mut frm_suggestion = None;
|
||||
payment_data.frm_message = Some(frm_fraud_check.clone());
|
||||
if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) {
|
||||
if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) {
|
||||
frm_info.suggested_action = Some(FrmSuggestion::FrmCancelTransaction);
|
||||
} else if matches!(frm_configs.frm_action, api_enums::FrmAction::ManualReview) {
|
||||
frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview);
|
||||
}
|
||||
} else if matches!(frm_fraud_check.frm_status, FraudCheckStatus::ManualReview) {
|
||||
frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview);
|
||||
}
|
||||
fraud_check_operation
|
||||
.to_domain()?
|
||||
.execute_post_tasks(
|
||||
state,
|
||||
req_state,
|
||||
&mut frm_data,
|
||||
merchant_account,
|
||||
frm_configs,
|
||||
@ -549,6 +590,7 @@ where
|
||||
key_store,
|
||||
payment_data,
|
||||
customer,
|
||||
should_continue_capture,
|
||||
)
|
||||
.await?;
|
||||
logger::debug!("frm_post_tasks_data: {:?}", frm_data);
|
||||
@ -588,7 +630,6 @@ pub async fn call_frm_before_connector_call<'a, F, Req, Ctx>(
|
||||
where
|
||||
F: Send + Clone,
|
||||
{
|
||||
if is_operation_allowed(operation) {
|
||||
let (is_frm_enabled, frm_routing_algorithm, frm_connector_label, frm_configs) =
|
||||
should_call_frm(merchant_account, payment_data, db, key_store.clone()).await?;
|
||||
if let Some((frm_routing_algorithm_val, profile_id)) =
|
||||
@ -618,20 +659,46 @@ where
|
||||
should_continue_transaction,
|
||||
should_continue_capture,
|
||||
key_store,
|
||||
operation,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
*frm_info = Some(updated_frm_info);
|
||||
}
|
||||
}
|
||||
let fraud_capture_method = frm_info.as_ref().and_then(|frm_info| {
|
||||
frm_info
|
||||
.frm_data
|
||||
.as_ref()
|
||||
.map(|frm_data| frm_data.fraud_check.payment_capture_method)
|
||||
});
|
||||
if matches!(fraud_capture_method, Some(Some(CaptureMethod::Manual)))
|
||||
&& matches!(
|
||||
payment_data.payment_attempt.status,
|
||||
api_models::enums::AttemptStatus::Unresolved
|
||||
)
|
||||
{
|
||||
if let Some(info) = frm_info {
|
||||
info.suggested_action = Some(FrmSuggestion::FrmAuthorizeTransaction)
|
||||
};
|
||||
*should_continue_transaction = false;
|
||||
logger::debug!(
|
||||
"skipping connector call since payment_capture_method is already {:?}",
|
||||
fraud_capture_method
|
||||
);
|
||||
};
|
||||
logger::debug!("frm_configs: {:?} {:?}", frm_configs, is_frm_enabled);
|
||||
return Ok(frm_configs);
|
||||
}
|
||||
Ok(None)
|
||||
Ok(frm_configs)
|
||||
}
|
||||
|
||||
pub fn is_operation_allowed<Op: Debug>(operation: &Op) -> bool {
|
||||
!["PaymentSession", "PaymentApprove", "PaymentReject"]
|
||||
![
|
||||
"PaymentSession",
|
||||
"PaymentApprove",
|
||||
"PaymentReject",
|
||||
"PaymentCapture",
|
||||
"PaymentsCancel",
|
||||
]
|
||||
.contains(&format!("{operation:?}").as_str())
|
||||
}
|
||||
|
||||
@ -759,6 +826,7 @@ pub async fn make_fulfillment_api_call(
|
||||
metadata: fraud_check.metadata,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: FraudCheckLastStep::Fulfillment,
|
||||
payment_capture_method: fraud_check.payment_capture_method,
|
||||
};
|
||||
let _updated = db
|
||||
.update_fraud_check_response_with_attempt_id(fraud_check_copy, fraud_check_update)
|
||||
|
||||
@ -15,7 +15,7 @@ use crate::{
|
||||
payments,
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::AppState,
|
||||
routes::{app::ReqState, AppState},
|
||||
types::{domain, fraud_check::FrmRouterData},
|
||||
};
|
||||
|
||||
@ -47,10 +47,12 @@ pub trait GetTracker<D>: Send {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub trait Domain<F>: Send + Sync {
|
||||
async fn post_payment_frm<'a>(
|
||||
&'a self,
|
||||
state: &'a AppState,
|
||||
req_state: ReqState,
|
||||
payment_data: &mut payments::PaymentData<F>,
|
||||
frm_data: &mut FrmData,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
@ -78,6 +80,7 @@ pub trait Domain<F>: Send + Sync {
|
||||
async fn execute_post_tasks(
|
||||
&self,
|
||||
_state: &AppState,
|
||||
_req_state: ReqState,
|
||||
frm_data: &mut FrmData,
|
||||
_merchant_account: &domain::MerchantAccount,
|
||||
_frm_configs: FrmConfigsObject,
|
||||
@ -85,6 +88,7 @@ pub trait Domain<F>: Send + Sync {
|
||||
_key_store: domain::MerchantKeyStore,
|
||||
_payment_data: &mut payments::PaymentData<F>,
|
||||
_customer: &Option<domain::Customer>,
|
||||
_should_continue_capture: &mut bool,
|
||||
) -> RouterResult<Option<FrmData>>
|
||||
where
|
||||
F: Send + Clone,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
use api_models::payments::HeaderPayload;
|
||||
use async_trait::async_trait;
|
||||
use common_enums::FrmSuggestion;
|
||||
use common_enums::{CaptureMethod, FrmSuggestion};
|
||||
use common_utils::ext_traits::Encode;
|
||||
use data_models::payments::{
|
||||
payment_attempt::PaymentAttemptUpdate, payment_intent::PaymentIntentUpdate,
|
||||
@ -13,18 +14,20 @@ use crate::{
|
||||
errors::{RouterResult, StorageErrorExt},
|
||||
fraud_check::{
|
||||
self as frm_core,
|
||||
types::{FrmData, PaymentDetails, PaymentToFrmData, REFUND_INITIATED},
|
||||
types::{FrmData, PaymentDetails, PaymentToFrmData, CANCEL_INITIATED},
|
||||
ConnectorDetailsCore, FrmConfigsObject,
|
||||
},
|
||||
payments, refunds,
|
||||
payment_methods::Oss,
|
||||
payments,
|
||||
},
|
||||
db::StorageInterface,
|
||||
errors, services,
|
||||
errors,
|
||||
routes::app::ReqState,
|
||||
services::{self, api},
|
||||
types::{
|
||||
api::{
|
||||
enums::{AttemptStatus, FrmAction, IntentStatus},
|
||||
fraud_check as frm_api,
|
||||
refunds::{RefundRequest, RefundType},
|
||||
fraud_check as frm_api, payments as payment_types, Capture, Void,
|
||||
},
|
||||
domain,
|
||||
fraud_check::{
|
||||
@ -107,6 +110,7 @@ impl GetTracker<PaymentToFrmData> for FraudCheckPost {
|
||||
metadata: None,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: FraudCheckLastStep::Processing,
|
||||
payment_capture_method: payment_data.payment_attempt.capture_method,
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -140,6 +144,7 @@ impl<F: Send + Clone> Domain<F> for FraudCheckPost {
|
||||
async fn post_payment_frm<'a>(
|
||||
&'a self,
|
||||
state: &'a AppState,
|
||||
_req_state: ReqState,
|
||||
payment_data: &mut payments::PaymentData<F>,
|
||||
frm_data: &mut FrmData,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
@ -177,6 +182,7 @@ impl<F: Send + Clone> Domain<F> for FraudCheckPost {
|
||||
async fn execute_post_tasks(
|
||||
&self,
|
||||
state: &AppState,
|
||||
req_state: ReqState,
|
||||
frm_data: &mut FrmData,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
frm_configs: FrmConfigsObject,
|
||||
@ -184,38 +190,47 @@ impl<F: Send + Clone> Domain<F> for FraudCheckPost {
|
||||
key_store: domain::MerchantKeyStore,
|
||||
payment_data: &mut payments::PaymentData<F>,
|
||||
customer: &Option<domain::Customer>,
|
||||
_should_continue_capture: &mut bool,
|
||||
) -> RouterResult<Option<FrmData>> {
|
||||
if matches!(frm_data.fraud_check.frm_status, FraudCheckStatus::Fraud)
|
||||
&& matches!(frm_configs.frm_action, FrmAction::AutoRefund)
|
||||
&& matches!(frm_configs.frm_action, FrmAction::CancelTxn)
|
||||
&& matches!(
|
||||
frm_data.fraud_check.last_step,
|
||||
FraudCheckLastStep::CheckoutOrSale
|
||||
)
|
||||
{
|
||||
*frm_suggestion = Some(FrmSuggestion::FrmAutoRefund);
|
||||
let ref_req = RefundRequest {
|
||||
refund_id: None,
|
||||
payment_id: payment_data.payment_intent.payment_id.clone(),
|
||||
merchant_id: Some(merchant_account.merchant_id.clone()),
|
||||
amount: None,
|
||||
reason: frm_data
|
||||
.fraud_check
|
||||
.frm_reason
|
||||
.clone()
|
||||
.map(|data| data.to_string()),
|
||||
refund_type: Some(RefundType::Instant),
|
||||
metadata: None,
|
||||
*frm_suggestion = Some(FrmSuggestion::FrmCancelTransaction);
|
||||
|
||||
let cancel_req = api_models::payments::PaymentsCancelRequest {
|
||||
payment_id: frm_data.payment_intent.payment_id.clone(),
|
||||
cancellation_reason: frm_data.fraud_check.frm_error.clone(),
|
||||
merchant_connector_details: None,
|
||||
};
|
||||
let refund = Box::pin(refunds::refund_create_core(
|
||||
let cancel_res = Box::pin(payments::payments_core::<
|
||||
Void,
|
||||
payment_types::PaymentsResponse,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
Oss,
|
||||
>(
|
||||
state.clone(),
|
||||
req_state.clone(),
|
||||
merchant_account.clone(),
|
||||
key_store.clone(),
|
||||
ref_req,
|
||||
payments::PaymentCancel,
|
||||
cancel_req,
|
||||
api::AuthFlow::Merchant,
|
||||
payments::CallConnectorAction::Trigger,
|
||||
None,
|
||||
HeaderPayload::default(),
|
||||
))
|
||||
.await?;
|
||||
if let services::ApplicationResponse::Json(new_refund) = refund {
|
||||
frm_data.refund = Some(new_refund);
|
||||
logger::debug!("payment_id : {:?} has been cancelled since it has been found fraudulent by configured frm connector",payment_data.payment_attempt.payment_id);
|
||||
if let services::ApplicationResponse::JsonWithHeaders((payments_response, _)) =
|
||||
cancel_res
|
||||
{
|
||||
payment_data.payment_intent.status = payments_response.status;
|
||||
}
|
||||
let _router_data = frm_core::call_frm_service::<F, frm_api::RecordReturn, _>(
|
||||
state,
|
||||
@ -227,6 +242,51 @@ impl<F: Send + Clone> Domain<F> for FraudCheckPost {
|
||||
)
|
||||
.await?;
|
||||
frm_data.fraud_check.last_step = FraudCheckLastStep::TransactionOrRecordRefund;
|
||||
} else if matches!(frm_data.fraud_check.frm_status, FraudCheckStatus::Fraud)
|
||||
&& matches!(frm_configs.frm_action, FrmAction::ManualReview)
|
||||
{
|
||||
*frm_suggestion = Some(FrmSuggestion::FrmManualReview);
|
||||
} else if matches!(frm_data.fraud_check.frm_status, FraudCheckStatus::Legit)
|
||||
&& matches!(
|
||||
frm_data.fraud_check.payment_capture_method,
|
||||
Some(CaptureMethod::Automatic)
|
||||
)
|
||||
{
|
||||
let capture_request = api_models::payments::PaymentsCaptureRequest {
|
||||
payment_id: frm_data.payment_intent.payment_id.clone(),
|
||||
merchant_id: None,
|
||||
amount_to_capture: None,
|
||||
refund_uncaptured_amount: None,
|
||||
statement_descriptor_suffix: None,
|
||||
statement_descriptor_prefix: None,
|
||||
merchant_connector_details: None,
|
||||
};
|
||||
let capture_response = Box::pin(payments::payments_core::<
|
||||
Capture,
|
||||
payment_types::PaymentsResponse,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
Oss,
|
||||
>(
|
||||
state.clone(),
|
||||
req_state.clone(),
|
||||
merchant_account.clone(),
|
||||
key_store.clone(),
|
||||
payments::PaymentCapture,
|
||||
capture_request,
|
||||
api::AuthFlow::Merchant,
|
||||
payments::CallConnectorAction::Trigger,
|
||||
None,
|
||||
HeaderPayload::default(),
|
||||
))
|
||||
.await?;
|
||||
logger::debug!("payment_id : {:?} has been captured since it has been found legit by configured frm connector",payment_data.payment_attempt.payment_id);
|
||||
if let services::ApplicationResponse::JsonWithHeaders((payments_response, _)) =
|
||||
capture_response
|
||||
{
|
||||
payment_data.payment_intent.status = payments_response.status;
|
||||
}
|
||||
};
|
||||
return Ok(Some(frm_data.to_owned()));
|
||||
}
|
||||
@ -302,6 +362,7 @@ impl<F: Clone + Send> UpdateTracker<FrmData, F> for FraudCheckPost {
|
||||
metadata: connector_metadata,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: frm_data.fraud_check.last_step,
|
||||
payment_capture_method: frm_data.fraud_check.payment_capture_method,
|
||||
};
|
||||
Some(fraud_check_update)
|
||||
},
|
||||
@ -346,6 +407,7 @@ impl<F: Clone + Send> UpdateTracker<FrmData, F> for FraudCheckPost {
|
||||
metadata: connector_metadata,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: frm_data.fraud_check.last_step,
|
||||
payment_capture_method: frm_data.fraud_check.payment_capture_method,
|
||||
};
|
||||
Some(fraud_check_update)
|
||||
}
|
||||
@ -396,6 +458,7 @@ impl<F: Clone + Send> UpdateTracker<FrmData, F> for FraudCheckPost {
|
||||
metadata: connector_metadata,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: frm_data.fraud_check.last_step,
|
||||
payment_capture_method: frm_data.fraud_check.payment_capture_method,
|
||||
};
|
||||
Some(fraud_check_update)
|
||||
}
|
||||
@ -412,15 +475,27 @@ impl<F: Clone + Send> UpdateTracker<FrmData, F> for FraudCheckPost {
|
||||
}
|
||||
};
|
||||
|
||||
if frm_suggestion == Some(FrmSuggestion::FrmAutoRefund) {
|
||||
if let Some(frm_suggestion) = frm_suggestion {
|
||||
let (payment_attempt_status, payment_intent_status) = match frm_suggestion {
|
||||
FrmSuggestion::FrmCancelTransaction => {
|
||||
(AttemptStatus::Failure, IntentStatus::Failed)
|
||||
}
|
||||
FrmSuggestion::FrmManualReview => (
|
||||
AttemptStatus::Unresolved,
|
||||
IntentStatus::RequiresMerchantAction,
|
||||
),
|
||||
FrmSuggestion::FrmAuthorizeTransaction => {
|
||||
(AttemptStatus::Authorized, IntentStatus::RequiresCapture)
|
||||
}
|
||||
};
|
||||
payment_data.payment_attempt = db
|
||||
.update_payment_attempt_with_attempt_id(
|
||||
payment_data.payment_attempt.clone(),
|
||||
PaymentAttemptUpdate::RejectUpdate {
|
||||
status: AttemptStatus::Failure,
|
||||
status: payment_attempt_status,
|
||||
error_code: Some(Some(frm_data.fraud_check.frm_status.to_string())),
|
||||
error_message: Some(Some(REFUND_INITIATED.to_string())),
|
||||
updated_by: frm_data.merchant_account.storage_scheme.to_string(), // merchant_decision: Some(MerchantDecision::AutoRefunded),
|
||||
error_message: Some(Some(CANCEL_INITIATED.to_string())),
|
||||
updated_by: frm_data.merchant_account.storage_scheme.to_string(),
|
||||
},
|
||||
frm_data.merchant_account.storage_scheme,
|
||||
)
|
||||
@ -431,8 +506,8 @@ impl<F: Clone + Send> UpdateTracker<FrmData, F> for FraudCheckPost {
|
||||
.update_payment_intent(
|
||||
payment_data.payment_intent.clone(),
|
||||
PaymentIntentUpdate::RejectUpdate {
|
||||
status: IntentStatus::Failed,
|
||||
merchant_decision: Some(MerchantDecision::AutoRefunded.to_string()),
|
||||
status: payment_intent_status,
|
||||
merchant_decision: Some(MerchantDecision::Rejected.to_string()),
|
||||
updated_by: frm_data.merchant_account.storage_scheme.to_string(),
|
||||
},
|
||||
frm_data.merchant_account.storage_scheme,
|
||||
|
||||
@ -18,6 +18,7 @@ use crate::{
|
||||
},
|
||||
db::StorageInterface,
|
||||
errors,
|
||||
routes::app::ReqState,
|
||||
types::{
|
||||
api::fraud_check as frm_api,
|
||||
domain,
|
||||
@ -104,6 +105,7 @@ impl GetTracker<PaymentToFrmData> for FraudCheckPre {
|
||||
metadata: None,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: FraudCheckLastStep::Processing,
|
||||
payment_capture_method: payment_data.payment_attempt.capture_method,
|
||||
})
|
||||
.await
|
||||
}
|
||||
@ -138,6 +140,7 @@ impl<F: Send + Clone> Domain<F> for FraudCheckPre {
|
||||
async fn post_payment_frm<'a>(
|
||||
&'a self,
|
||||
state: &'a AppState,
|
||||
_req_state: ReqState,
|
||||
payment_data: &mut payments::PaymentData<F>,
|
||||
frm_data: &mut FrmData,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
@ -248,6 +251,7 @@ impl<F: Clone + Send> UpdateTracker<FrmData, F> for FraudCheckPre {
|
||||
metadata: connector_metadata,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: frm_data.fraud_check.last_step,
|
||||
payment_capture_method: frm_data.fraud_check.payment_capture_method,
|
||||
};
|
||||
Some(fraud_check_update)
|
||||
}
|
||||
@ -300,6 +304,7 @@ impl<F: Clone + Send> UpdateTracker<FrmData, F> for FraudCheckPre {
|
||||
metadata: connector_metadata,
|
||||
modified_at: common_utils::date_time::now(),
|
||||
last_step: frm_data.fraud_check.last_step,
|
||||
payment_capture_method: None,
|
||||
};
|
||||
Some(fraud_check_update)
|
||||
}
|
||||
|
||||
@ -215,4 +215,4 @@ pub struct FrmFulfillmentSignifydApiResponse {
|
||||
pub shipment_ids: Vec<String>,
|
||||
}
|
||||
|
||||
pub const REFUND_INITIATED: &str = "Refund Initiated with the processor";
|
||||
pub const CANCEL_INITIATED: &str = "Cancel Initiated with the processor";
|
||||
|
||||
@ -226,7 +226,7 @@ where
|
||||
};
|
||||
#[cfg(feature = "frm")]
|
||||
logger::debug!(
|
||||
"frm_configs: {:?}\nshould_cancel_transaction: {:?}\nshould_continue_capture: {:?}",
|
||||
"frm_configs: {:?}\nshould_continue_transaction: {:?}\nshould_continue_capture: {:?}",
|
||||
frm_configs,
|
||||
should_continue_transaction,
|
||||
should_continue_capture,
|
||||
@ -243,7 +243,6 @@ where
|
||||
&key_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if should_continue_transaction {
|
||||
#[cfg(feature = "frm")]
|
||||
match (
|
||||
@ -252,8 +251,15 @@ where
|
||||
) {
|
||||
(false, Some(storage_enums::CaptureMethod::Automatic))
|
||||
| (false, Some(storage_enums::CaptureMethod::Scheduled)) => {
|
||||
if let Some(info) = &mut frm_info {
|
||||
if let Some(frm_data) = &mut info.frm_data {
|
||||
frm_data.fraud_check.payment_capture_method =
|
||||
payment_data.payment_attempt.capture_method;
|
||||
}
|
||||
}
|
||||
payment_data.payment_attempt.capture_method =
|
||||
Some(storage_enums::CaptureMethod::Manual);
|
||||
logger::debug!("payment_id : {:?} capture method has been changed to manual, since it has configured Post FRM flow",payment_data.payment_attempt.payment_id);
|
||||
}
|
||||
_ => (),
|
||||
};
|
||||
@ -274,7 +280,7 @@ where
|
||||
};
|
||||
let router_data = call_connector_service(
|
||||
state,
|
||||
req_state,
|
||||
req_state.clone(),
|
||||
&merchant_account,
|
||||
&key_store,
|
||||
connector.clone(),
|
||||
@ -374,7 +380,7 @@ where
|
||||
if config_bool && router_data.should_call_gsm() {
|
||||
router_data = retry::do_gsm_actions(
|
||||
state,
|
||||
req_state,
|
||||
req_state.clone(),
|
||||
&mut payment_data,
|
||||
connectors,
|
||||
connector_data.clone(),
|
||||
@ -450,6 +456,7 @@ where
|
||||
if let Some(fraud_info) = &mut frm_info {
|
||||
Box::pin(frm_core::post_payment_frm_core(
|
||||
state,
|
||||
req_state,
|
||||
&merchant_account,
|
||||
&mut payment_data,
|
||||
fraud_info,
|
||||
@ -461,6 +468,7 @@ where
|
||||
.attach_printable("Frm configs label not found")?,
|
||||
&customer,
|
||||
key_store.clone(),
|
||||
&mut should_continue_capture,
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use api_models::enums::FrmSuggestion;
|
||||
use api_models::enums::{AttemptStatus, FrmSuggestion, IntentStatus};
|
||||
use async_trait::async_trait;
|
||||
use error_stack::ResultExt;
|
||||
use router_derive::PaymentOperation;
|
||||
@ -207,7 +207,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
|
||||
storage_scheme: storage_enums::MerchantStorageScheme,
|
||||
_updated_customer: Option<storage::CustomerUpdate>,
|
||||
_merchant_key_store: &domain::MerchantKeyStore,
|
||||
_frm_suggestion: Option<FrmSuggestion>,
|
||||
frm_suggestion: Option<FrmSuggestion>,
|
||||
_header_payload: api::HeaderPayload,
|
||||
) -> RouterResult<(
|
||||
BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>,
|
||||
@ -216,7 +216,12 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
|
||||
where
|
||||
F: 'b + Send,
|
||||
{
|
||||
if matches!(frm_suggestion, Some(FrmSuggestion::FrmAuthorizeTransaction)) {
|
||||
payment_data.payment_intent.status = IntentStatus::RequiresCapture; // In Approve flow, payment which has payment_capture_method "manual" and attempt status as "Unresolved",
|
||||
payment_data.payment_attempt.status = AttemptStatus::Authorized; // We shouldn't call the connector instead we need to update the payment attempt and payment intent.
|
||||
}
|
||||
let intent_status_update = storage::PaymentIntentUpdate::ApproveUpdate {
|
||||
status: payment_data.payment_intent.status,
|
||||
merchant_decision: Some(api_models::enums::MerchantDecision::Approved.to_string()),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
};
|
||||
@ -229,6 +234,17 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
db.store
|
||||
.update_payment_attempt_with_attempt_id(
|
||||
payment_data.payment_attempt.clone(),
|
||||
storage::PaymentAttemptUpdate::StatusUpdate {
|
||||
status: payment_data.payment_attempt.status,
|
||||
updated_by: storage_scheme.to_string(),
|
||||
},
|
||||
storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
Ok((Box::new(self), payment_data))
|
||||
}
|
||||
|
||||
@ -923,6 +923,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
|
||||
let payment_method = payment_data.payment_attempt.payment_method;
|
||||
let browser_info = payment_data.payment_attempt.browser_info.clone();
|
||||
let frm_message = payment_data.frm_message.clone();
|
||||
let capture_method = payment_data.payment_attempt.capture_method;
|
||||
|
||||
let default_status_result = (
|
||||
storage_enums::IntentStatus::Processing,
|
||||
@ -945,7 +946,11 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
|
||||
storage_enums::AttemptStatus::Unresolved,
|
||||
(None, None),
|
||||
),
|
||||
FrmSuggestion::FrmAutoRefund => default_status_result.clone(),
|
||||
FrmSuggestion::FrmAuthorizeTransaction => (
|
||||
storage_enums::IntentStatus::RequiresCapture,
|
||||
storage_enums::AttemptStatus::Authorized,
|
||||
(None, None),
|
||||
),
|
||||
};
|
||||
|
||||
let status_handler_for_authentication_results =
|
||||
@ -1038,6 +1043,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
|
||||
let m_payment_method_id = payment_data.payment_attempt.payment_method_id.clone();
|
||||
let m_browser_info = browser_info.clone();
|
||||
let m_connector = connector.clone();
|
||||
let m_capture_method = capture_method;
|
||||
let m_payment_token = payment_token.clone();
|
||||
let m_additional_pm_data = additional_pm_data.clone();
|
||||
let m_business_sub_label = business_sub_label.clone();
|
||||
@ -1078,6 +1084,7 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
|
||||
status: attempt_status,
|
||||
payment_method,
|
||||
authentication_type,
|
||||
capture_method: m_capture_method,
|
||||
browser_info: m_browser_info,
|
||||
connector: m_connector,
|
||||
payment_token: m_payment_token,
|
||||
|
||||
@ -849,6 +849,7 @@ where
|
||||
enums::IntentStatus::Succeeded
|
||||
| enums::IntentStatus::Failed
|
||||
| enums::IntentStatus::PartiallyCaptured
|
||||
| enums::IntentStatus::RequiresMerchantAction
|
||||
) {
|
||||
let payments_response = crate::core::payments::transformers::payments_to_payments_response(
|
||||
payment_data,
|
||||
|
||||
@ -1445,6 +1445,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
currency,
|
||||
status,
|
||||
authentication_type,
|
||||
capture_method,
|
||||
payment_method,
|
||||
browser_info,
|
||||
connector,
|
||||
@ -1472,6 +1473,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
currency,
|
||||
status,
|
||||
authentication_type,
|
||||
capture_method,
|
||||
payment_method,
|
||||
browser_info,
|
||||
connector,
|
||||
@ -1744,6 +1746,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
currency,
|
||||
status,
|
||||
authentication_type,
|
||||
capture_method,
|
||||
payment_method,
|
||||
browser_info,
|
||||
connector,
|
||||
@ -1771,6 +1774,7 @@ impl DataModelExt for PaymentAttemptUpdate {
|
||||
currency,
|
||||
status,
|
||||
authentication_type,
|
||||
capture_method,
|
||||
payment_method,
|
||||
browser_info,
|
||||
connector,
|
||||
|
||||
@ -1113,9 +1113,11 @@ impl DataModelExt for PaymentIntentUpdate {
|
||||
updated_by,
|
||||
},
|
||||
Self::ApproveUpdate {
|
||||
status,
|
||||
merchant_decision,
|
||||
updated_by,
|
||||
} => DieselPaymentIntentUpdate::ApproveUpdate {
|
||||
status,
|
||||
merchant_decision,
|
||||
updated_by,
|
||||
},
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE fraud_check
|
||||
DROP COLUMN IF EXISTS payment_capture_method;
|
||||
@ -0,0 +1,2 @@
|
||||
ALTER TABLE fraud_check
|
||||
ADD COLUMN IF NOT EXISTS payment_capture_method "CaptureMethod" NULL;
|
||||
Reference in New Issue
Block a user