From 229f111f6cb4ea30caa7b89328a047a1be8b9be0 Mon Sep 17 00:00:00 2001 From: Jagan Date: Mon, 4 Sep 2023 17:44:39 +0530 Subject: [PATCH] feat(frm): Add support to accept and decline payment when manually reviewed by merchant for risky transaction (#2071) --- crates/api_models/src/payments.rs | 17 + crates/common_enums/src/enums.rs | 23 +- .../src/payments/payment_intent.rs | 26 ++ crates/diesel_models/src/payment_attempt.rs | 21 + crates/diesel_models/src/payment_intent.rs | 24 + crates/diesel_models/src/schema.rs | 2 + crates/router/src/core/payments.rs | 11 +- crates/router/src/core/payments/flows.rs | 154 +++++++ .../src/core/payments/flows/approve_flow.rs | 78 ++++ .../src/core/payments/flows/reject_flow.rs | 77 ++++ crates/router/src/core/payments/helpers.rs | 3 + crates/router/src/core/payments/operations.rs | 61 ++- .../payments/operations/payment_approve.rs | 411 ++++++++++++++++++ .../payments/operations/payment_cancel.rs | 4 +- .../payments/operations/payment_capture.rs | 4 +- .../operations/payment_complete_authorize.rs | 4 +- .../payments/operations/payment_confirm.rs | 35 +- .../payments/operations/payment_create.rs | 5 +- .../operations/payment_method_validate.rs | 4 +- .../payments/operations/payment_reject.rs | 239 ++++++++++ .../payments/operations/payment_response.rs | 58 ++- .../payments/operations/payment_session.rs | 4 +- .../core/payments/operations/payment_start.rs | 4 +- .../payments/operations/payment_status.rs | 27 +- .../payments/operations/payment_update.rs | 4 +- .../router/src/core/payments/transformers.rs | 44 +- crates/router/src/db/payment_intent.rs | 1 + crates/router/src/routes/payment_methods.rs | 18 +- crates/router/src/types.rs | 18 + crates/router/src/types/api/payments.rs | 30 +- crates/router_derive/src/macros/operation.rs | 13 + crates/router_env/src/logger/types.rs | 4 + .../src/payments/payment_intent.rs | 25 ++ .../down.sql | 1 + .../up.sql | 1 + openapi/openapi_spec.json | 5 + 36 files changed, 1382 insertions(+), 78 deletions(-) create mode 100644 crates/router/src/core/payments/flows/approve_flow.rs create mode 100644 crates/router/src/core/payments/flows/reject_flow.rs create mode 100644 crates/router/src/core/payments/operations/payment_approve.rs create mode 100644 crates/router/src/core/payments/operations/payment_reject.rs create mode 100644 migrations/2023-08-31-093852_add_merchant_decision/down.sql create mode 100644 migrations/2023-08-31-093852_add_merchant_decision/up.sql diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 7d0f24ebbb4..c5267c9ab81 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -1908,6 +1908,9 @@ pub struct PaymentsResponse { /// total number of attempts associated with this payment pub attempt_count: i16, + + /// Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment + pub merchant_decision: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema)] @@ -2632,6 +2635,20 @@ pub struct PaymentsCancelRequest { pub merchant_connector_details: Option, } +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct PaymentsApproveRequest { + /// The identifier for the payment + #[serde(skip)] + pub payment_id: String, +} + +#[derive(Default, Debug, serde::Deserialize, serde::Serialize, Clone, ToSchema)] +pub struct PaymentsRejectRequest { + /// The identifier for the payment + #[serde(skip)] + pub payment_id: String, +} + #[derive(Default, Debug, serde::Deserialize, serde::Serialize, ToSchema, Clone)] pub struct PaymentsStartRequest { /// Unique identifier for the payment. This ensures idempotency for multiple payments diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index e23d90acff2..13ee351fe6b 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -1675,6 +1675,25 @@ pub enum PayoutEntityType { Personal, } +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[strum(serialize_all = "snake_case")] +pub enum MerchantDecision { + Approved, + Rejected, + AutoRefunded, +} + #[derive( Clone, Copy, @@ -1691,9 +1710,11 @@ pub enum PayoutEntityType { )] #[serde(rename_all = "snake_case")] #[strum(serialize_all = "snake_case")] -pub enum CancelTransaction { +pub enum FrmSuggestion { #[default] FrmCancelTransaction, + FrmManualReview, + FrmAutoRefund, } #[derive( diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 74d23695ed9..345ac13143d 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -101,6 +101,9 @@ pub struct PaymentIntent { pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: Option, + // Denotes the action(approve or reject) taken by merchant in case of manual review. + // Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment + pub merchant_decision: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -138,6 +141,7 @@ pub struct PaymentIntentNew { pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: Option, + pub merchant_decision: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -191,6 +195,13 @@ pub enum PaymentIntentUpdate { active_attempt_id: String, attempt_count: i16, }, + ApproveUpdate { + merchant_decision: Option, + }, + RejectUpdate { + status: storage_enums::IntentStatus, + merchant_decision: Option, + }, } #[derive(Clone, Debug, Default)] @@ -215,6 +226,9 @@ pub struct PaymentIntentUpdateInternal { pub statement_descriptor_suffix: Option, pub order_details: Option>, pub attempt_count: Option, + // Denotes the action(approve or reject) taken by merchant in case of manual review. + // Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment + pub merchant_decision: Option, } impl PaymentIntentUpdate { @@ -354,6 +368,18 @@ impl From for PaymentIntentUpdateInternal { attempt_count: Some(attempt_count), ..Default::default() }, + PaymentIntentUpdate::ApproveUpdate { merchant_decision } => Self { + merchant_decision, + ..Default::default() + }, + PaymentIntentUpdate::RejectUpdate { + status, + merchant_decision, + } => Self { + status: Some(status), + merchant_decision, + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 826d4c0ee90..9cb025c52ba 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -153,11 +153,18 @@ pub enum PaymentAttemptUpdate { payment_experience: Option, business_sub_label: Option, straight_through_algorithm: Option, + error_code: Option>, + error_message: Option>, }, VoidUpdate { status: storage_enums::AttemptStatus, cancellation_reason: Option, }, + RejectUpdate { + status: storage_enums::AttemptStatus, + error_code: Option>, + error_message: Option>, + }, ResponseUpdate { status: storage_enums::AttemptStatus, connector: Option, @@ -321,6 +328,8 @@ impl From for PaymentAttemptUpdateInternal { payment_experience, business_sub_label, straight_through_algorithm, + error_code, + error_message, } => Self { amount: Some(amount), currency: Some(currency), @@ -336,6 +345,8 @@ impl From for PaymentAttemptUpdateInternal { payment_experience, business_sub_label, straight_through_algorithm, + error_code, + error_message, ..Default::default() }, PaymentAttemptUpdate::VoidUpdate { @@ -346,6 +357,16 @@ impl From for PaymentAttemptUpdateInternal { cancellation_reason, ..Default::default() }, + PaymentAttemptUpdate::RejectUpdate { + status, + error_code, + error_message, + } => Self { + status: Some(status), + error_code, + error_message, + ..Default::default() + }, PaymentAttemptUpdate::ResponseUpdate { status, connector, diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 4b345233361..123d734d20b 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -43,6 +43,9 @@ pub struct PaymentIntent { pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: Option, + // Denotes the action(approve or reject) taken by merchant in case of manual review. + // Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment + pub merchant_decision: Option, } #[derive( @@ -92,6 +95,7 @@ pub struct PaymentIntentNew { pub feature_metadata: Option, pub attempt_count: i16, pub profile_id: Option, + pub merchant_decision: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -145,6 +149,13 @@ pub enum PaymentIntentUpdate { active_attempt_id: String, attempt_count: i16, }, + ApproveUpdate { + merchant_decision: Option, + }, + RejectUpdate { + status: storage_enums::IntentStatus, + merchant_decision: Option, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -172,6 +183,7 @@ pub struct PaymentIntentUpdateInternal { #[diesel(deserialize_as = super::OptionalDieselArray)] pub order_details: Option>, pub attempt_count: Option, + merchant_decision: Option, } impl PaymentIntentUpdate { @@ -311,6 +323,18 @@ impl From for PaymentIntentUpdateInternal { attempt_count: Some(attempt_count), ..Default::default() }, + PaymentIntentUpdate::ApproveUpdate { merchant_decision } => Self { + merchant_decision, + ..Default::default() + }, + PaymentIntentUpdate::RejectUpdate { + status, + merchant_decision, + } => Self { + status: Some(status), + merchant_decision, + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e85a9be0735..71e49ea37b4 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -595,6 +595,8 @@ diesel::table! { attempt_count -> Int2, #[max_length = 64] profile_id -> Nullable, + #[max_length = 64] + merchant_decision -> Nullable, } } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 8783f1f9d31..0736f7f2bcf 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -9,9 +9,8 @@ pub mod types; use std::{fmt::Debug, marker::PhantomData, ops::Deref, time::Instant}; -use api_models::payments::FrmMessage; use common_utils::{ext_traits::AsyncExt, pii}; -use diesel_models::ephemeral_key; +use diesel_models::{ephemeral_key, fraud_check::FraudCheck}; use error_stack::{IntoReport, ResultExt}; use futures::future::join_all; use masking::Secret; @@ -19,8 +18,9 @@ use router_env::{instrument, tracing}; use time; pub use self::operations::{ - PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, PaymentMethodValidate, - PaymentResponse, PaymentSession, PaymentStatus, PaymentUpdate, + PaymentApprove, PaymentCancel, PaymentCapture, PaymentConfirm, PaymentCreate, + PaymentMethodValidate, PaymentReject, PaymentResponse, PaymentSession, PaymentStatus, + PaymentUpdate, }; use self::{ flows::{ConstructFlowSpecificData, Feature}, @@ -1146,7 +1146,7 @@ where pub recurring_mandate_payment_data: Option, pub ephemeral_key: Option, pub redirect_response: Option, - pub frm_message: Option, + pub frm_message: Option, } #[derive(Debug, Default, Clone)] @@ -1239,6 +1239,7 @@ pub fn should_call_connector( ) } "CompleteAuthorize" => true, + "PaymentApprove" => true, "PaymentSession" => true, _ => false, } diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs index 9d084a79db9..57b6dce522c 100644 --- a/crates/router/src/core/payments/flows.rs +++ b/crates/router/src/core/payments/flows.rs @@ -1,8 +1,10 @@ +pub mod approve_flow; pub mod authorize_flow; pub mod cancel_flow; pub mod capture_flow; pub mod complete_authorize_flow; pub mod psync_flow; +pub mod reject_flow; pub mod session_flow; pub mod verify_flow; @@ -1280,3 +1282,155 @@ default_imp_for_payouts_recipient!( connector::Worldpay, connector::Zen ); + +macro_rules! default_imp_for_approve { + ($($path:ident::$connector:ident),*) => { + $( + impl api::PaymentApprove for $path::$connector {} + impl + services::ConnectorIntegration< + api::Approve, + types::PaymentsApproveData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::PaymentApprove for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::Approve, + types::PaymentsApproveData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_approve!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); + +macro_rules! default_imp_for_reject { + ($($path:ident::$connector:ident),*) => { + $( + impl api::PaymentReject for $path::$connector {} + impl + services::ConnectorIntegration< + api::Reject, + types::PaymentsRejectData, + types::PaymentsResponseData, + > for $path::$connector + {} + )* + }; +} + +#[cfg(feature = "dummy_connector")] +impl api::PaymentReject for connector::DummyConnector {} +#[cfg(feature = "dummy_connector")] +impl + services::ConnectorIntegration< + api::Reject, + types::PaymentsRejectData, + types::PaymentsResponseData, + > for connector::DummyConnector +{ +} + +default_imp_for_reject!( + connector::Aci, + connector::Adyen, + connector::Airwallex, + connector::Authorizedotnet, + connector::Bambora, + connector::Bitpay, + connector::Bluesnap, + connector::Boku, + connector::Braintree, + connector::Cashtocode, + connector::Checkout, + connector::Cryptopay, + connector::Cybersource, + connector::Coinbase, + connector::Dlocal, + connector::Fiserv, + connector::Forte, + connector::Globalpay, + connector::Globepay, + connector::Helcim, + connector::Iatapay, + connector::Klarna, + connector::Mollie, + connector::Multisafepay, + connector::Nexinets, + connector::Nmi, + connector::Noon, + connector::Nuvei, + connector::Opayo, + connector::Opennode, + connector::Payeezy, + connector::Payme, + connector::Paypal, + connector::Payu, + connector::Powertranz, + connector::Rapyd, + connector::Square, + connector::Stax, + connector::Stripe, + connector::Shift4, + connector::Trustpay, + connector::Tsys, + connector::Wise, + connector::Worldline, + connector::Worldpay, + connector::Zen +); diff --git a/crates/router/src/core/payments/flows/approve_flow.rs b/crates/router/src/core/payments/flows/approve_flow.rs new file mode 100644 index 00000000000..5a17dba2c94 --- /dev/null +++ b/crates/router/src/core/payments/flows/approve_flow.rs @@ -0,0 +1,78 @@ +use async_trait::async_trait; + +use super::{ConstructFlowSpecificData, Feature}; +use crate::{ + core::{ + errors::{api_error_response::NotImplementedMessage, ApiErrorResponse, RouterResult}, + payments::{self, access_token, transformers, PaymentData}, + }, + routes::AppState, + services, + types::{self, api, domain}, +}; + +#[async_trait] +impl + ConstructFlowSpecificData + for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + ) -> RouterResult { + transformers::construct_payment_router_data::( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + ) + .await + } +} + +#[async_trait] +impl Feature + for types::RouterData +{ + async fn decide_flows<'a>( + self, + _state: &AppState, + _connector: &api::ConnectorData, + _customer: &Option, + _call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, + _connector_request: Option, + ) -> RouterResult { + Err(ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("Flow not supported".to_string()), + } + .into()) + } + + async fn add_access_token<'a>( + &self, + state: &AppState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self).await + } + + async fn build_flow_specific_connector_request( + &mut self, + _state: &AppState, + _connector: &api::ConnectorData, + _call_connector_action: payments::CallConnectorAction, + ) -> RouterResult<(Option, bool)> { + Err(ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("Flow not supported".to_string()), + } + .into()) + } +} diff --git a/crates/router/src/core/payments/flows/reject_flow.rs b/crates/router/src/core/payments/flows/reject_flow.rs new file mode 100644 index 00000000000..1dcd6fd8892 --- /dev/null +++ b/crates/router/src/core/payments/flows/reject_flow.rs @@ -0,0 +1,77 @@ +use async_trait::async_trait; + +use super::{ConstructFlowSpecificData, Feature}; +use crate::{ + core::{ + errors::{api_error_response::NotImplementedMessage, ApiErrorResponse, RouterResult}, + payments::{self, access_token, transformers, PaymentData}, + }, + routes::AppState, + services, + types::{self, api, domain}, +}; + +#[async_trait] +impl ConstructFlowSpecificData + for PaymentData +{ + async fn construct_router_data<'a>( + &self, + state: &AppState, + connector_id: &str, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + customer: &Option, + ) -> RouterResult { + transformers::construct_payment_router_data::( + state, + self.clone(), + connector_id, + merchant_account, + key_store, + customer, + ) + .await + } +} + +#[async_trait] +impl Feature + for types::RouterData +{ + async fn decide_flows<'a>( + self, + _state: &AppState, + _connector: &api::ConnectorData, + _customer: &Option, + _call_connector_action: payments::CallConnectorAction, + _merchant_account: &domain::MerchantAccount, + _connector_request: Option, + ) -> RouterResult { + Err(ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("Flow not supported".to_string()), + } + .into()) + } + + async fn add_access_token<'a>( + &self, + state: &AppState, + connector: &api::ConnectorData, + merchant_account: &domain::MerchantAccount, + ) -> RouterResult { + access_token::add_access_token(state, connector, merchant_account, self).await + } + + async fn build_flow_specific_connector_request( + &mut self, + _state: &AppState, + _connector: &api::ConnectorData, + _call_connector_action: payments::CallConnectorAction, + ) -> RouterResult<(Option, bool)> { + Err(ApiErrorResponse::NotImplemented { + message: NotImplementedMessage::Reason("Flow not supported".to_string()), + } + .into()) + } +} diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index dda14c12ee7..f927adf116c 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2233,6 +2233,7 @@ mod tests { feature_metadata: None, attempt_count: 1, profile_id: None, + merchant_decision: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(900); @@ -2278,6 +2279,7 @@ mod tests { feature_metadata: None, attempt_count: 1, profile_id: None, + merchant_decision: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -2323,6 +2325,7 @@ mod tests { feature_metadata: None, attempt_count: 1, profile_id: None, + merchant_decision: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index c697693c451..71fb6154ae4 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -1,24 +1,27 @@ +pub mod payment_approve; pub mod payment_cancel; pub mod payment_capture; pub mod payment_complete_authorize; pub mod payment_confirm; pub mod payment_create; pub mod payment_method_validate; +pub mod payment_reject; pub mod payment_response; pub mod payment_session; pub mod payment_start; pub mod payment_status; pub mod payment_update; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use error_stack::{report, ResultExt}; use router_env::{instrument, tracing}; pub use self::{ - payment_cancel::PaymentCancel, payment_capture::PaymentCapture, - payment_confirm::PaymentConfirm, payment_create::PaymentCreate, - payment_method_validate::PaymentMethodValidate, payment_response::PaymentResponse, + payment_approve::PaymentApprove, payment_cancel::PaymentCancel, + payment_capture::PaymentCapture, payment_confirm::PaymentConfirm, + payment_create::PaymentCreate, payment_method_validate::PaymentMethodValidate, + payment_reject::PaymentReject, payment_response::PaymentResponse, payment_session::PaymentSession, payment_start::PaymentStart, payment_status::PaymentStatus, payment_update::PaymentUpdate, }; @@ -150,7 +153,7 @@ pub trait UpdateTracker: Send { storage_scheme: enums::MerchantStorageScheme, updated_customer: Option, mechant_key_store: &domain::MerchantKeyStore, - should_cancel_transaction: Option, + frm_suggestion: Option, ) -> RouterResult<(BoxedOperation<'b, F, Req>, D)> where F: 'b + Send; @@ -342,3 +345,51 @@ where helpers::get_connector_default(state, None).await } } + +#[async_trait] +impl> + Domain for Op +where + for<'a> &'a Op: Operation, +{ + #[instrument(skip_all)] + async fn get_or_create_customer_details<'a>( + &'a self, + _db: &dyn StorageInterface, + _payment_data: &mut PaymentData, + _request: Option, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult< + ( + BoxedOperation<'a, F, api::PaymentsRejectRequest>, + Option, + ), + errors::StorageError, + > { + Ok((Box::new(self), None)) + } + + #[instrument(skip_all)] + async fn make_pm_data<'a>( + &'a self, + _state: &'a AppState, + _payment_data: &mut PaymentData, + _storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult<( + BoxedOperation<'a, F, api::PaymentsRejectRequest>, + Option, + )> { + Ok((Box::new(self), None)) + } + + async fn get_connector<'a>( + &'a self, + _merchant_account: &domain::MerchantAccount, + state: &AppState, + _request: &api::PaymentsRejectRequest, + _payment_intent: &storage::PaymentIntent, + _merchant_key_store: &domain::MerchantKeyStore, + ) -> CustomResult { + helpers::get_connector_default(state, None).await + } +} diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs new file mode 100644 index 00000000000..22fdfba9fa3 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -0,0 +1,411 @@ +use std::marker::PhantomData; + +use api_models::enums::FrmSuggestion; +use async_trait::async_trait; +use error_stack::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}, + payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + utils as core_utils, + }, + db::StorageInterface, + routes::AppState, + services, + types::{ + self, + api::{self, PaymentIdTypeExt}, + domain, + storage::{self, enums as storage_enums}, + transformers::ForeignInto, + }, + utils::{self, OptionExt}, +}; + +#[derive(Debug, Clone, Copy, PaymentOperation)] +#[operation(ops = "all", flow = "authorize")] +pub struct PaymentApprove; + +#[async_trait] +impl GetTracker, api::PaymentsRequest> 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, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + _auth_flow: services::AuthFlow, + ) -> RouterResult<( + BoxedOperation<'a, F, api::PaymentsRequest>, + PaymentData, + Option, + )> { + 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 payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_intent = db + .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, + &[ + storage_enums::IntentStatus::Failed, + storage_enums::IntentStatus::Succeeded, + ], + "confirm", + )?; + + 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, + ) + .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_id.clone(); + payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + &payment_intent.payment_id, + merchant_id, + &attempt_id.clone(), + storage_scheme, + ) + .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.shipping.is_some(), + request.billing.is_some(), + request.setup_future_usage.is_some(), + &payment_intent + .customer_id + .clone() + .or_else(|| request.customer_id.clone()), + )?; + + let shipping_address = helpers::get_address_for_payment_request( + db, + request.shipping.as_ref(), + payment_intent.shipping_address_id.as_deref(), + merchant_id, + payment_intent.customer_id.as_ref(), + key_store, + ) + .await?; + let billing_address = helpers::get_address_for_payment_request( + db, + request.billing.as_ref(), + payment_intent.billing_address_id.as_deref(), + merchant_id, + payment_intent.customer_id.as_ref(), + key_store, + ) + .await?; + + let connector_response = db + .find_connector_response_by_payment_id_merchant_id_attempt_id( + &payment_attempt.payment_id, + &payment_attempt.merchant_id, + &payment_attempt.attempt_id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + 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()); + + 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| api_models::payments::MandateData { + customer_acceptance: mandate_data.customer_acceptance, + mandate_type: payment_attempt + .mandate_details + .clone() + .map(ForeignInto::foreign_into) + .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()) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable_lazy(|| { + format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) + }); + + Ok(( + Box::new(self), + PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + connector_response, + amount, + email: request.email.clone(), + mandate_id: None, + mandate_connector, + setup_mandate, + token, + 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(), + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + sessions_token: vec![], + card_cvc: request.card_cvc.clone(), + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response, + frm_message: frm_response.ok(), + }, + 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(), + }), + )) + } +} + +#[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>, + 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, + ) -> RouterResult<( + BoxedOperation<'a, F, api::PaymentsRequest>, + Option, + )> { + let (op, payment_method_data) = + helpers::make_pm_data(Box::new(self), state, payment_data).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> for PaymentApprove { + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + db: &dyn StorageInterface, + mut payment_data: PaymentData, + _customer: Option, + storage_scheme: storage_enums::MerchantStorageScheme, + _updated_customer: Option, + _merchant_key_store: &domain::MerchantKeyStore, + _frm_suggestion: Option, + ) -> RouterResult<(BoxedOperation<'b, F, api::PaymentsRequest>, PaymentData)> + where + F: 'b + Send, + { + let intent_status_update = storage::PaymentIntentUpdate::ApproveUpdate { + merchant_decision: Some(api_models::enums::MerchantDecision::Approved.to_string()), + }; + payment_data.payment_intent = db + .update_payment_intent( + payment_data.payment_intent, + intent_status_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + Ok((Box::new(self), payment_data)) + } +} + +impl ValidateRequest for PaymentApprove { + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &api::PaymentsRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'b, F, api::PaymentsRequest>, + operations::ValidateResult<'a>, + )> { + let given_payment_id = match &request.payment_id { + Some(id_type) => Some( + id_type + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?, + ), + None => None, + }; + + let request_merchant_id = request.merchant_id.as_deref(); + helpers::validate_merchant_id(&merchant_account.merchant_id, request_merchant_id) + .change_context(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "merchant_id".to_string(), + expected_format: "merchant_id from merchant account".to_string(), + })?; + + helpers::validate_payment_method_fields_present(request)?; + + let mandate_type = + helpers::validate_mandate(request, payments::is_operation_confirm(self))?; + let payment_id = core_utils::get_or_generate_id("payment_id", &given_payment_id, "pay")?; + + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: &merchant_account.merchant_id, + payment_id: api::PaymentIdType::PaymentIntentId(payment_id), + mandate_type, + storage_scheme: merchant_account.storage_scheme, + requeue: matches!( + request.retry_action, + Some(api_models::enums::RetryAction::Requeue) + ), + }, + )) + } +} diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 1a6dc8ee8e6..b219021c578 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; use error_stack::ResultExt; @@ -182,7 +182,7 @@ impl UpdateTracker, api::PaymentsCancelRequest> for storage_scheme: enums::MerchantStorageScheme, _updated_customer: Option, _mechant_key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsCancelRequest>, PaymentData, diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index ed47e4705da..0ebc0668ee7 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::AsyncExt; use diesel_models::connector_response::ConnectorResponse; @@ -252,7 +252,7 @@ impl UpdateTracker, api::PaymentsCaptureRe storage_scheme: enums::MerchantStorageScheme, _updated_customer: Option, _mechant_key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsCaptureRequest>, payments::PaymentData, diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index f0e27008597..045c286dbaa 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use error_stack::ResultExt; use router_derive::PaymentOperation; @@ -332,7 +332,7 @@ impl UpdateTracker, api::PaymentsRequest> for Comple _storage_scheme: storage_enums::MerchantStorageScheme, _updated_customer: Option, _merchant_key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<(BoxedOperation<'b, F, api::PaymentsRequest>, PaymentData)> where F: 'b + Send, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 0d08f1c9c2f..e1a148640fd 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::ResultExt; @@ -387,24 +387,35 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen storage_scheme: storage_enums::MerchantStorageScheme, updated_customer: Option, key_store: &domain::MerchantKeyStore, - should_cancel_transaction: Option, + frm_suggestion: Option, ) -> RouterResult<(BoxedOperation<'b, F, api::PaymentsRequest>, PaymentData)> where F: 'b + Send, { let payment_method = payment_data.payment_attempt.payment_method; let browser_info = payment_data.payment_attempt.browser_info.clone(); - - let (intent_status, attempt_status) = match should_cancel_transaction { - Some(should_cancel) => match should_cancel { - CancelTransaction::FrmCancelTransaction => ( - storage_enums::IntentStatus::Failed, - storage_enums::AttemptStatus::Failure, - ), - }, - None => ( + let frm_message = payment_data.frm_message.clone(); + + let (intent_status, attempt_status, (error_code, error_message)) = match frm_suggestion { + Some(FrmSuggestion::FrmCancelTransaction) => ( + storage_enums::IntentStatus::Failed, + storage_enums::AttemptStatus::Failure, + frm_message.map_or((None, None), |fraud_check| { + ( + Some(Some(fraud_check.frm_status.to_string())), + Some(fraud_check.frm_reason.map(|reason| reason.to_string())), + ) + }), + ), + Some(FrmSuggestion::FrmManualReview) => ( + storage_enums::IntentStatus::RequiresMerchantAction, + storage_enums::AttemptStatus::Unresolved, + (None, None), + ), + _ => ( storage_enums::IntentStatus::Processing, storage_enums::AttemptStatus::Pending, + (None, None), ), }; @@ -448,6 +459,8 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen payment_experience, business_sub_label, straight_through_algorithm, + error_code, + error_message, }, storage_scheme, ) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 82a616773eb..6e33ba88ffb 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use diesel_models::ephemeral_key; @@ -365,7 +365,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen storage_scheme: enums::MerchantStorageScheme, _updated_customer: Option, _merchant_key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<(BoxedOperation<'b, F, api::PaymentsRequest>, PaymentData)> where F: 'b + Send, @@ -675,6 +675,7 @@ impl PaymentCreate { feature_metadata, attempt_count: 1, profile_id, + merchant_decision: None, }) } diff --git a/crates/router/src/core/payments/operations/payment_method_validate.rs b/crates/router/src/core/payments/operations/payment_method_validate.rs index dedacba5f3c..2f845a1341b 100644 --- a/crates/router/src/core/payments/operations/payment_method_validate.rs +++ b/crates/router/src/core/payments/operations/payment_method_validate.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::{date_time, errors::CustomResult, ext_traits::AsyncExt}; use error_stack::ResultExt; @@ -214,7 +214,7 @@ impl UpdateTracker, api::VerifyRequest> for PaymentM storage_scheme: storage_enums::MerchantStorageScheme, _updated_customer: Option, _mechant_key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<(BoxedOperation<'b, F, api::VerifyRequest>, PaymentData)> where F: 'b + Send, diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs new file mode 100644 index 00000000000..e156f66d145 --- /dev/null +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -0,0 +1,239 @@ +use std::marker::PhantomData; + +use api_models::{enums::FrmSuggestion, payments::PaymentsRejectRequest}; +use async_trait::async_trait; +use error_stack::ResultExt; +use router_derive; +use router_env::{instrument, tracing}; + +use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; +use crate::{ + core::{ + errors::{self, RouterResult, StorageErrorExt}, + payments::{helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, + }, + db::StorageInterface, + routes::AppState, + services, + types::{ + api::{self, PaymentIdTypeExt}, + domain, + storage::{self, enums}, + }, + utils::OptionExt, +}; + +#[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] +#[operation(ops = "all", flow = "reject")] +pub struct PaymentReject; + +#[async_trait] +impl GetTracker, PaymentsRejectRequest> for PaymentReject { + #[instrument(skip_all)] + async fn get_trackers<'a>( + &'a self, + state: &'a AppState, + payment_id: &api::PaymentIdType, + _request: &PaymentsRejectRequest, + _mandate_type: Option, + merchant_account: &domain::MerchantAccount, + key_store: &domain::MerchantKeyStore, + _auth_flow: services::AuthFlow, + ) -> RouterResult<( + BoxedOperation<'a, F, PaymentsRejectRequest>, + PaymentData, + Option, + )> { + let db = &*state.store; + let merchant_id = &merchant_account.merchant_id; + let storage_scheme = merchant_account.storage_scheme; + let payment_id = payment_id + .get_payment_intent_id() + .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + + let payment_intent = db + .find_payment_intent_by_payment_id_merchant_id(&payment_id, merchant_id, storage_scheme) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + helpers::validate_payment_status_against_not_allowed_statuses( + &payment_intent.status, + &[ + enums::IntentStatus::Failed, + enums::IntentStatus::Succeeded, + enums::IntentStatus::Processing, + ], + "reject", + )?; + + let attempt_id = payment_intent.active_attempt_id.clone(); + let payment_attempt = db + .find_payment_attempt_by_payment_id_merchant_id_attempt_id( + payment_intent.payment_id.as_str(), + merchant_id, + attempt_id.clone().as_str(), + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + let shipping_address = helpers::get_address_for_payment_request( + db, + None, + payment_intent.shipping_address_id.as_deref(), + merchant_id, + payment_intent.customer_id.as_ref(), + key_store, + ) + .await?; + + let billing_address = helpers::get_address_for_payment_request( + db, + None, + payment_intent.billing_address_id.as_deref(), + merchant_id, + payment_intent.customer_id.as_ref(), + key_store, + ) + .await?; + + let connector_response = db + .find_connector_response_by_payment_id_merchant_id_attempt_id( + &payment_attempt.payment_id, + &payment_attempt.merchant_id, + &payment_attempt.attempt_id, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let currency = payment_attempt.currency.get_required_value("currency")?; + let amount = payment_attempt.amount.into(); + + let frm_response = db + .find_fraud_check_by_payment_id(payment_intent.payment_id.clone(), merchant_account.merchant_id.clone()) + .await + .change_context(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable_lazy(|| { + format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {attempt_id}", &merchant_account.merchant_id) + }); + + Ok(( + Box::new(self), + PaymentData { + flow: PhantomData, + payment_intent, + payment_attempt, + currency, + amount, + email: None, + mandate_id: None, + 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: None, + payment_method_data: None, + force_sync: None, + refunds: vec![], + disputes: vec![], + attempts: None, + connector_response, + sessions_token: vec![], + card_cvc: None, + creds_identifier: None, + pm_token: None, + connector_customer_id: None, + recurring_mandate_payment_data: None, + ephemeral_key: None, + multiple_capture_data: None, + redirect_response: None, + frm_message: frm_response.ok(), + }, + None, + )) + } +} + +#[async_trait] +impl UpdateTracker, PaymentsRejectRequest> for PaymentReject { + #[instrument(skip_all)] + async fn update_trackers<'b>( + &'b self, + db: &dyn StorageInterface, + mut payment_data: PaymentData, + _customer: Option, + storage_scheme: enums::MerchantStorageScheme, + _updated_customer: Option, + _mechant_key_store: &domain::MerchantKeyStore, + _should_decline_transaction: Option, + ) -> RouterResult<(BoxedOperation<'b, F, PaymentsRejectRequest>, PaymentData)> + where + F: 'b + Send, + { + let intent_status_update = storage::PaymentIntentUpdate::RejectUpdate { + status: enums::IntentStatus::Failed, + merchant_decision: Some(enums::MerchantDecision::Rejected.to_string()), + }; + let (error_code, error_message) = + payment_data + .frm_message + .clone() + .map_or((None, None), |fraud_check| { + ( + Some(Some(fraud_check.frm_status.to_string())), + Some(fraud_check.frm_reason.map(|reason| reason.to_string())), + ) + }); + let attempt_status_update = storage::PaymentAttemptUpdate::RejectUpdate { + status: enums::AttemptStatus::Failure, + error_code, + error_message, + }; + + payment_data.payment_intent = db + .update_payment_intent( + payment_data.payment_intent, + intent_status_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + payment_data.payment_attempt = db + .update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + attempt_status_update, + storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + + Ok((Box::new(self), payment_data)) + } +} + +impl ValidateRequest for PaymentReject { + #[instrument(skip_all)] + fn validate_request<'a, 'b>( + &'b self, + request: &PaymentsRejectRequest, + merchant_account: &'a domain::MerchantAccount, + ) -> RouterResult<( + BoxedOperation<'b, F, PaymentsRejectRequest>, + operations::ValidateResult<'a>, + )> { + Ok(( + Box::new(self), + operations::ValidateResult { + merchant_id: &merchant_account.merchant_id, + payment_id: api::PaymentIdType::PaymentIntentId(request.payment_id.to_owned()), + mandate_type: None, + storage_scheme: merchant_account.storage_scheme, + requeue: false, + }, + )) + } +} diff --git a/crates/router/src/core/payments/operations/payment_response.rs b/crates/router/src/core/payments/operations/payment_response.rs index a5268f39dcf..17de9ad25b1 100644 --- a/crates/router/src/core/payments/operations/payment_response.rs +++ b/crates/router/src/core/payments/operations/payment_response.rs @@ -26,7 +26,7 @@ use crate::{ #[derive(Debug, Clone, Copy, router_derive::PaymentOperation)] #[operation( ops = "post_tracker", - flow = "syncdata,authorizedata,canceldata,capturedata,completeauthorizedata,verifydata,sessiondata" + flow = "syncdata,authorizedata,canceldata,capturedata,completeauthorizedata,approvedata,rejectdata,verifydata,sessiondata" )] pub struct PaymentResponse; @@ -167,6 +167,62 @@ impl PostUpdateTracker, types::PaymentsCancelData> f } } +#[async_trait] +impl PostUpdateTracker, types::PaymentsApproveData> + for PaymentResponse +{ + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + payment_id: &api::PaymentIdType, + mut payment_data: PaymentData, + router_data: types::RouterData, + + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send, + { + payment_data = payment_response_update_tracker( + db, + payment_id, + payment_data, + router_data, + storage_scheme, + ) + .await?; + + Ok(payment_data) + } +} + +#[async_trait] +impl PostUpdateTracker, types::PaymentsRejectData> for PaymentResponse { + async fn update_tracker<'b>( + &'b self, + db: &dyn StorageInterface, + payment_id: &api::PaymentIdType, + mut payment_data: PaymentData, + router_data: types::RouterData, + + storage_scheme: enums::MerchantStorageScheme, + ) -> RouterResult> + where + F: 'b + Send, + { + payment_data = payment_response_update_tracker( + db, + payment_id, + payment_data, + router_data, + storage_scheme, + ) + .await?; + + Ok(payment_data) + } +} + #[async_trait] impl PostUpdateTracker, types::VerifyRequestData> for PaymentResponse { async fn update_tracker<'b>( diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 1502ce68ab9..604f442b621 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::{admin::PaymentMethodsEnabled, enums::CancelTransaction}; +use api_models::{admin::PaymentMethodsEnabled, enums::FrmSuggestion}; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, ValueExt}; use error_stack::ResultExt; @@ -204,7 +204,7 @@ impl UpdateTracker, api::PaymentsSessionRequest> for storage_scheme: storage_enums::MerchantStorageScheme, _updated_customer: Option, _mechant_key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsSessionRequest>, PaymentData, diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index bd8f5c2ac36..8699257019c 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use error_stack::ResultExt; use router_derive::PaymentOperation; @@ -177,7 +177,7 @@ impl UpdateTracker, api::PaymentsStartRequest> for P _storage_scheme: storage_enums::MerchantStorageScheme, _updated_customer: Option, _mechant_key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsStartRequest>, PaymentData, diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 044268ea0ac..324e82f7971 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::{errors::ReportSwitchExt, ext_traits::AsyncExt}; use error_stack::ResultExt; @@ -123,7 +123,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen _storage_scheme: enums::MerchantStorageScheme, _updated_customer: Option, _key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<(BoxedOperation<'b, F, api::PaymentsRequest>, PaymentData)> where F: 'b + Send, @@ -142,7 +142,7 @@ impl UpdateTracker, api::PaymentsRetrieveRequest> fo _storage_scheme: enums::MerchantStorageScheme, _updated_customer: Option, _key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<( BoxedOperation<'b, F, api::PaymentsRetrieveRequest>, PaymentData, @@ -313,25 +313,6 @@ async fn get_tracker_for_sync< format!("Error while retrieving frm_response, merchant_id: {}, payment_id: {payment_id_str}", &merchant_account.merchant_id) }); - let frm_message = match frm_response.ok() { - Some(response) => { - if response.frm_status.to_string() == "pending" { - None - } else { - Some(api_models::payments::FrmMessage { - frm_name: response.frm_name, - frm_transaction_id: response.frm_transaction_id, - frm_transaction_type: Some(response.frm_transaction_type.to_string()), - frm_status: Some(response.frm_status.to_string()), - frm_score: response.frm_score, - frm_reason: response.frm_reason, - frm_error: response.frm_error, - }) - } - } - None => None, - }; - let contains_encoded_data = connector_response.encoded_data.is_some(); let creds_identifier = request @@ -393,7 +374,7 @@ async fn get_tracker_for_sync< ephemeral_key: None, multiple_capture_data, redirect_response: None, - frm_message, + frm_message: frm_response.ok(), }, None, )) diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 3ca6b21ab30..ce3b3eeb449 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use api_models::enums::CancelTransaction; +use api_models::enums::FrmSuggestion; use async_trait::async_trait; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt}; use error_stack::ResultExt; @@ -426,7 +426,7 @@ impl UpdateTracker, api::PaymentsRequest> for Paymen storage_scheme: storage_enums::MerchantStorageScheme, _updated_customer: Option, _key_store: &domain::MerchantKeyStore, - _should_cancel_transaction: Option, + _frm_suggestion: Option, ) -> RouterResult<(BoxedOperation<'b, F, api::PaymentsRequest>, PaymentData)> where F: 'b + Send, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index bfab35d24c1..e2fdbd08d8b 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1,5 +1,6 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr}; +use api_models::payments::FrmMessage; use common_utils::fp_utils; use diesel_models::{ephemeral_key, payment_attempt::PaymentListFilters}; use error_stack::{IntoReport, ResultExt}; @@ -356,7 +357,7 @@ pub fn payments_to_payments_response( operation: &Op, ephemeral_key_option: Option, session_tokens: Vec, - frm_message: Option, + fraud_check: Option, mandate_data: Option, connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, connector_http_status_code: Option, @@ -424,6 +425,8 @@ where .change_context(errors::ApiErrorResponse::InvalidDataValue { field_name: "payment_method_data", })?; + let merchant_decision = payment_intent.merchant_decision.to_owned(); + let frm_message = fraud_check.map(FrmMessage::foreign_from); let payment_method_data_response = additional_payment_method_data.map(api::PaymentMethodDataResponse::from); @@ -602,6 +605,7 @@ where ) .set_ephemeral_key(ephemeral_key_option.map(ForeignFrom::foreign_from)) .set_frm_message(frm_message) + .set_merchant_decision(merchant_decision) .set_manual_retry_allowed(helpers::is_manual_retry_allowed( &payment_intent.status, &payment_attempt.status, @@ -1068,6 +1072,30 @@ impl TryFrom> for types::PaymentsCancelDa } } +impl TryFrom> for types::PaymentsApproveData { + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data; + Ok(Self { + amount: Some(payment_data.amount.into()), + currency: Some(payment_data.currency), + }) + } +} + +impl TryFrom> for types::PaymentsRejectData { + type Error = error_stack::Report; + + fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result { + let payment_data = additional_data.payment_data; + Ok(Self { + amount: Some(payment_data.amount.into()), + currency: Some(payment_data.currency), + }) + } +} + impl TryFrom> for types::PaymentsSessionData { type Error = error_stack::Report; @@ -1281,3 +1309,17 @@ impl TryFrom> for types::PaymentsPreProce }) } } + +impl ForeignFrom for FrmMessage { + fn foreign_from(fraud_check: payments::FraudCheck) -> Self { + Self { + frm_name: fraud_check.frm_name, + frm_transaction_id: fraud_check.frm_transaction_id, + frm_transaction_type: Some(fraud_check.frm_transaction_type.to_string()), + frm_status: Some(fraud_check.frm_status.to_string()), + frm_score: fraud_check.frm_score, + frm_reason: fraud_check.frm_reason, + frm_error: fraud_check.frm_error, + } + } +} diff --git a/crates/router/src/db/payment_intent.rs b/crates/router/src/db/payment_intent.rs index 88f428bdf43..8a70a208de5 100644 --- a/crates/router/src/db/payment_intent.rs +++ b/crates/router/src/db/payment_intent.rs @@ -103,6 +103,7 @@ impl PaymentIntentInterface for MockDb { feature_metadata: new.feature_metadata, attempt_count: new.attempt_count, profile_id: new.profile_id, + merchant_decision: new.merchant_decision, }; payment_intents.push(payment_intent.clone()); Ok(payment_intent) diff --git a/crates/router/src/routes/payment_methods.rs b/crates/router/src/routes/payment_methods.rs index 6e7a2c15da8..1024ef0092e 100644 --- a/crates/router/src/routes/payment_methods.rs +++ b/crates/router/src/routes/payment_methods.rs @@ -1,5 +1,6 @@ use actix_web::{web, HttpRequest, HttpResponse}; use common_utils::{consts::TOKEN_TTL, errors::CustomResult}; +use diesel_models::enums::IntentStatus; use error_stack::ResultExt; use router_env::{instrument, logger, tracing, Flow}; use time::PrimitiveDateTime; @@ -8,10 +9,7 @@ use super::app::AppState; use crate::{ core::{errors, payment_methods::cards}, services::{api, authentication as auth}, - types::{ - api::payment_methods::{self, PaymentMethodId}, - storage::enums as storage_enums, - }, + types::api::payment_methods::{self, PaymentMethodId}, }; /// PaymentMethods - Create @@ -404,11 +402,13 @@ impl ParentPaymentMethodToken { Ok(()) } - pub fn should_delete_payment_method_token(&self, status: storage_enums::IntentStatus) -> bool { - !matches!( - status, - diesel_models::enums::IntentStatus::RequiresCustomerAction - ) + pub fn should_delete_payment_method_token(&self, status: IntentStatus) -> bool { + // RequiresMerchantAction: When the payment goes for merchant review incase of potential fraud allow payment_method_token to be stored until resolved + ![ + IntentStatus::RequiresCustomerAction, + IntentStatus::RequiresMerchantAction, + ] + .contains(&status) } pub async fn delete(&self, state: &AppState) -> CustomResult<(), errors::ApiErrorResponse> { diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs index 3668879dee9..04d7f8d6e66 100644 --- a/crates/router/src/types.rs +++ b/crates/router/src/types.rs @@ -46,6 +46,10 @@ pub type PaymentsSyncRouterData = RouterData; pub type PaymentsCancelRouterData = RouterData; +pub type PaymentsRejectRouterData = + RouterData; +pub type PaymentsApproveRouterData = + RouterData; pub type PaymentsSessionRouterData = RouterData; pub type RefundsRouterData = RouterData; @@ -438,6 +442,18 @@ pub struct PaymentsCancelData { pub connector_meta: Option, } +#[derive(Debug, Default, Clone)] +pub struct PaymentsRejectData { + pub amount: Option, + pub currency: Option, +} + +#[derive(Debug, Default, Clone)] +pub struct PaymentsApproveData { + pub amount: Option, + pub currency: Option, +} + #[derive(Debug, Clone)] pub struct PaymentsSessionData { pub amount: i64, @@ -496,6 +512,8 @@ impl Capturable for CompleteAuthorizeData { } impl Capturable for VerifyRequestData {} impl Capturable for PaymentsCancelData {} +impl Capturable for PaymentsApproveData {} +impl Capturable for PaymentsRejectData {} impl Capturable for PaymentsSessionData {} impl Capturable for PaymentsSyncData {} diff --git a/crates/router/src/types/api/payments.rs b/crates/router/src/types/api/payments.rs index 935cf76a130..4cf06638580 100644 --- a/crates/router/src/types/api/payments.rs +++ b/crates/router/src/types/api/payments.rs @@ -4,12 +4,12 @@ pub use api_models::payments::{ MandateValidationFields, NextActionType, OnlineMandate, PayLaterData, PaymentIdType, PaymentListConstraints, PaymentListFilterConstraints, PaymentListFilters, PaymentListResponse, PaymentListResponseV2, PaymentMethodData, PaymentMethodDataResponse, PaymentOp, - PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsCancelRequest, - PaymentsCaptureRequest, PaymentsRedirectRequest, PaymentsRedirectionResponse, PaymentsRequest, - PaymentsResponse, PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, - PaymentsSessionResponse, PaymentsStartRequest, PgRedirectResponse, PhoneDetails, - RedirectionResponse, SessionToken, TimeRange, UrlDetails, VerifyRequest, VerifyResponse, - WalletData, + PaymentRetrieveBody, PaymentRetrieveBodyWithCredentials, PaymentsApproveRequest, + PaymentsCancelRequest, PaymentsCaptureRequest, PaymentsRedirectRequest, + PaymentsRedirectionResponse, PaymentsRejectRequest, PaymentsRequest, PaymentsResponse, + PaymentsResponseForm, PaymentsRetrieveRequest, PaymentsSessionRequest, PaymentsSessionResponse, + PaymentsStartRequest, PgRedirectResponse, PhoneDetails, RedirectionResponse, SessionToken, + TimeRange, UrlDetails, VerifyRequest, VerifyResponse, WalletData, }; use error_stack::{IntoReport, ResultExt}; use masking::PeekInterface; @@ -70,6 +70,9 @@ pub struct AuthorizeSessionToken; #[derive(Debug, Clone)] pub struct CompleteAuthorize; +#[derive(Debug, Clone)] +pub struct Approve; + // Used in gift cards balance check #[derive(Debug, Clone)] pub struct Balance; @@ -85,6 +88,9 @@ pub struct PSync; #[derive(Debug, Clone)] pub struct Void; +#[derive(Debug, Clone)] +pub struct Reject; + #[derive(Debug, Clone)] pub struct Session; @@ -158,6 +164,16 @@ pub trait PaymentVoid: { } +pub trait PaymentApprove: + api::ConnectorIntegration +{ +} + +pub trait PaymentReject: + api::ConnectorIntegration +{ +} + pub trait PaymentCapture: api::ConnectorIntegration { @@ -217,6 +233,8 @@ pub trait Payment: + PaymentSync + PaymentCapture + PaymentVoid + + PaymentApprove + + PaymentReject + PreVerify + PaymentSession + PaymentToken diff --git a/crates/router_derive/src/macros/operation.rs b/crates/router_derive/src/macros/operation.rs index 5a5cd426aa9..c6504f1d9c4 100644 --- a/crates/router_derive/src/macros/operation.rs +++ b/crates/router_derive/src/macros/operation.rs @@ -10,13 +10,16 @@ use crate::macros::helpers; enum Derives { Sync, Cancel, + Reject, Capture, + Approvedata, Authorize, Authorizedata, Syncdata, Canceldata, Capturedata, CompleteAuthorizeData, + Rejectdata, VerifyData, Start, Verify, @@ -29,13 +32,16 @@ impl From for Derives { match s.as_str() { "sync" => Self::Sync, "cancel" => Self::Cancel, + "reject" => Self::Reject, "syncdata" => Self::Syncdata, "authorize" => Self::Authorize, + "approvedata" => Self::Approvedata, "authorizedata" => Self::Authorizedata, "canceldata" => Self::Canceldata, "capture" => Self::Capture, "capturedata" => Self::Capturedata, "completeauthorizedata" => Self::CompleteAuthorizeData, + "rejectdata" => Self::Rejectdata, "start" => Self::Start, "verify" => Self::Verify, "verifydata" => Self::VerifyData, @@ -110,6 +116,9 @@ impl Conversion { Derives::Syncdata => syn::Ident::new("PaymentsSyncData", Span::call_site()), Derives::Cancel => syn::Ident::new("PaymentsCancelRequest", Span::call_site()), Derives::Canceldata => syn::Ident::new("PaymentsCancelData", Span::call_site()), + Derives::Approvedata => syn::Ident::new("PaymentsApproveData", Span::call_site()), + Derives::Reject => syn::Ident::new("PaymentsRejectRequest", Span::call_site()), + Derives::Rejectdata => syn::Ident::new("PaymentsRejectData", Span::call_site()), Derives::Capture => syn::Ident::new("PaymentsCaptureRequest", Span::call_site()), Derives::Capturedata => syn::Ident::new("PaymentsCaptureData", Span::call_site()), Derives::CompleteAuthorizeData => { @@ -331,6 +340,8 @@ pub fn operation_derive_inner(input: DeriveInput) -> syn::Result syn::Result PaymentIntentInterface for KVRouterStore { feature_metadata: new.feature_metadata.clone(), attempt_count: new.attempt_count, profile_id: new.profile_id.clone(), + merchant_decision: new.merchant_decision.clone(), }; match self @@ -696,6 +697,7 @@ impl DataModelExt for PaymentIntentNew { feature_metadata: self.feature_metadata, attempt_count: self.attempt_count, profile_id: self.profile_id, + merchant_decision: self.merchant_decision, } } @@ -731,6 +733,7 @@ impl DataModelExt for PaymentIntentNew { feature_metadata: storage_model.feature_metadata, attempt_count: storage_model.attempt_count, profile_id: storage_model.profile_id, + merchant_decision: storage_model.merchant_decision, } } } @@ -771,6 +774,7 @@ impl DataModelExt for PaymentIntent { feature_metadata: self.feature_metadata, attempt_count: self.attempt_count, profile_id: self.profile_id, + merchant_decision: self.merchant_decision, } } @@ -807,6 +811,7 @@ impl DataModelExt for PaymentIntent { feature_metadata: storage_model.feature_metadata, attempt_count: storage_model.attempt_count, profile_id: storage_model.profile_id, + merchant_decision: storage_model.merchant_decision, } } } @@ -900,6 +905,16 @@ impl DataModelExt for PaymentIntentUpdate { active_attempt_id, attempt_count, }, + Self::ApproveUpdate { merchant_decision } => { + DieselPaymentIntentUpdate::ApproveUpdate { merchant_decision } + } + Self::RejectUpdate { + status, + merchant_decision, + } => DieselPaymentIntentUpdate::RejectUpdate { + status, + merchant_decision, + }, } } @@ -989,6 +1004,16 @@ impl DataModelExt for PaymentIntentUpdate { active_attempt_id, attempt_count, }, + DieselPaymentIntentUpdate::ApproveUpdate { merchant_decision } => { + Self::ApproveUpdate { merchant_decision } + } + DieselPaymentIntentUpdate::RejectUpdate { + status, + merchant_decision, + } => Self::RejectUpdate { + status, + merchant_decision, + }, } } } diff --git a/migrations/2023-08-31-093852_add_merchant_decision/down.sql b/migrations/2023-08-31-093852_add_merchant_decision/down.sql new file mode 100644 index 00000000000..65b3ba867b3 --- /dev/null +++ b/migrations/2023-08-31-093852_add_merchant_decision/down.sql @@ -0,0 +1 @@ +alter table payment_intent drop column merchant_decision; \ No newline at end of file diff --git a/migrations/2023-08-31-093852_add_merchant_decision/up.sql b/migrations/2023-08-31-093852_add_merchant_decision/up.sql new file mode 100644 index 00000000000..0d50d7c7359 --- /dev/null +++ b/migrations/2023-08-31-093852_add_merchant_decision/up.sql @@ -0,0 +1 @@ +alter table payment_intent add column merchant_decision VARCHAR(64); \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 5f8858cd80b..b389c686f65 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -9418,6 +9418,11 @@ "type": "integer", "format": "int32", "description": "total number of attempts associated with this payment" + }, + "merchant_decision": { + "type": "string", + "description": "Denotes the action(approve or reject) taken by merchant in case of manual review. Manual review can occur when the transaction is marked as risky by the frm_processor, payment processor or when there is underpayment/over payment incase of crypto payment", + "nullable": true } } },