mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 19:46:48 +08:00
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:
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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",
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>,
|
||||
|
||||
@ -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()
|
||||
},
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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")]
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user