diff --git a/crates/api_models/src/errors/actix.rs b/crates/api_models/src/errors/actix.rs index 725bc64843..5c05b6e80d 100644 --- a/crates/api_models/src/errors/actix.rs +++ b/crates/api_models/src/errors/actix.rs @@ -17,6 +17,7 @@ impl actix_web::ResponseError for ApiErrorResponse { Self::MethodNotAllowed(_) => StatusCode::METHOD_NOT_ALLOWED, Self::NotFound(_) => StatusCode::NOT_FOUND, Self::BadRequest(_) => StatusCode::BAD_REQUEST, + Self::DomainError(_) => StatusCode::OK, } } diff --git a/crates/api_models/src/errors/types.rs b/crates/api_models/src/errors/types.rs index 5f303f93c5..538a75e1b2 100644 --- a/crates/api_models/src/errors/types.rs +++ b/crates/api_models/src/errors/types.rs @@ -94,6 +94,7 @@ pub enum ApiErrorResponse { NotFound(ApiError), MethodNotAllowed(ApiError), BadRequest(ApiError), + DomainError(ApiError), } impl ::core::fmt::Display for ApiErrorResponse { @@ -122,6 +123,7 @@ impl ApiErrorResponse { | Self::NotFound(i) | Self::MethodNotAllowed(i) | Self::BadRequest(i) + | Self::DomainError(i) | Self::ConnectorError(i, _) => i, } } @@ -139,6 +141,7 @@ impl ApiErrorResponse { | Self::NotFound(i) | Self::MethodNotAllowed(i) | Self::BadRequest(i) + | Self::DomainError(i) | Self::ConnectorError(i, _) => i, } } @@ -156,6 +159,7 @@ impl ApiErrorResponse { | Self::NotFound(_) | Self::BadRequest(_) => "invalid_request", Self::InternalServerError(_) => "api", + Self::DomainError(_) => "blocked", Self::ConnectorError(_, _) => "connector", } } diff --git a/crates/data_models/src/payments/payment_attempt.rs b/crates/data_models/src/payments/payment_attempt.rs index e6b9950b45..2b5705c155 100644 --- a/crates/data_models/src/payments/payment_attempt.rs +++ b/crates/data_models/src/payments/payment_attempt.rs @@ -308,6 +308,12 @@ pub enum PaymentAttemptUpdate { error_message: Option>, updated_by: String, }, + BlocklistUpdate { + status: storage_enums::AttemptStatus, + error_code: Option>, + error_message: Option>, + updated_by: String, + }, VoidUpdate { status: storage_enums::AttemptStatus, cancellation_reason: Option, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 4a7603384c..d286cc312b 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -218,6 +218,12 @@ pub enum PaymentAttemptUpdate { cancellation_reason: Option, updated_by: String, }, + BlocklistUpdate { + status: storage_enums::AttemptStatus, + error_code: Option>, + error_message: Option>, + updated_by: String, + }, RejectUpdate { status: storage_enums::AttemptStatus, error_code: Option>, @@ -312,7 +318,7 @@ pub struct PaymentAttemptUpdateInternal { status: Option, connector_transaction_id: Option, amount_to_capture: Option, - connector: Option, + connector: Option>, authentication_type: Option, payment_method: Option, error_message: Option>, @@ -338,7 +344,7 @@ pub struct PaymentAttemptUpdateInternal { tax_amount: Option, amount_capturable: Option, updated_by: String, - merchant_connector_id: Option, + merchant_connector_id: Option>, authentication_data: Option, encoded_data: Option, unified_code: Option>, @@ -411,7 +417,7 @@ impl PaymentAttemptUpdate { status: status.unwrap_or(source.status), connector_transaction_id: connector_transaction_id.or(source.connector_transaction_id), amount_to_capture: amount_to_capture.or(source.amount_to_capture), - connector: connector.or(source.connector), + connector: connector.unwrap_or(source.connector), authentication_type: authentication_type.or(source.authentication_type), payment_method: payment_method.or(source.payment_method), error_message: error_message.unwrap_or(source.error_message), @@ -439,7 +445,7 @@ impl PaymentAttemptUpdate { tax_amount: tax_amount.or(source.tax_amount), amount_capturable: amount_capturable.unwrap_or(source.amount_capturable), updated_by, - merchant_connector_id: merchant_connector_id.or(source.merchant_connector_id), + merchant_connector_id: merchant_connector_id.unwrap_or(source.merchant_connector_id), authentication_data: authentication_data.or(source.authentication_data), encoded_data: encoded_data.or(source.encoded_data), unified_code: unified_code.unwrap_or(source.unified_code), @@ -527,7 +533,7 @@ impl From for PaymentAttemptUpdateInternal { payment_method, modified_at: Some(common_utils::date_time::now()), browser_info, - connector, + connector: connector.map(Some), payment_token, payment_method_data, payment_method_type, @@ -538,7 +544,7 @@ impl From for PaymentAttemptUpdateInternal { error_message, amount_capturable, updated_by, - merchant_connector_id, + merchant_connector_id: merchant_connector_id.map(Some), surcharge_amount, tax_amount, ..Default::default() @@ -565,6 +571,20 @@ impl From for PaymentAttemptUpdateInternal { updated_by, ..Default::default() }, + PaymentAttemptUpdate::BlocklistUpdate { + status, + error_code, + error_message, + updated_by, + } => Self { + status: Some(status), + error_code, + connector: Some(None), + error_message, + updated_by, + merchant_connector_id: Some(None), + ..Default::default() + }, PaymentAttemptUpdate::ResponseUpdate { status, connector, @@ -586,7 +606,7 @@ impl From for PaymentAttemptUpdateInternal { unified_message, } => Self { status: Some(status), - connector, + connector: connector.map(Some), connector_transaction_id, authentication_type, payment_method_id, @@ -618,7 +638,7 @@ impl From for PaymentAttemptUpdateInternal { unified_message, connector_transaction_id, } => Self { - connector, + connector: connector.map(Some), status: Some(status), error_message, error_code, @@ -647,13 +667,13 @@ impl From for PaymentAttemptUpdateInternal { merchant_connector_id, } => Self { payment_token, - connector, + connector: connector.map(Some), straight_through_algorithm, amount_capturable, surcharge_amount, tax_amount, updated_by, - merchant_connector_id, + merchant_connector_id: merchant_connector_id.map(Some), ..Default::default() }, PaymentAttemptUpdate::UnresolvedResponseUpdate { @@ -668,7 +688,7 @@ impl From for PaymentAttemptUpdateInternal { updated_by, } => Self { status: Some(status), - connector, + connector: connector.map(Some), connector_transaction_id, payment_method_id, modified_at: Some(common_utils::date_time::now()), @@ -728,7 +748,7 @@ impl From for PaymentAttemptUpdateInternal { authentication_data, encoded_data, connector_transaction_id, - connector, + connector: connector.map(Some), updated_by, ..Default::default() }, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 759e968125..ee9f91fc47 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -207,6 +207,14 @@ pub enum StripeErrorCode { status_code: u16, }, + #[error(error_type = StripeErrorType::CardError, code = "", message = "{code}: {message}")] + PaymentBlockedError { + code: u16, + message: String, + status: String, + reason: String, + }, + #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "The connector provided in the request is incorrect or not available")] IncorrectConnectorNameGiven, #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "No such {object}: '{id}'")] @@ -521,7 +529,17 @@ impl From for StripeErrorCode { connector_name, }, errors::ApiErrorResponse::DuplicatePaymentMethod => Self::DuplicatePaymentMethod, - errors::ApiErrorResponse::PaymentBlocked => Self::PaymentFailed, + errors::ApiErrorResponse::PaymentBlockedError { + code, + message, + status, + reason, + } => Self::PaymentBlockedError { + code, + message, + status, + reason, + }, errors::ApiErrorResponse::ClientSecretInvalid => Self::PaymentIntentInvalidParameter { param: "client_secret".to_owned(), }, @@ -680,6 +698,9 @@ impl actix_web::ResponseError for StripeErrorCode { Self::ExternalConnectorError { status_code, .. } => { StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR) } + Self::PaymentBlockedError { code, .. } => { + StatusCode::from_u16(*code).unwrap_or(StatusCode::OK) + } Self::LockTimeout => StatusCode::LOCKED, } } diff --git a/crates/router/src/core/blocklist/utils.rs b/crates/router/src/core/blocklist/utils.rs index 0e452a90f4..bc0a7335f1 100644 --- a/crates/router/src/core/blocklist/utils.rs +++ b/crates/router/src/core/blocklist/utils.rs @@ -1,5 +1,9 @@ use api_models::blocklist as api_blocklist; -use common_utils::crypto::{self, SignMessage}; +use common_enums::MerchantDecision; +use common_utils::{ + crypto::{self, SignMessage}, + errors::CustomResult, +}; use error_stack::{IntoReport, ResultExt}; #[cfg(feature = "aws_kms")] use external_services::aws_kms; @@ -7,8 +11,12 @@ use external_services::aws_kms; use super::{errors, AppState}; use crate::{ consts, - core::errors::{RouterResult, StorageErrorExt}, - types::{storage, transformers::ForeignInto}, + core::{ + errors::{RouterResult, StorageErrorExt}, + payments::PaymentData, + }, + logger, + types::{domain, storage, transformers::ForeignInto}, utils, }; @@ -261,7 +269,7 @@ pub async fn get_merchant_fingerprint_secret( } } -pub fn get_merchant_fingerprint_secret_key(merchant_id: &str) -> String { +fn get_merchant_fingerprint_secret_key(merchant_id: &str) -> String { format!("fingerprint_secret_{merchant_id}") } @@ -357,3 +365,250 @@ async fn delete_card_bin_blocklist_entry( message: "could not find a blocklist entry for the given bin".to_string(), }) } + +pub async fn validate_data_for_blocklist( + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, +) -> CustomResult +where + F: Send + Clone, +{ + let db = &state.store; + let merchant_id = &merchant_account.merchant_id; + let merchant_fingerprint_secret = + get_merchant_fingerprint_secret(state, merchant_id.as_str()).await?; + + // Hashed Fingerprint to check whether or not this payment should be blocked. + let card_number_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_card_no().as_bytes(), + ) + .attach_printable("error in pm fingerprint creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + // Hashed Cardbin to check whether or not this payment should be blocked. + let card_bin_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_card_isin().as_bytes(), + ) + .attach_printable("error in card bin hash creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + // Hashed Extended Cardbin to check whether or not this payment should be blocked. + let extended_card_bin_fingerprint = payment_data + .payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_extended_card_bin().as_bytes(), + ) + .attach_printable("error in extended card bin hash creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + //validating the payment method. + let mut blocklist_futures = Vec::new(); + if let Some(card_number_fingerprint) = card_number_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + merchant_id, + card_number_fingerprint, + )); + } + + if let Some(card_bin_fingerprint) = card_bin_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + merchant_id, + card_bin_fingerprint, + )); + } + + if let Some(extended_card_bin_fingerprint) = extended_card_bin_fingerprint.as_ref() { + blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( + merchant_id, + extended_card_bin_fingerprint, + )); + } + + let blocklist_lookups = futures::future::join_all(blocklist_futures).await; + + let mut should_payment_be_blocked = false; + for lookup in blocklist_lookups { + match lookup { + Ok(_) => { + should_payment_be_blocked = true; + } + Err(e) => { + logger::error!(blocklist_db_error=?e, "failed db operations for blocklist"); + } + } + } + + if should_payment_be_blocked { + // Update db for attempt and intent status. + db.update_payment_intent( + payment_data.payment_intent.clone(), + storage::PaymentIntentUpdate::RejectUpdate { + status: common_enums::IntentStatus::Failed, + merchant_decision: Some(MerchantDecision::Rejected.to_string()), + updated_by: merchant_account.storage_scheme.to_string(), + }, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable( + "Failed to update status in Payment Intent to failed due to it being blocklisted", + )?; + + // If payment is blocked not showing connector details + let attempt_update = storage::PaymentAttemptUpdate::BlocklistUpdate { + status: common_enums::AttemptStatus::Failure, + error_code: Some(Some("HE-03".to_string())), + error_message: Some(Some("This payment method is blocked".to_string())), + updated_by: merchant_account.storage_scheme.to_string(), + }; + db.update_payment_attempt_with_attempt_id( + payment_data.payment_attempt.clone(), + attempt_update, + merchant_account.storage_scheme, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound) + .attach_printable( + "Failed to update status in Payment Attempt to failed, due to it being blocklisted", + )?; + + Err(errors::ApiErrorResponse::PaymentBlockedError { + code: 200, + message: "This payment method is blocked".to_string(), + status: "Failed".to_string(), + reason: "Blocked".to_string(), + } + .into()) + } else { + payment_data.payment_intent.fingerprint_id = generate_payment_fingerprint( + state, + payment_data.payment_attempt.merchant_id.clone(), + payment_data.payment_method_data.clone(), + ) + .await?; + + Ok(false) + } +} + +pub async fn generate_payment_fingerprint( + state: &AppState, + merchant_id: String, + payment_method_data: Option, +) -> CustomResult, errors::ApiErrorResponse> { + let db = &state.store; + let merchant_fingerprint_secret = get_merchant_fingerprint_secret(state, &merchant_id).await?; + let card_number_fingerprint = payment_method_data + .as_ref() + .and_then(|pm_data| match pm_data { + api_models::payments::PaymentMethodData::Card(card) => { + crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_fingerprint_secret.as_bytes(), + card.card_number.clone().get_card_no().as_bytes(), + ) + .attach_printable("error in pm fingerprint creation") + .map_or_else( + |err| { + logger::error!(error=?err); + None + }, + Some, + ) + } + _ => None, + }) + .map(hex::encode); + + let mut fingerprint_id = None; + if let Some(encoded_hash) = card_number_fingerprint { + #[cfg(feature = "kms")] + let encrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .encrypt(encoded_hash) + .await + .map_or_else( + |e| { + logger::error!(error=?e, "failed kms encryption of card fingerprint"); + None + }, + Some, + ); + + #[cfg(not(feature = "kms"))] + let encrypted_fingerprint = Some(encoded_hash); + + if let Some(encrypted_fingerprint) = encrypted_fingerprint { + fingerprint_id = db + .insert_blocklist_fingerprint_entry( + diesel_models::blocklist_fingerprint::BlocklistFingerprintNew { + merchant_id, + fingerprint_id: utils::generate_id(consts::ID_LENGTH, "fingerprint"), + encrypted_fingerprint, + data_kind: common_enums::BlocklistDataKind::PaymentMethod, + created_at: common_utils::date_time::now(), + }, + ) + .await + .map_or_else( + |e| { + logger::error!(error=?e, "failed storing card fingerprint in db"); + None + }, + |fp| Some(fp.fingerprint_id), + ); + } + } + Ok(fingerprint_id) +} diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index e83483b081..780859d986 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -186,8 +186,13 @@ pub enum ApiErrorResponse { PaymentNotSucceeded, #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "The specified merchant connector account is disabled")] MerchantConnectorAccountDisabled, - #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "The specified payment is blocked")] - PaymentBlocked, + #[error(error_type = ErrorType::ValidationError, code = "HE_03", message = "{code}: {message}")] + PaymentBlockedError { + code: u16, + message: String, + status: String, + reason: String, + }, #[error(error_type= ErrorType::ObjectNotFound, code = "HE_04", message = "Successful payment not found for the given payment id")] SuccessfulPaymentNotFound, #[error(error_type = ErrorType::ObjectNotFound, code = "HE_04", message = "The connector provided in the request is incorrect or not available")] diff --git a/crates/router/src/core/errors/transformers.rs b/crates/router/src/core/errors/transformers.rs index 0119335b7c..d0ca83c84f 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -190,7 +190,11 @@ impl ErrorSwitch for ApiErrorRespon AER::BadRequest(ApiError::new("HE", 3, "Mandate Validation Failed", Some(Extra { reason: Some(reason.clone()), ..Default::default() }))) } Self::PaymentNotSucceeded => AER::BadRequest(ApiError::new("HE", 3, "The payment has not succeeded yet. Please pass a successful payment to initiate refund", None)), - Self::PaymentBlocked => AER::BadRequest(ApiError::new("HE", 3, "The payment is blocked", None)), + Self::PaymentBlockedError { + message, + reason, + .. + } => AER::DomainError(ApiError::new("HE", 3, message, Some(Extra { reason: Some(reason.clone()), ..Default::default() }))), Self::SuccessfulPaymentNotFound => { AER::NotFound(ApiError::new("HE", 4, "Successful payment not found for the given payment id", None)) } diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index fd265c07da..38f2df5706 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -1071,6 +1071,9 @@ where *payment_data = pd; + // Validating the blocklist guard and generate the fingerprint + blocklist_guard(state, merchant_account, operation, payment_data).await?; + let updated_customer = call_create_connector_customer_if_required( state, customer, @@ -1229,6 +1232,45 @@ where router_data_res } +async fn blocklist_guard( + state: &AppState, + merchant_account: &domain::MerchantAccount, + operation: &BoxedOperation<'_, F, ApiRequest, Ctx>, + payment_data: &mut PaymentData, +) -> CustomResult +where + F: Send + Clone + Sync, + Ctx: PaymentMethodRetrieve, +{ + let merchant_id = &payment_data.payment_attempt.merchant_id; + let blocklist_enabled_key = format!("guard_blocklist_for_{merchant_id}"); + let blocklist_guard_enabled = state + .store + .find_config_by_key_unwrap_or(&blocklist_enabled_key, Some("false".to_string())) + .await; + + let blocklist_guard_enabled: bool = match blocklist_guard_enabled { + Ok(config) => serde_json::from_str(&config.config).unwrap_or(false), + + // If it is not present in db we are defaulting it to false + Err(inner) => { + if !inner.current_context().is_db_not_found() { + logger::error!("Error fetching guard blocklist enabled config {:?}", inner); + } + false + } + }; + + if blocklist_guard_enabled { + Ok(operation + .to_domain()? + .guard_payment_against_blocklist(state, merchant_account, payment_data) + .await?) + } else { + Ok(false) + } +} + #[allow(clippy::too_many_arguments)] pub async fn call_multiple_connectors_service( state: &AppState, diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 89d3131dde..379725a04c 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -165,6 +165,16 @@ pub trait Domain: Send + Sync { ) -> CustomResult<(), errors::ApiErrorResponse> { Ok(()) } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] @@ -267,6 +277,16 @@ where ) .await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] @@ -329,6 +349,16 @@ where ) -> CustomResult { helpers::get_connector_default(state, None).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] @@ -392,6 +422,16 @@ where ) -> CustomResult { helpers::get_connector_default(state, None).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] @@ -445,4 +485,14 @@ where ) -> CustomResult { helpers::get_connector_default(state, None).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } 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 b4f538e1d0..2a0aa79338 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -351,6 +351,16 @@ impl Domain( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 486a60f7bc..54ec47ecea 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -2,21 +2,15 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use common_utils::{ - crypto::{self, SignMessage}, - ext_traits::{AsyncExt, Encode}, -}; +use common_utils::ext_traits::{AsyncExt, Encode}; use error_stack::{report, IntoReport, ResultExt}; -#[cfg(feature = "aws_kms")] -use external_services::aws_kms; use futures::FutureExt; use router_derive::PaymentOperation; -use router_env::{instrument, logger, tracing}; +use router_env::{instrument, tracing}; use tracing_futures::Instrument; use super::{BoxedOperation, Domain, GetTracker, Operation, UpdateTracker, ValidateRequest}; use crate::{ - consts, core::{ blocklist::utils as blocklist_utils, errors::{self, CustomResult, RouterResult, StorageErrorExt}, @@ -641,6 +635,16 @@ impl Domain CustomResult<(), errors::ApiErrorResponse> { populate_surcharge_details(state, payment_data).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + state: &AppState, + merchant_account: &domain::MerchantAccount, + payment_data: &mut PaymentData, + ) -> CustomResult { + blocklist_utils::validate_data_for_blocklist(state, merchant_account, payment_data).await + } } #[async_trait] @@ -665,34 +669,32 @@ impl where F: 'b + Send, { - let db = state.store.as_ref(); let payment_method = payment_data.payment_attempt.payment_method; let browser_info = payment_data.payment_attempt.browser_info.clone(); let frm_message = payment_data.frm_message.clone(); - let (mut intent_status, mut 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), - ), - }; + 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), + ), + }; let connector = payment_data.payment_attempt.connector.clone(); let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); @@ -755,158 +757,6 @@ impl let m_error_code = error_code.clone(); let m_error_message = error_message.clone(); let m_db = state.clone().store; - - // Validate Blocklist - let merchant_id = payment_data.payment_attempt.merchant_id; - let merchant_fingerprint_secret = - blocklist_utils::get_merchant_fingerprint_secret(state, &merchant_id).await?; - - // Hashed Fingerprint to check whether or not this payment should be blocked. - let card_number_fingerprint = payment_data - .payment_method_data - .as_ref() - .and_then(|pm_data| match pm_data { - api_models::payments::PaymentMethodData::Card(card) => { - crypto::HmacSha512::sign_message( - &crypto::HmacSha512, - merchant_fingerprint_secret.as_bytes(), - card.card_number.clone().get_card_no().as_bytes(), - ) - .attach_printable("error in pm fingerprint creation") - .map_or_else( - |err| { - logger::error!(error=?err); - None - }, - Some, - ) - } - _ => None, - }) - .map(hex::encode); - - // Hashed Cardbin to check whether or not this payment should be blocked. - let card_bin_fingerprint = payment_data - .payment_method_data - .as_ref() - .and_then(|pm_data| match pm_data { - api_models::payments::PaymentMethodData::Card(card) => { - crypto::HmacSha512::sign_message( - &crypto::HmacSha512, - merchant_fingerprint_secret.as_bytes(), - card.card_number.clone().get_card_isin().as_bytes(), - ) - .attach_printable("error in card bin hash creation") - .map_or_else( - |err| { - logger::error!(error=?err); - None - }, - Some, - ) - } - _ => None, - }) - .map(hex::encode); - - // Hashed Extended Cardbin to check whether or not this payment should be blocked. - let extended_card_bin_fingerprint = payment_data - .payment_method_data - .as_ref() - .and_then(|pm_data| match pm_data { - api_models::payments::PaymentMethodData::Card(card) => { - crypto::HmacSha512::sign_message( - &crypto::HmacSha512, - merchant_fingerprint_secret.as_bytes(), - card.card_number.clone().get_extended_card_bin().as_bytes(), - ) - .attach_printable("error in extended card bin hash creation") - .map_or_else( - |err| { - logger::error!(error=?err); - None - }, - Some, - ) - } - _ => None, - }) - .map(hex::encode); - - let mut fingerprint_id = None; - - //validating the payment method. - let mut is_pm_blocklisted = false; - - let mut blocklist_futures = Vec::new(); - if let Some(card_number_fingerprint) = card_number_fingerprint.as_ref() { - blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( - &merchant_id, - card_number_fingerprint, - )); - } - - if let Some(card_bin_fingerprint) = card_bin_fingerprint.as_ref() { - blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( - &merchant_id, - card_bin_fingerprint, - )); - } - - if let Some(extended_card_bin_fingerprint) = extended_card_bin_fingerprint.as_ref() { - blocklist_futures.push(db.find_blocklist_lookup_entry_by_merchant_id_fingerprint( - &merchant_id, - extended_card_bin_fingerprint, - )); - } - - let blocklist_lookups = futures::future::join_all(blocklist_futures).await; - - if blocklist_lookups.iter().any(|x| x.is_ok()) { - intent_status = storage_enums::IntentStatus::Failed; - attempt_status = storage_enums::AttemptStatus::Failure; - is_pm_blocklisted = true; - } - - if let Some(encoded_hash) = card_number_fingerprint { - #[cfg(feature = "aws_kms")] - let encrypted_fingerprint = aws_kms::get_aws_kms_client(&state.conf.kms) - .await - .encrypt(encoded_hash) - .await - .map_or_else( - |e| { - logger::error!(error=?e, "failed kms encryption of card fingerprint"); - None - }, - Some, - ); - - #[cfg(not(feature = "aws_kms"))] - let encrypted_fingerprint = Some(encoded_hash); - - if let Some(encrypted_fingerprint) = encrypted_fingerprint { - fingerprint_id = db - .insert_blocklist_fingerprint_entry( - diesel_models::blocklist_fingerprint::BlocklistFingerprintNew { - merchant_id, - fingerprint_id: utils::generate_id(consts::ID_LENGTH, "fingerprint"), - encrypted_fingerprint, - data_kind: common_enums::BlocklistDataKind::PaymentMethod, - created_at: common_utils::date_time::now(), - }, - ) - .await - .map_or_else( - |e| { - logger::error!(error=?e, "failed storing card fingerprint in db"); - None - }, - |fp| Some(fp.fingerprint_id), - ); - } - } - let surcharge_amount = payment_data .surcharge_details .as_ref() @@ -951,6 +801,7 @@ impl ); let m_payment_data_payment_intent = payment_data.payment_intent.clone(); + let m_fingerprint_id = payment_data.payment_intent.fingerprint_id.clone(); let m_customer_id = customer_id.clone(); let m_shipping_address_id = shipping_address.clone(); let m_billing_address_id = billing_address.clone(); @@ -987,7 +838,7 @@ impl metadata: m_metadata, payment_confirm_source: header_payload.payment_confirm_source, updated_by: m_storage_scheme, - fingerprint_id, + fingerprint_id: m_fingerprint_id, session_expiry, }, storage_scheme, @@ -1037,11 +888,6 @@ impl payment_data.payment_intent = payment_intent; payment_data.payment_attempt = payment_attempt; - // Block the payment if the entry was present in the Blocklist - if is_pm_blocklisted { - return Err(errors::ApiErrorResponse::PaymentBlocked.into()); - } - Ok((Box::new(self), payment_data)) } } diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 378cbb2546..97ff3a6c9c 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -466,6 +466,16 @@ impl Domain CustomResult { helpers::get_connector_default(state, request.routing.clone()).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index 170db39388..7db174313d 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -446,6 +446,16 @@ where session_connector_data, )) } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> errors::CustomResult { + Ok(false) + } } impl From for api::GetToken { diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index 8b4ec4e3f5..79130b211a 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -314,4 +314,14 @@ where ) -> CustomResult { helpers::get_connector_default(state, None).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 9db9742ca9..81dfb7127d 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -132,6 +132,16 @@ impl Domain CustomResult { helpers::get_connector_default(state, request.routing.clone()).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index 664fce820a..f52dee627c 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -463,6 +463,16 @@ impl Domain CustomResult { helpers::get_connector_default(state, request.routing.clone()).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut PaymentData, + ) -> CustomResult { + Ok(false) + } } #[async_trait] diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index e7bb5622b7..36095a1b7e 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -328,4 +328,14 @@ impl ) -> CustomResult { helpers::get_connector_default(state, None).await } + + #[instrument(skip_all)] + async fn guard_payment_against_blocklist<'a>( + &'a self, + _state: &AppState, + _merchant_account: &domain::MerchantAccount, + _payment_data: &mut payments::PaymentData, + ) -> CustomResult { + Ok(false) + } } diff --git a/crates/router/src/db/blocklist_lookup.rs b/crates/router/src/db/blocklist_lookup.rs index f5fb4ea9ed..5060fac7f6 100644 --- a/crates/router/src/db/blocklist_lookup.rs +++ b/crates/router/src/db/blocklist_lookup.rs @@ -50,7 +50,7 @@ impl BlocklistLookupInterface for Store { merchant_id: &str, fingerprint: &str, ) -> CustomResult { - let conn = connection::pg_connection_write(self).await?; + let conn = connection::pg_connection_read(self).await?; storage::BlocklistLookup::find_by_merchant_id_fingerprint(&conn, merchant_id, fingerprint) .await .map_err(Into::into) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index ad52a8aaea..6af136c106 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -1366,6 +1366,17 @@ impl DataModelExt for PaymentAttemptUpdate { authentication_type, updated_by, }, + Self::BlocklistUpdate { + status, + error_code, + error_message, + updated_by, + } => DieselPaymentAttemptUpdate::BlocklistUpdate { + status, + error_code, + error_message, + updated_by, + }, Self::ConfirmUpdate { amount, currency, @@ -1686,6 +1697,17 @@ impl DataModelExt for PaymentAttemptUpdate { cancellation_reason, updated_by, }, + DieselPaymentAttemptUpdate::BlocklistUpdate { + status, + error_code, + error_message, + updated_by, + } => Self::BlocklistUpdate { + status, + error_code, + error_message, + updated_by, + }, DieselPaymentAttemptUpdate::ResponseUpdate { status, connector,