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:
chikke srujan
2024-04-29 12:27:12 +05:30
committed by GitHub
parent c3a1db16f3
commit 01ec7c64a4
20 changed files with 337 additions and 125 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -450,6 +450,7 @@ diesel::table! {
modified_at -> Timestamp,
#[max_length = 64]
last_step -> Varchar,
payment_capture_method -> Nullable<CaptureMethod>,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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?;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1113,9 +1113,11 @@ impl DataModelExt for PaymentIntentUpdate {
updated_by,
},
Self::ApproveUpdate {
status,
merchant_decision,
updated_by,
} => DieselPaymentIntentUpdate::ApproveUpdate {
status,
merchant_decision,
updated_by,
},

View File

@ -0,0 +1,2 @@
ALTER TABLE fraud_check
DROP COLUMN IF EXISTS payment_capture_method;

View File

@ -0,0 +1,2 @@
ALTER TABLE fraud_check
ADD COLUMN IF NOT EXISTS payment_capture_method "CaptureMethod" NULL;