diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 0681edbb15..44e1775469 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -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( diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index 0c16563204..7e406ca24d 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -303,6 +303,7 @@ pub enum PaymentAttemptUpdate { currency: storage_enums::Currency, status: storage_enums::AttemptStatus, authentication_type: Option, + capture_method: Option, payment_method: Option, browser_info: Option, connector: Option, diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 0ab39bcbad..a5e631b5ac 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -182,6 +182,7 @@ pub enum PaymentIntentUpdate { updated_by: String, }, ApproveUpdate { + status: storage_enums::IntentStatus, merchant_decision: Option, updated_by: String, }, @@ -382,9 +383,11 @@ impl From for PaymentIntentUpdateInternal { ..Default::default() }, PaymentIntentUpdate::ApproveUpdate { + status, merchant_decision, updated_by, } => Self { + status: Some(status), merchant_decision, updated_by, ..Default::default() diff --git a/crates/diesel_models/src/fraud_check.rs b/crates/diesel_models/src/fraud_check.rs index 8c20fe466a..5513afcfbd 100644 --- a/crates/diesel_models/src/fraud_check.rs +++ b/crates/diesel_models/src/fraud_check.rs @@ -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, pub modified_at: PrimitiveDateTime, pub last_step: FraudCheckLastStep, + pub payment_capture_method: Option, // 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, pub modified_at: PrimitiveDateTime, pub last_step: FraudCheckLastStep, + pub payment_capture_method: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -59,6 +62,7 @@ pub enum FraudCheckUpdate { metadata: Option, modified_at: PrimitiveDateTime, last_step: FraudCheckLastStep, + payment_capture_method: Option, }, ErrorUpdate { status: FraudCheckStatus, @@ -76,6 +80,7 @@ pub struct FraudCheckUpdateInternal { frm_error: Option>, metadata: Option, last_step: FraudCheckLastStep, + payment_capture_method: Option, } impl From for FraudCheckUpdateInternal { @@ -89,6 +94,7 @@ impl From 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 for FraudCheckUpdateInternal { frm_score, metadata, last_step, + payment_capture_method, ..Default::default() }, FraudCheckUpdate::ErrorUpdate { diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 603e0f4ebf..65481d93be 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -209,6 +209,7 @@ pub enum PaymentAttemptUpdate { currency: storage_enums::Currency, status: storage_enums::AttemptStatus, authentication_type: Option, + capture_method: Option, payment_method: Option, browser_info: Option, connector: Option, @@ -559,6 +560,7 @@ impl From for PaymentAttemptUpdateInternal { amount, currency, authentication_type, + capture_method, status, payment_method, browser_info, @@ -610,6 +612,7 @@ impl From for PaymentAttemptUpdateInternal { payment_method_billing_address_id, fingerprint_id, payment_method_id, + capture_method, ..Default::default() }, PaymentAttemptUpdate::VoidUpdate { diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index d1711c8ba0..ee6e3960b7 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -180,6 +180,7 @@ pub enum PaymentIntentUpdate { updated_by: String, }, ApproveUpdate { + status: storage_enums::IntentStatus, merchant_decision: Option, updated_by: String, }, @@ -456,9 +457,11 @@ impl From for PaymentIntentUpdateInternal { ..Default::default() }, PaymentIntentUpdate::ApproveUpdate { + status, merchant_decision, updated_by, } => Self { + status: Some(status), merchant_decision, updated_by, ..Default::default() diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 44429cc4ca..e983b09ff2 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -450,6 +450,7 @@ diesel::table! { modified_at -> Timestamp, #[max_length = 64] last_step -> Varchar, + payment_capture_method -> Nullable, } } diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index 0e3f67c051..b62d3afd4a 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -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, @@ -433,85 +446,108 @@ 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> where F: Send + Clone, { - if let Some(frm_data) = &mut frm_info.frm_data { - if matches!( - frm_configs.frm_preferred_flow_type, - api_enums::FrmPreferredFlowTypes::Pre - ) { - let fraud_check_operation = &mut frm_info.fraud_check_operation; + 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 + ) { + let fraud_check_operation = &mut frm_info.fraud_check_operation; - let frm_router_data = fraud_check_operation - .to_domain()? - .pre_payment_frm( + let frm_router_data = fraud_check_operation + .to_domain()? + .pre_payment_frm( + state, + payment_data, + frm_data, + merchant_account, + customer, + key_store.clone(), + ) + .await?; + let _router_data = call_frm_service::( state, payment_data, frm_data, merchant_account, + &key_store, customer, - key_store, ) .await?; - let frm_data_updated = fraud_check_operation - .to_update_tracker()? - .update_tracker( - &*state.store, - frm_data.clone(), - payment_data, - None, - frm_router_data, - ) - .await?; - 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) { + let frm_data_updated = fraud_check_operation + .to_update_tracker()? + .update_tracker( + &*state.store, + frm_data.clone(), + payment_data, + None, + frm_router_data, + ) + .await?; + 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) { *should_continue_transaction = false; - frm_info.suggested_action = Some(FrmSuggestion::FrmCancelTransaction); - } else if matches!(frm_configs.frm_action, api_enums::FrmAction::ManualReview) { - *should_continue_capture = false; - frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview); + 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; + frm_info.suggested_action = Some(FrmSuggestion::FrmManualReview); + } } + logger::debug!( + "frm_updated_data: {:?} {:?}", + frm_info.fraud_check_operation, + frm_info.suggested_action + ); + 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 { + Some(frm_data.to_owned()) } - logger::debug!( - "frm_updated_data: {:?} {:?}", - frm_info.fraud_check_operation, - frm_info.suggested_action - ); - Ok(Some(frm_data_updated)) } else { - Ok(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, frm_info: &mut FrmInfo, frm_configs: FrmConfigsObject, customer: &Option, key_store: domain::MerchantKeyStore, + should_continue_capture: &mut bool, ) -> RouterResult> 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,51 +630,76 @@ 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)) = - frm_routing_algorithm.zip(frm_connector_label) - { - if let Some(frm_configs) = frm_configs.clone() { - let mut updated_frm_info = make_frm_data_and_fraud_check_operation( - db, + 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)) = + frm_routing_algorithm.zip(frm_connector_label) + { + if let Some(frm_configs) = frm_configs.clone() { + let mut updated_frm_info = make_frm_data_and_fraud_check_operation( + db, + state, + merchant_account, + payment_data.to_owned(), + frm_routing_algorithm_val, + profile_id, + frm_configs.clone(), + customer, + ) + .await?; + + if is_frm_enabled { + pre_payment_frm_core( state, merchant_account, - payment_data.to_owned(), - frm_routing_algorithm_val, - profile_id, - frm_configs.clone(), + payment_data, + &mut updated_frm_info, + frm_configs, customer, + should_continue_transaction, + should_continue_capture, + key_store, + operation, ) .await?; - - if is_frm_enabled { - pre_payment_frm_core( - state, - merchant_account, - payment_data, - &mut updated_frm_info, - frm_configs, - customer, - should_continue_transaction, - should_continue_capture, - key_store, - ) - .await?; - } - *frm_info = Some(updated_frm_info); } + *frm_info = Some(updated_frm_info); } - logger::debug!("frm_configs: {:?} {:?}", frm_configs, is_frm_enabled); - return Ok(frm_configs); } - Ok(None) + 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); + Ok(frm_configs) } pub fn is_operation_allowed(operation: &Op) -> bool { - !["PaymentSession", "PaymentApprove", "PaymentReject"] - .contains(&format!("{operation:?}").as_str()) + ![ + "PaymentSession", + "PaymentApprove", + "PaymentReject", + "PaymentCapture", + "PaymentsCancel", + ] + .contains(&format!("{operation:?}").as_str()) } impl From for PaymentDetails { @@ -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) diff --git a/crates/router/src/core/fraud_check/operation.rs b/crates/router/src/core/fraud_check/operation.rs index e7677dad6f..a9b95cdf3b 100644 --- a/crates/router/src/core/fraud_check/operation.rs +++ b/crates/router/src/core/fraud_check/operation.rs @@ -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: Send { } #[async_trait] +#[allow(clippy::too_many_arguments)] pub trait Domain: Send + Sync { async fn post_payment_frm<'a>( &'a self, state: &'a AppState, + req_state: ReqState, payment_data: &mut payments::PaymentData, frm_data: &mut FrmData, merchant_account: &domain::MerchantAccount, @@ -78,6 +80,7 @@ pub trait Domain: 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: Send + Sync { _key_store: domain::MerchantKeyStore, _payment_data: &mut payments::PaymentData, _customer: &Option, + _should_continue_capture: &mut bool, ) -> RouterResult> where F: Send + Clone, diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_post.rs b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs index a6cd097612..d59c8c5ff4 100644 --- a/crates/router/src/core/fraud_check/operation/fraud_check_post.rs +++ b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs @@ -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 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 Domain for FraudCheckPost { async fn post_payment_frm<'a>( &'a self, state: &'a AppState, + _req_state: ReqState, payment_data: &mut payments::PaymentData, frm_data: &mut FrmData, merchant_account: &domain::MerchantAccount, @@ -177,6 +182,7 @@ impl Domain 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 Domain for FraudCheckPost { key_store: domain::MerchantKeyStore, payment_data: &mut payments::PaymentData, customer: &Option, + _should_continue_capture: &mut bool, ) -> RouterResult> { 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::( state, @@ -227,6 +242,51 @@ impl Domain 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 UpdateTracker 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 UpdateTracker 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 UpdateTracker 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 UpdateTracker 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 UpdateTracker 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, diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs index ed582574bf..eac8dc84da 100644 --- a/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs +++ b/crates/router/src/core/fraud_check/operation/fraud_check_pre.rs @@ -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 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 Domain for FraudCheckPre { async fn post_payment_frm<'a>( &'a self, state: &'a AppState, + _req_state: ReqState, payment_data: &mut payments::PaymentData, frm_data: &mut FrmData, merchant_account: &domain::MerchantAccount, @@ -248,6 +251,7 @@ impl UpdateTracker 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 UpdateTracker 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) } diff --git a/crates/router/src/core/fraud_check/types.rs b/crates/router/src/core/fraud_check/types.rs index e60458646f..46a341142f 100644 --- a/crates/router/src/core/fraud_check/types.rs +++ b/crates/router/src/core/fraud_check/types.rs @@ -215,4 +215,4 @@ pub struct FrmFulfillmentSignifydApiResponse { pub shipment_ids: Vec, } -pub const REFUND_INITIATED: &str = "Refund Initiated with the processor"; +pub const CANCEL_INITIATED: &str = "Cancel Initiated with the processor"; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 67f095450c..2e3157ae6e 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -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?; } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index d9744cff77..87943b4cf2 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -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 storage_scheme: storage_enums::MerchantStorageScheme, _updated_customer: Option, _merchant_key_store: &domain::MerchantKeyStore, - _frm_suggestion: Option, + frm_suggestion: Option, _header_payload: api::HeaderPayload, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>, @@ -216,7 +216,12 @@ impl 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 ) .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)) } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index e73bf33f64..72e0903f7c 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -923,6 +923,7 @@ impl 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 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 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 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, diff --git a/crates/router/src/utils.rs b/crates/router/src/utils.rs index 85ef0cfe6b..2d4b34b30d 100644 --- a/crates/router/src/utils.rs +++ b/crates/router/src/utils.rs @@ -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, diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 1fd25f3ded..683b0469a2 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -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, diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 6b4cf8b82e..4d984a02cc 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -1113,9 +1113,11 @@ impl DataModelExt for PaymentIntentUpdate { updated_by, }, Self::ApproveUpdate { + status, merchant_decision, updated_by, } => DieselPaymentIntentUpdate::ApproveUpdate { + status, merchant_decision, updated_by, }, diff --git a/migrations/2024-04-24-104042_add_capture_method_in_fraud_check_table/down.sql b/migrations/2024-04-24-104042_add_capture_method_in_fraud_check_table/down.sql new file mode 100644 index 0000000000..596fb1022f --- /dev/null +++ b/migrations/2024-04-24-104042_add_capture_method_in_fraud_check_table/down.sql @@ -0,0 +1,2 @@ +ALTER TABLE fraud_check +DROP COLUMN IF EXISTS payment_capture_method; \ No newline at end of file diff --git a/migrations/2024-04-24-104042_add_capture_method_in_fraud_check_table/up.sql b/migrations/2024-04-24-104042_add_capture_method_in_fraud_check_table/up.sql new file mode 100644 index 0000000000..08e89a49c1 --- /dev/null +++ b/migrations/2024-04-24-104042_add_capture_method_in_fraud_check_table/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE fraud_check +ADD COLUMN IF NOT EXISTS payment_capture_method "CaptureMethod" NULL; \ No newline at end of file