From aed2ab2f58c9d72426b908df8028178ece36f6cd Mon Sep 17 00:00:00 2001 From: Kashif Date: Wed, 20 Dec 2023 13:15:00 +0530 Subject: [PATCH 1/3] fix(frm): update FRM's manual review flow --- crates/router/src/core/fraud_check.rs | 10 +- crates/router/src/core/payments.rs | 35 ++- .../payments/operations/payment_approve.rs | 259 +++-------------- .../payments/operations/payment_response.rs | 267 ++++++++++-------- crates/router/src/core/payments/retry.rs | 5 + crates/router/src/routes/payments.rs | 8 +- 6 files changed, 228 insertions(+), 356 deletions(-) diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index 850c6c8322f8..4948d7a263a1 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -432,6 +432,7 @@ pub async fn pre_payment_frm_core<'a, F>( frm_configs: FrmConfigsObject, customer: &Option, should_continue_transaction: &mut bool, + should_continue_capture: &mut bool, key_store: domain::MerchantKeyStore, ) -> RouterResult> where @@ -467,13 +468,12 @@ where .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) - //DontTakeAction - { - *should_continue_transaction = false; + if matches!(frm_fraud_check.frm_status, FraudCheckStatus::Fraud) { if matches!(frm_configs.frm_action, api_enums::FrmAction::CancelTxn) { + *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); } } @@ -583,6 +583,7 @@ pub async fn call_frm_before_connector_call<'a, F, Req, Ctx>( frm_info: &mut Option>, customer: &Option, should_continue_transaction: &mut bool, + should_continue_capture: &mut bool, key_store: domain::MerchantKeyStore, ) -> RouterResult> where @@ -616,6 +617,7 @@ where frm_configs, customer, should_continue_transaction, + should_continue_capture, key_store, ) .await?; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 3c7ac0fd78d0..cdcff78ebc59 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -180,6 +180,8 @@ where #[allow(unused_variables, unused_mut)] let mut should_continue_transaction: bool = true; #[cfg(feature = "frm")] + let mut should_continue_capture: bool = true; + #[cfg(feature = "frm")] let frm_configs = if state.conf.frm.enabled { frm_core::call_frm_before_connector_call( db, @@ -190,6 +192,7 @@ where &mut frm_info, &customer, &mut should_continue_transaction, + &mut should_continue_capture, key_store.clone(), ) .await? @@ -198,12 +201,25 @@ where }; #[cfg(feature = "frm")] logger::debug!( - "should_cancel_transaction: {:?} {:?} ", + "frm_configs: {:?}\nshould_cancel_transaction: {:?}\nshould_continue_capture: {:?}", frm_configs, - should_continue_transaction + should_continue_transaction, + should_continue_capture, ); if should_continue_transaction { + #[cfg(feature = "frm")] + match ( + should_continue_capture, + payment_data.payment_attempt.capture_method, + ) { + (false, Some(storage_enums::CaptureMethod::Automatic)) + | (false, Some(storage_enums::CaptureMethod::Scheduled)) => { + payment_data.payment_attempt.capture_method = + Some(storage_enums::CaptureMethod::Manual); + } + _ => (), + }; operation .to_domain()? .populate_payment_data(state, &mut payment_data, &merchant_account) @@ -236,6 +252,10 @@ where &validate_result, schedule_time, header_payload, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) .await?; let operation = Box::new(PaymentResponse); @@ -287,6 +307,10 @@ where &validate_result, schedule_time, header_payload, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) .await?; @@ -314,6 +338,10 @@ where &customer, &validate_result, schedule_time, + #[cfg(feature = "frm")] + frm_info.as_ref().and_then(|fi| fi.suggested_action), + #[cfg(not(feature = "frm"))] + None, ) .await?; }; @@ -973,6 +1001,7 @@ pub async fn call_connector_service( validate_result: &operations::ValidateResult<'_>, schedule_time: Option, header_payload: HeaderPayload, + frm_suggestion: Option, ) -> RouterResult> where F: Send + Clone + Sync, @@ -1148,7 +1177,7 @@ where merchant_account.storage_scheme, updated_customer, key_store, - None, + frm_suggestion, header_payload, ) .await?; diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index 7d0ec0718c25..247da9dba00c 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -2,54 +2,51 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use data_models::mandates::MandateData; -use error_stack::{report, IntoReport, ResultExt}; +use error_stack::{IntoReport, ResultExt}; use router_derive::PaymentOperation; use router_env::{instrument, tracing}; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ core::{ - errors::{self, CustomResult, RouterResult, StorageErrorExt}, + errors::{self, RouterResult, StorageErrorExt}, payment_methods::PaymentMethodRetrieve, - payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + payments::{helpers, operations, PaymentAddress, PaymentData}, utils as core_utils, }, - db::StorageInterface, routes::AppState, services, types::{ - self, api::{self, PaymentIdTypeExt}, domain, storage::{self, enums as storage_enums}, }, - utils::{self, OptionExt}, + utils::OptionExt, }; #[derive(Debug, Clone, Copy, PaymentOperation)] -#[operation(operations = "all", flow = "authorize")] +#[operation(operations = "all", flow = "capture")] pub struct PaymentApprove; #[async_trait] impl - GetTracker, api::PaymentsRequest, Ctx> for PaymentApprove + GetTracker, api::PaymentsCaptureRequest, Ctx> for PaymentApprove { #[instrument(skip_all)] async fn get_trackers<'a>( &'a self, state: &'a AppState, payment_id: &api::PaymentIdType, - request: &api::PaymentsRequest, - mandate_type: Option, + _request: &api::PaymentsCaptureRequest, + _mandate_type: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; - let (mut payment_intent, mut payment_attempt, currency, amount); + let (mut payment_intent, payment_attempt, currency, amount); let payment_id = payment_id .get_payment_intent_id() @@ -59,9 +56,6 @@ impl .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - payment_intent.setup_future_usage = request - .setup_future_usage - .or(payment_intent.setup_future_usage); helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, @@ -69,7 +63,7 @@ impl storage_enums::IntentStatus::Failed, storage_enums::IntentStatus::Succeeded, ], - "confirm", + "approve", )?; let profile_id = payment_intent @@ -87,31 +81,6 @@ impl id: profile_id.to_string(), })?; - let ( - token, - payment_method, - payment_method_type, - setup_mandate, - recurring_mandate_payment_data, - mandate_connector, - ) = helpers::get_token_pm_type_mandate_details( - state, - request, - mandate_type.clone(), - merchant_account, - key_store, - ) - .await?; - - let browser_info = request - .browser_info - .clone() - .map(|x| utils::Encode::::encode_to_value(&x)) - .transpose() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "browser_info", - })?; - let attempt_id = payment_intent.active_attempt.get_id().clone(); payment_attempt = db .find_payment_attempt_by_payment_id_merchant_id_attempt_id( @@ -123,35 +92,12 @@ impl .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - let token = token.or_else(|| payment_attempt.payment_token.clone()); - - helpers::validate_pm_or_token_given( - &request.payment_method, - &request.payment_method_data, - &request.payment_method_type, - &mandate_type, - &token, - )?; - - payment_attempt.payment_method = payment_method.or(payment_attempt.payment_method); - payment_attempt.browser_info = browser_info; - payment_attempt.payment_method_type = - payment_method_type.or(payment_attempt.payment_method_type); - payment_attempt.payment_experience = request.payment_experience; currency = payment_attempt.currency.get_required_value("currency")?; amount = payment_attempt.amount.into(); - helpers::validate_customer_id_mandatory_cases( - request.setup_future_usage.is_some(), - &payment_intent - .customer_id - .clone() - .or_else(|| request.customer_id.clone()), - )?; - let shipping_address = helpers::create_or_find_address_for_payment_by_request( db, - request.shipping.as_ref(), + None, payment_intent.shipping_address_id.as_deref(), merchant_id, payment_intent.customer_id.as_ref(), @@ -162,7 +108,7 @@ impl .await?; let billing_address = helpers::create_or_find_address_for_payment_by_request( db, - request.billing.as_ref(), + None, payment_intent.billing_address_id.as_deref(), merchant_id, payment_intent.customer_id.as_ref(), @@ -172,47 +118,8 @@ impl ) .await?; - let redirect_response = request - .feature_metadata - .as_ref() - .and_then(|fm| fm.redirect_response.clone()); - payment_intent.shipping_address_id = shipping_address.clone().map(|i| i.address_id); payment_intent.billing_address_id = billing_address.clone().map(|i| i.address_id); - payment_intent.return_url = request - .return_url - .as_ref() - .map(|a| a.to_string()) - .or(payment_intent.return_url); - - payment_intent.allowed_payment_method_types = request - .get_allowed_payment_method_types_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting allowed_payment_types to Value")? - .or(payment_intent.allowed_payment_method_types); - - payment_intent.connector_metadata = request - .get_connector_metadata_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting connector_metadata to Value")? - .or(payment_intent.connector_metadata); - - payment_intent.feature_metadata = request - .get_feature_metadata_as_value() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Error converting feature_metadata to Value")? - .or(payment_intent.feature_metadata); - - payment_intent.metadata = request.metadata.clone().or(payment_intent.metadata); - - // The operation merges mandate data from both request and payment_attempt - let setup_mandate = setup_mandate.map(|mandate_data| MandateData { - customer_acceptance: mandate_data.customer_acceptance, - mandate_type: payment_attempt - .mandate_details - .clone() - .or(mandate_data.mandate_type), - }); let frm_response = db .find_fraud_check_by_payment_id(payment_intent.payment_id.clone(), merchant_account.merchant_id.clone()) @@ -228,49 +135,41 @@ impl payment_attempt, currency, amount, - email: request.email.clone(), + email: None, mandate_id: None, - mandate_connector, - setup_mandate, - token, + mandate_connector: None, + setup_mandate: None, + token: None, address: PaymentAddress { shipping: shipping_address.as_ref().map(|a| a.into()), billing: billing_address.as_ref().map(|a| a.into()), }, - confirm: request.confirm, - payment_method_data: request.payment_method_data.clone(), + confirm: None, + payment_method_data: None, force_sync: None, refunds: vec![], disputes: vec![], attempts: None, sessions_token: vec![], - card_cvc: request.card_cvc.clone(), + card_cvc: None, creds_identifier: None, pm_token: None, connector_customer_id: None, - recurring_mandate_payment_data, + recurring_mandate_payment_data: None, ephemeral_key: None, multiple_capture_data: None, - redirect_response, + redirect_response: None, surcharge_details: None, frm_message: frm_response.ok(), payment_link_data: None, incremental_authorization_details: None, authorizations: vec![], - frm_metadata: request.frm_metadata.clone(), + frm_metadata: None, }; - let customer_details = Some(CustomerDetails { - customer_id: request.customer_id.clone(), - name: request.name.clone(), - email: request.email.clone(), - phone: request.phone.clone(), - phone_country_code: request.phone_country_code.clone(), - }); - let get_trackers_response = operations::GetTrackerResponse { operation: Box::new(self), - customer_details, + customer_details: None, payment_data, business_profile, }; @@ -279,84 +178,9 @@ impl } } -#[async_trait] -impl Domain - for PaymentApprove -{ - #[instrument(skip_all)] - async fn get_or_create_customer_details<'a>( - &'a self, - db: &dyn StorageInterface, - payment_data: &mut PaymentData, - request: Option, - key_store: &domain::MerchantKeyStore, - ) -> CustomResult< - ( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - Option, - ), - errors::StorageError, - > { - helpers::create_customer_if_not_exist( - Box::new(self), - db, - payment_data, - request, - &key_store.merchant_id, - key_store, - ) - .await - } - - #[instrument(skip_all)] - async fn make_pm_data<'a>( - &'a self, - state: &'a AppState, - payment_data: &mut PaymentData, - _storage_scheme: storage_enums::MerchantStorageScheme, - merchant_key_store: &domain::MerchantKeyStore, - ) -> RouterResult<( - BoxedOperation<'a, F, api::PaymentsRequest, Ctx>, - Option, - )> { - let (op, payment_method_data) = - helpers::make_pm_data(Box::new(self), state, payment_data, merchant_key_store).await?; - - utils::when(payment_method_data.is_none(), || { - Err(errors::ApiErrorResponse::PaymentMethodNotFound) - })?; - - Ok((op, payment_method_data)) - } - - #[instrument(skip_all)] - async fn add_task_to_process_tracker<'a>( - &'a self, - _state: &'a AppState, - _payment_attempt: &storage::PaymentAttempt, - _requeue: bool, - _schedule_time: Option, - ) -> CustomResult<(), errors::ApiErrorResponse> { - Ok(()) - } - - async fn get_connector<'a>( - &'a self, - _merchant_account: &domain::MerchantAccount, - state: &AppState, - request: &api::PaymentsRequest, - _payment_intent: &storage::PaymentIntent, - _key_store: &domain::MerchantKeyStore, - ) -> CustomResult { - // Use a new connector in the confirm call or use the same one which was passed when - // creating the payment or if none is passed then use the routing algorithm - helpers::get_connector_default(state, request.routing.clone()).await - } -} - #[async_trait] impl - UpdateTracker, api::PaymentsRequest, Ctx> for PaymentApprove + UpdateTracker, api::PaymentsCaptureRequest, Ctx> for PaymentApprove { #[instrument(skip_all)] async fn update_trackers<'b>( @@ -370,7 +194,7 @@ impl _frm_suggestion: Option, _header_payload: api::HeaderPayload, ) -> RouterResult<( - BoxedOperation<'b, F, api::PaymentsRequest, Ctx>, + BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>, PaymentData, )> where @@ -394,16 +218,16 @@ impl } } -impl ValidateRequest - for PaymentApprove +impl + ValidateRequest for PaymentApprove { #[instrument(skip_all)] fn validate_request<'a, 'b>( &'b self, - request: &api::PaymentsRequest, + request: &api::PaymentsCaptureRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult<( - BoxedOperation<'b, F, api::PaymentsRequest, Ctx>, + BoxedOperation<'b, F, api::PaymentsCaptureRequest, Ctx>, operations::ValidateResult<'a>, )> { let request_merchant_id = request.merchant_id.as_deref(); @@ -413,28 +237,17 @@ impl ValidateRequest( }; (capture_update, attempt_update) } - Ok(payments_response) => match payments_response { - types::PaymentsResponseData::PreProcessingResponse { - pre_processing_id, - connector_metadata, - connector_response_reference_id, - .. - } => { - let connector_transaction_id = match pre_processing_id.to_owned() { - types::PreprocessingResponseId::PreProcessingId(_) => None, - types::PreprocessingResponseId::ConnectorTransactionId(connector_txn_id) => { - Some(connector_txn_id) - } - }; - let preprocessing_step_id = match pre_processing_id { - types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => { - Some(pre_processing_id) + Ok(payments_response) => { + let attempt_status = payment_data.payment_attempt.status.to_owned(); + let connector_status = router_data.status.to_owned(); + let updated_attempt_status = match ( + connector_status, + attempt_status, + payment_data.frm_message.to_owned(), + ) { + ( + enums::AttemptStatus::Authorized, + enums::AttemptStatus::Unresolved, + Some(frm_message), + ) => match frm_message.frm_status { + enums::FraudCheckStatus::Fraud | enums::FraudCheckStatus::ManualReview => { + attempt_status } - types::PreprocessingResponseId::ConnectorTransactionId(_) => None, - }; - let payment_attempt_update = storage::PaymentAttemptUpdate::PreprocessingUpdate { - status: router_data.get_attempt_status_for_db_update(&payment_data), - payment_method_id: Some(router_data.payment_method_id), + _ => router_data.get_attempt_status_for_db_update(&payment_data), + }, + _ => router_data.get_attempt_status_for_db_update(&payment_data), + }; + match payments_response { + types::PaymentsResponseData::PreProcessingResponse { + pre_processing_id, connector_metadata, - preprocessing_step_id, - connector_transaction_id, connector_response_reference_id, - updated_by: storage_scheme.to_string(), - }; - - (None, Some(payment_attempt_update)) - } - types::PaymentsResponseData::TransactionResponse { - resource_id, - redirection_data, - connector_metadata, - connector_response_reference_id, - incremental_authorization_allowed, - .. - } => { - payment_data - .payment_intent - .incremental_authorization_allowed = - core_utils::get_incremental_authorization_allowed_value( - incremental_authorization_allowed, - payment_data - .payment_intent - .request_incremental_authorization, - ); - let connector_transaction_id = match resource_id { - types::ResponseId::NoResponseId => None, - types::ResponseId::ConnectorTransactionId(id) - | types::ResponseId::EncodedData(id) => Some(id), - }; - - let encoded_data = payment_data.payment_attempt.encoded_data.clone(); - - let authentication_data = redirection_data - .map(|data| utils::Encode::::encode_to_value(&data)) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Could not parse the connector response")?; - - // incase of success, update error code and error message - let error_status = if router_data.status == enums::AttemptStatus::Charged { - Some(None) - } else { - None - }; + .. + } => { + let connector_transaction_id = match pre_processing_id.to_owned() { + types::PreprocessingResponseId::PreProcessingId(_) => None, + types::PreprocessingResponseId::ConnectorTransactionId( + connector_txn_id, + ) => Some(connector_txn_id), + }; + let preprocessing_step_id = match pre_processing_id { + types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => { + Some(pre_processing_id) + } + types::PreprocessingResponseId::ConnectorTransactionId(_) => None, + }; + let payment_attempt_update = + storage::PaymentAttemptUpdate::PreprocessingUpdate { + status: updated_attempt_status, + payment_method_id: Some(router_data.payment_method_id), + connector_metadata, + preprocessing_step_id, + connector_transaction_id, + connector_response_reference_id, + updated_by: storage_scheme.to_string(), + }; - if router_data.status == enums::AttemptStatus::Charged { - metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); + (None, Some(payment_attempt_update)) } + types::PaymentsResponseData::TransactionResponse { + resource_id, + redirection_data, + connector_metadata, + connector_response_reference_id, + incremental_authorization_allowed, + .. + } => { + payment_data + .payment_intent + .incremental_authorization_allowed = + core_utils::get_incremental_authorization_allowed_value( + incremental_authorization_allowed, + payment_data + .payment_intent + .request_incremental_authorization, + ); + let connector_transaction_id = match resource_id { + types::ResponseId::NoResponseId => None, + types::ResponseId::ConnectorTransactionId(id) + | types::ResponseId::EncodedData(id) => Some(id), + }; + + let encoded_data = payment_data.payment_attempt.encoded_data.clone(); + + let authentication_data = redirection_data + .map(|data| utils::Encode::::encode_to_value(&data)) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Could not parse the connector response")?; + + // incase of success, update error code and error message + let error_status = if router_data.status == enums::AttemptStatus::Charged { + Some(None) + } else { + None + }; - utils::add_apple_pay_payment_status_metrics( - router_data.status, - router_data.apple_pay_flow.clone(), - payment_data.payment_attempt.connector.clone(), - payment_data.payment_attempt.merchant_id.clone(), - ); + if router_data.status == enums::AttemptStatus::Charged { + metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]); + } + + utils::add_apple_pay_payment_status_metrics( + router_data.status, + router_data.apple_pay_flow.clone(), + payment_data.payment_attempt.connector.clone(), + payment_data.payment_attempt.merchant_id.clone(), + ); - let (capture_updates, payment_attempt_update) = - match payment_data.multiple_capture_data { + let (capture_updates, payment_attempt_update) = match payment_data + .multiple_capture_data + { Some(multiple_capture_data) => { let capture_update = storage::CaptureUpdate::ResponseUpdate { status: enums::CaptureStatus::foreign_try_from(router_data.status)?, @@ -612,7 +634,7 @@ async fn payment_response_update_tracker( None => ( None, Some(storage::PaymentAttemptUpdate::ResponseUpdate { - status: router_data.get_attempt_status_for_db_update(&payment_data), + status: updated_attempt_status, connector: None, connector_transaction_id: connector_transaction_id.clone(), authentication_type: None, @@ -645,52 +667,55 @@ async fn payment_response_update_tracker( ), }; - (capture_updates, payment_attempt_update) - } - types::PaymentsResponseData::TransactionUnresolvedResponse { - resource_id, - reason, - connector_response_reference_id, - } => { - let connector_transaction_id = match resource_id { - types::ResponseId::NoResponseId => None, - types::ResponseId::ConnectorTransactionId(id) - | types::ResponseId::EncodedData(id) => Some(id), - }; - ( - None, - Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { - status: router_data.get_attempt_status_for_db_update(&payment_data), - connector: None, - connector_transaction_id, - payment_method_id: Some(router_data.payment_method_id), - error_code: Some(reason.clone().map(|cd| cd.code)), - error_message: Some(reason.clone().map(|cd| cd.message)), - error_reason: Some(reason.map(|cd| cd.message)), - connector_response_reference_id, - updated_by: storage_scheme.to_string(), - }), - ) - } - types::PaymentsResponseData::SessionResponse { .. } => (None, None), - types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), - types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), - types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), - types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), - types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => (None, None), - types::PaymentsResponseData::MultipleCaptureResponse { - capture_sync_response_list, - } => match payment_data.multiple_capture_data { - Some(multiple_capture_data) => { - let capture_update_list = response_to_capture_update( - &multiple_capture_data, - capture_sync_response_list, - )?; - (Some((multiple_capture_data, capture_update_list)), None) + (capture_updates, payment_attempt_update) } - None => (None, None), - }, - }, + types::PaymentsResponseData::TransactionUnresolvedResponse { + resource_id, + reason, + connector_response_reference_id, + } => { + let connector_transaction_id = match resource_id { + types::ResponseId::NoResponseId => None, + types::ResponseId::ConnectorTransactionId(id) + | types::ResponseId::EncodedData(id) => Some(id), + }; + ( + None, + Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate { + status: updated_attempt_status, + connector: None, + connector_transaction_id, + payment_method_id: Some(router_data.payment_method_id), + error_code: Some(reason.clone().map(|cd| cd.code)), + error_message: Some(reason.clone().map(|cd| cd.message)), + error_reason: Some(reason.map(|cd| cd.message)), + connector_response_reference_id, + updated_by: storage_scheme.to_string(), + }), + ) + } + types::PaymentsResponseData::SessionResponse { .. } => (None, None), + types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None), + types::PaymentsResponseData::TokenizationResponse { .. } => (None, None), + types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None), + types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None), + types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => { + (None, None) + } + types::PaymentsResponseData::MultipleCaptureResponse { + capture_sync_response_list, + } => match payment_data.multiple_capture_data { + Some(multiple_capture_data) => { + let capture_update_list = response_to_capture_update( + &multiple_capture_data, + capture_sync_response_list, + )?; + (Some((multiple_capture_data, capture_update_list)), None) + } + None => (None, None), + }, + } + } }; payment_data.multiple_capture_data = match capture_update { Some((mut multiple_capture_data, capture_updates)) => { diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index 0fd45c5af3b5..8d74eb3fa961 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -40,6 +40,7 @@ pub async fn do_gsm_actions( customer: &Option, validate_result: &operations::ValidateResult<'_>, schedule_time: Option, + frm_suggestion: Option, ) -> RouterResult> where F: Clone + Send + Sync, @@ -90,6 +91,7 @@ where validate_result, schedule_time, true, + frm_suggestion, ) .await?; } @@ -133,6 +135,7 @@ where schedule_time, //this is an auto retry payment, but not step-up false, + frm_suggestion, ) .await?; @@ -275,6 +278,7 @@ pub async fn do_retry( validate_result: &operations::ValidateResult<'_>, schedule_time: Option, is_step_up: bool, + frm_suggestion: Option, ) -> RouterResult> where F: Clone + Send + Sync, @@ -310,6 +314,7 @@ where validate_result, schedule_time, api::HeaderPayload::default(), + frm_suggestion, ) .await } diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index b836f02cded2..bea288c40fce 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -932,7 +932,7 @@ pub async fn payments_approve( payload.clone(), |state, auth, req| { payments::payments_core::< - api_types::Authorize, + api_types::Capture, payment_types::PaymentsResponse, _, _, @@ -943,10 +943,8 @@ pub async fn payments_approve( auth.merchant_account, auth.key_store, payments::PaymentApprove, - payment_types::PaymentsRequest { - payment_id: Some(payment_types::PaymentIdType::PaymentIntentId( - req.payment_id, - )), + payment_types::PaymentsCaptureRequest { + payment_id: req.payment_id, ..Default::default() }, api::AuthFlow::Merchant, From defd14704098c00774bc2c36637808ec75f75603 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 21 Dec 2023 14:42:29 +0530 Subject: [PATCH 2/3] fix(frm): call connector's PaymentVoid action on merchant's decision to reject the txn --- crates/router/src/core/payments.rs | 1 + .../payments/operations/payment_reject.rs | 20 +++++++++---------- crates/router/src/routes/payments.rs | 8 ++++++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index bf255b1811dd..c1214e494113 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -2106,6 +2106,7 @@ pub fn should_call_connector( } "CompleteAuthorize" => true, "PaymentApprove" => true, + "PaymentReject" => true, "PaymentSession" => true, "PaymentIncrementalAuthorization" => matches!( payment_data.payment_intent.status, diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 37c7dfd1bae6..90ecc93075f5 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::{enums::FrmSuggestion, payments::PaymentsRejectRequest}; +use api_models::{enums::FrmSuggestion, payments::PaymentsCancelRequest}; use async_trait::async_trait; use error_stack::ResultExt; use router_derive; @@ -24,24 +24,24 @@ use crate::{ }; #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] -#[operation(operations = "all", flow = "reject")] +#[operation(operations = "all", flow = "cancel")] pub struct PaymentReject; #[async_trait] impl - GetTracker, PaymentsRejectRequest, Ctx> for PaymentReject + GetTracker, PaymentsCancelRequest, Ctx> for PaymentReject { #[instrument(skip_all)] async fn get_trackers<'a>( &'a self, state: &'a AppState, payment_id: &api::PaymentIdType, - _request: &PaymentsRejectRequest, + _request: &PaymentsCancelRequest, _mandate_type: Option, merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, - ) -> RouterResult> { + ) -> RouterResult> { let db = &*state.store; let merchant_id = &merchant_account.merchant_id; let storage_scheme = merchant_account.storage_scheme; @@ -176,7 +176,7 @@ impl #[async_trait] impl - UpdateTracker, PaymentsRejectRequest, Ctx> for PaymentReject + UpdateTracker, PaymentsCancelRequest, Ctx> for PaymentReject { #[instrument(skip_all)] async fn update_trackers<'b>( @@ -190,7 +190,7 @@ impl _should_decline_transaction: Option, _header_payload: api::HeaderPayload, ) -> RouterResult<( - BoxedOperation<'b, F, PaymentsRejectRequest, Ctx>, + BoxedOperation<'b, F, PaymentsCancelRequest, Ctx>, PaymentData, )> where @@ -242,16 +242,16 @@ impl } } -impl ValidateRequest +impl ValidateRequest for PaymentReject { #[instrument(skip_all)] fn validate_request<'a, 'b>( &'b self, - request: &PaymentsRejectRequest, + request: &PaymentsCancelRequest, merchant_account: &'a domain::MerchantAccount, ) -> RouterResult<( - BoxedOperation<'b, F, PaymentsRejectRequest, Ctx>, + BoxedOperation<'b, F, PaymentsCancelRequest, Ctx>, operations::ValidateResult<'a>, )> { Ok(( diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index bea288c40fce..b3b35ea5e900 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -990,7 +990,7 @@ pub async fn payments_reject( payload.clone(), |state, auth, req| { payments::payments_core::< - api_types::Reject, + api_types::Void, payment_types::PaymentsResponse, _, _, @@ -1001,7 +1001,11 @@ pub async fn payments_reject( auth.merchant_account, auth.key_store, payments::PaymentReject, - req, + payment_types::PaymentsCancelRequest { + payment_id: req.payment_id, + cancellation_reason: Some("Rejected by merchant".to_string()), + ..Default::default() + }, api::AuthFlow::Merchant, payments::CallConnectorAction::Trigger, None, From acb2fec52c3245fd2e9b301fd78dd4844b5bf178 Mon Sep 17 00:00:00 2001 From: Kashif Date: Tue, 9 Jan 2024 15:14:44 +0530 Subject: [PATCH 3/3] fix(frm): disallow payment rejection for payment intent status - cancelled --- crates/router/src/core/payments/operations/payment_reject.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index 90ecc93075f5..e958422d3127 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -57,6 +57,7 @@ impl helpers::validate_payment_status_against_not_allowed_statuses( &payment_intent.status, &[ + enums::IntentStatus::Cancelled, enums::IntentStatus::Failed, enums::IntentStatus::Succeeded, enums::IntentStatus::Processing,