refactor(blocklist): separate utility function & kill switch for validating data in blocklist (#3360)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Prajjwal Kumar
2024-02-06 19:06:47 +05:30
committed by GitHub
parent ebe4ac30a8
commit 0a97a1eb63
20 changed files with 557 additions and 211 deletions

View File

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

View File

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

View File

@ -308,6 +308,12 @@ pub enum PaymentAttemptUpdate {
error_message: Option<Option<String>>,
updated_by: String,
},
BlocklistUpdate {
status: storage_enums::AttemptStatus,
error_code: Option<Option<String>>,
error_message: Option<Option<String>>,
updated_by: String,
},
VoidUpdate {
status: storage_enums::AttemptStatus,
cancellation_reason: Option<String>,

View File

@ -218,6 +218,12 @@ pub enum PaymentAttemptUpdate {
cancellation_reason: Option<String>,
updated_by: String,
},
BlocklistUpdate {
status: storage_enums::AttemptStatus,
error_code: Option<Option<String>>,
error_message: Option<Option<String>>,
updated_by: String,
},
RejectUpdate {
status: storage_enums::AttemptStatus,
error_code: Option<Option<String>>,
@ -312,7 +318,7 @@ pub struct PaymentAttemptUpdateInternal {
status: Option<storage_enums::AttemptStatus>,
connector_transaction_id: Option<String>,
amount_to_capture: Option<i64>,
connector: Option<String>,
connector: Option<Option<String>>,
authentication_type: Option<storage_enums::AuthenticationType>,
payment_method: Option<storage_enums::PaymentMethod>,
error_message: Option<Option<String>>,
@ -338,7 +344,7 @@ pub struct PaymentAttemptUpdateInternal {
tax_amount: Option<i64>,
amount_capturable: Option<i64>,
updated_by: String,
merchant_connector_id: Option<String>,
merchant_connector_id: Option<Option<String>>,
authentication_data: Option<serde_json::Value>,
encoded_data: Option<String>,
unified_code: Option<Option<String>>,
@ -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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> 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<PaymentAttemptUpdate> for PaymentAttemptUpdateInternal {
authentication_data,
encoded_data,
connector_transaction_id,
connector,
connector: connector.map(Some),
updated_by,
..Default::default()
},

View File

@ -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<errors::ApiErrorResponse> 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,
}
}

View File

@ -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<F>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
payment_data: &mut PaymentData<F>,
) -> CustomResult<bool, errors::ApiErrorResponse>
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<crate::types::api::PaymentMethodData>,
) -> CustomResult<Option<String>, 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)
}

View File

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

View File

@ -190,7 +190,11 @@ impl ErrorSwitch<api_models::errors::types::ApiErrorResponse> 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))
}

View File

@ -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<F, ApiRequest, Ctx>(
state: &AppState,
merchant_account: &domain::MerchantAccount,
operation: &BoxedOperation<'_, F, ApiRequest, Ctx>,
payment_data: &mut PaymentData<F>,
) -> CustomResult<bool, errors::ApiErrorResponse>
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<F, Op, Req, Ctx>(
state: &AppState,

View File

@ -165,6 +165,16 @@ pub trait Domain<F: Clone, R, Ctx: PaymentMethodRetrieve>: 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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
#[async_trait]
@ -329,6 +349,16 @@ where
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
#[async_trait]
@ -392,6 +422,16 @@ where
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
#[async_trait]
@ -445,4 +485,14 @@ where
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}

View File

@ -351,6 +351,16 @@ impl<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
// creating the payment or if none is passed then use the routing algorithm
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
#[async_trait]

View File

@ -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<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
) -> 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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
blocklist_utils::validate_data_for_blocklist(state, merchant_account, payment_data).await
}
}
#[async_trait]
@ -665,13 +669,11 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
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 {
let (intent_status, attempt_status, (error_code, error_message)) = match frm_suggestion {
Some(FrmSuggestion::FrmCancelTransaction) => (
storage_enums::IntentStatus::Failed,
storage_enums::AttemptStatus::Failure,
@ -755,158 +757,6 @@ impl<F: Clone, Ctx: PaymentMethodRetrieve>
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<F: Clone, Ctx: PaymentMethodRetrieve>
);
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<F: Clone, Ctx: PaymentMethodRetrieve>
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<F: Clone, Ctx: PaymentMethodRetrieve>
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))
}
}

View File

@ -466,6 +466,16 @@ impl<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
#[async_trait]

View File

@ -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<F>,
) -> errors::CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
impl From<api_models::enums::PaymentMethodType> for api::GetToken {

View File

@ -314,4 +314,14 @@ where
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}

View File

@ -132,6 +132,16 @@ impl<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
#[async_trait]

View File

@ -463,6 +463,16 @@ impl<F: Clone + Send, Ctx: PaymentMethodRetrieve> Domain<F, api::PaymentsRequest
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}
#[async_trait]

View File

@ -328,4 +328,14 @@ impl<F: Clone + Send, Ctx: PaymentMethodRetrieve>
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
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<F>,
) -> CustomResult<bool, errors::ApiErrorResponse> {
Ok(false)
}
}

View File

@ -50,7 +50,7 @@ impl BlocklistLookupInterface for Store {
merchant_id: &str,
fingerprint: &str,
) -> CustomResult<storage::BlocklistLookup, errors::StorageError> {
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)

View File

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