diff --git a/crates/api_models/src/blocklist.rs b/crates/api_models/src/blocklist.rs new file mode 100644 index 0000000000..fc838eed5c --- /dev/null +++ b/crates/api_models/src/blocklist.rs @@ -0,0 +1,41 @@ +use common_enums::enums; +use common_utils::events::ApiEventMetric; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "snake_case", tag = "type", content = "data")] +pub enum BlocklistRequest { + CardBin(String), + Fingerprint(String), + ExtendedCardBin(String), +} + +pub type AddToBlocklistRequest = BlocklistRequest; +pub type DeleteFromBlocklistRequest = BlocklistRequest; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct BlocklistResponse { + pub fingerprint_id: String, + pub data_kind: enums::BlocklistDataKind, + #[serde(with = "common_utils::custom_serde::iso8601")] + pub created_at: time::PrimitiveDateTime, +} + +pub type AddToBlocklistResponse = BlocklistResponse; +pub type DeleteFromBlocklistResponse = BlocklistResponse; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ListBlocklistQuery { + pub data_kind: enums::BlocklistDataKind, + #[serde(default = "default_list_limit")] + pub limit: u16, + #[serde(default)] + pub offset: u16, +} + +fn default_list_limit() -> u16 { + 10 +} + +impl ApiEventMetric for BlocklistRequest {} +impl ApiEventMetric for BlocklistResponse {} +impl ApiEventMetric for ListBlocklistQuery {} diff --git a/crates/api_models/src/lib.rs b/crates/api_models/src/lib.rs index 459443747e..dc1f6eb653 100644 --- a/crates/api_models/src/lib.rs +++ b/crates/api_models/src/lib.rs @@ -3,6 +3,7 @@ pub mod admin; pub mod analytics; pub mod api_keys; pub mod bank_accounts; +pub mod blocklist; pub mod cards_info; pub mod conditional_configs; pub mod connector_onboarding; diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 45611a9145..f9077500dd 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -2274,6 +2274,9 @@ pub struct PaymentsResponse { /// List of incremental authorizations happened to the payment pub incremental_authorizations: Option>, + + /// Payment Fingerprint + pub fingerprint: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema, serde::Serialize)] diff --git a/crates/cards/src/validate.rs b/crates/cards/src/validate.rs index ca47c73c7c..87b04baa1a 100644 --- a/crates/cards/src/validate.rs +++ b/crates/cards/src/validate.rs @@ -24,6 +24,13 @@ impl CardNumber { pub fn get_card_isin(self) -> String { self.0.peek().chars().take(6).collect::() } + + pub fn get_extended_card_bin(self) -> String { + self.0.peek().chars().take(8).collect::() + } + pub fn get_card_no(self) -> String { + self.0.peek().chars().collect::() + } pub fn get_last4(self) -> String { self.0 .peek() diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 3af1c0e826..949cc2e003 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -6,12 +6,13 @@ use utoipa::ToSchema; pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, - DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventType as EventType, - DbFutureUsage as FutureUsage, DbIntentStatus as IntentStatus, - DbMandateStatus as MandateStatus, DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, - DbPaymentType as PaymentType, DbRefundStatus as RefundStatus, + DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, + DbCaptureStatus as CaptureStatus, DbConnectorType as ConnectorType, + DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDisputeStage as DisputeStage, + DbDisputeStatus as DisputeStatus, DbEventType as EventType, DbFutureUsage as FutureUsage, + DbIntentStatus as IntentStatus, DbMandateStatus as MandateStatus, + DbPaymentMethodIssuerCode as PaymentMethodIssuerCode, DbPaymentType as PaymentType, + DbRefundStatus as RefundStatus, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, }; } @@ -275,6 +276,27 @@ pub enum AuthorizationStatus { Unresolved, } +#[derive( + Clone, + Debug, + PartialEq, + Eq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, + Hash, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BlocklistDataKind { + PaymentMethod, + CardBin, + ExtendedCardBin, +} + #[derive( Clone, Copy, diff --git a/crates/data_models/src/errors.rs b/crates/data_models/src/errors.rs index 9616a3a944..bed1ab9ccb 100644 --- a/crates/data_models/src/errors.rs +++ b/crates/data_models/src/errors.rs @@ -24,6 +24,8 @@ pub enum StorageError { SerializationFailed, #[error("MockDb error")] MockDbError, + #[error("Kafka error")] + KafkaError, #[error("Customer with this id is Redacted")] CustomerRedacted, #[error("Deserialization failure")] diff --git a/crates/data_models/src/payments.rs b/crates/data_models/src/payments.rs index cc6b03f89a..713003d666 100644 --- a/crates/data_models/src/payments.rs +++ b/crates/data_models/src/payments.rs @@ -53,5 +53,6 @@ pub struct PaymentIntent { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index 80671ec7f6..7470b5f850 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -110,6 +110,7 @@ pub struct PaymentIntentNew { pub request_incremental_authorization: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } @@ -163,6 +164,7 @@ pub enum PaymentIntentUpdate { metadata: Option, payment_confirm_source: Option, updated_by: String, + fingerprint_id: Option, session_expiry: Option, }, PaymentAttemptAndAttemptCountUpdate { @@ -228,6 +230,7 @@ pub struct PaymentIntentUpdateInternal { pub surcharge_applicable: Option, pub incremental_authorization_allowed: Option, pub authorization_count: Option, + pub fingerprint_id: Option, pub session_expiry: Option, } @@ -252,6 +255,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, } => Self { amount: Some(amount), @@ -272,6 +276,7 @@ impl From for PaymentIntentUpdateInternal { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, ..Default::default() }, diff --git a/crates/diesel_models/src/blocklist.rs b/crates/diesel_models/src/blocklist.rs new file mode 100644 index 0000000000..9e88802aa3 --- /dev/null +++ b/crates/diesel_models/src/blocklist.rs @@ -0,0 +1,26 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist; + +#[derive(Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist)] +pub struct BlocklistNew { + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub metadata: Option, + pub created_at: time::PrimitiveDateTime, +} + +#[derive(Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Deserialize, Serialize)] +#[diesel(table_name = blocklist)] +pub struct Blocklist { + #[serde(skip)] + pub id: i32, + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub metadata: Option, + pub created_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/blocklist_fingerprint.rs b/crates/diesel_models/src/blocklist_fingerprint.rs new file mode 100644 index 0000000000..e75856622e --- /dev/null +++ b/crates/diesel_models/src/blocklist_fingerprint.rs @@ -0,0 +1,26 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist_fingerprint; + +#[derive(Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist_fingerprint)] +pub struct BlocklistFingerprintNew { + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub encrypted_fingerprint: String, + pub created_at: time::PrimitiveDateTime, +} + +#[derive(Clone, Debug, Eq, PartialEq, Queryable, Identifiable, Deserialize, Serialize)] +#[diesel(table_name = blocklist_fingerprint)] +pub struct BlocklistFingerprint { + #[serde(skip_serializing)] + pub id: i32, + pub merchant_id: String, + pub fingerprint_id: String, + pub data_kind: common_enums::BlocklistDataKind, + pub encrypted_fingerprint: String, + pub created_at: time::PrimitiveDateTime, +} diff --git a/crates/diesel_models/src/blocklist_lookup.rs b/crates/diesel_models/src/blocklist_lookup.rs new file mode 100644 index 0000000000..ad2a893e03 --- /dev/null +++ b/crates/diesel_models/src/blocklist_lookup.rs @@ -0,0 +1,20 @@ +use diesel::{Identifiable, Insertable, Queryable}; +use serde::{Deserialize, Serialize}; + +use crate::schema::blocklist_lookup; + +#[derive(Default, Clone, Debug, Eq, Insertable, PartialEq, Serialize, Deserialize)] +#[diesel(table_name = blocklist_lookup)] +pub struct BlocklistLookupNew { + pub merchant_id: String, + pub fingerprint: String, +} + +#[derive(Default, Clone, Debug, Eq, PartialEq, Identifiable, Queryable, Deserialize, Serialize)] +#[diesel(table_name = blocklist_lookup)] +pub struct BlocklistLookup { + #[serde(skip)] + pub id: i32, + pub merchant_id: String, + pub fingerprint: String, +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 792e8ffc8b..a06937c99a 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -2,9 +2,9 @@ pub mod diesel_exports { pub use super::{ DbAttemptStatus as AttemptStatus, DbAuthenticationType as AuthenticationType, - DbCaptureMethod as CaptureMethod, DbCaptureStatus as CaptureStatus, - DbConnectorStatus as ConnectorStatus, DbConnectorType as ConnectorType, - DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, + DbBlocklistDataKind as BlocklistDataKind, DbCaptureMethod as CaptureMethod, + DbCaptureStatus as CaptureStatus, DbConnectorStatus as ConnectorStatus, + DbConnectorType as ConnectorType, DbCountryAlpha2 as CountryAlpha2, DbCurrency as Currency, DbDashboardMetadata as DashboardMetadata, DbDisputeStage as DisputeStage, DbDisputeStatus as DisputeStatus, DbEventClass as EventClass, DbEventObjectType as EventObjectType, DbEventType as EventType, diff --git a/crates/diesel_models/src/lib.rs b/crates/diesel_models/src/lib.rs index fa32fb84a1..82b1e29ee8 100644 --- a/crates/diesel_models/src/lib.rs +++ b/crates/diesel_models/src/lib.rs @@ -1,11 +1,14 @@ pub mod address; pub mod api_keys; +pub mod blocklist_lookup; pub mod business_profile; pub mod capture; pub mod cards_info; pub mod configs; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; pub mod customers; pub mod dispute; pub mod encryption; diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 17784bc565..31bc0c06c5 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -56,6 +56,7 @@ pub struct PaymentIntent { pub incremental_authorization_allowed: Option, pub authorization_count: Option, pub session_expiry: Option, + pub fingerprint_id: Option, } #[derive( @@ -107,6 +108,7 @@ pub struct PaymentIntentNew { pub authorization_count: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] pub session_expiry: Option, + pub fingerprint_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -160,6 +162,7 @@ pub enum PaymentIntentUpdate { payment_confirm_source: Option, updated_by: String, session_expiry: Option, + fingerprint_id: Option, }, PaymentAttemptAndAttemptCountUpdate { active_attempt_id: String, @@ -226,6 +229,7 @@ pub struct PaymentIntentUpdateInternal { pub incremental_authorization_allowed: Option, pub authorization_count: Option, pub session_expiry: Option, + pub fingerprint_id: Option, } impl PaymentIntentUpdate { @@ -259,6 +263,7 @@ impl PaymentIntentUpdate { incremental_authorization_allowed, authorization_count, session_expiry, + fingerprint_id, } = self.into(); PaymentIntent { amount: amount.unwrap_or(source.amount), @@ -288,9 +293,11 @@ impl PaymentIntentUpdate { payment_confirm_source: payment_confirm_source.or(source.payment_confirm_source), updated_by, surcharge_applicable: surcharge_applicable.or(source.surcharge_applicable), + incremental_authorization_allowed: incremental_authorization_allowed .or(source.incremental_authorization_allowed), authorization_count: authorization_count.or(source.authorization_count), + fingerprint_id: fingerprint_id.or(source.fingerprint_id), session_expiry: session_expiry.or(source.session_expiry), ..source } @@ -319,6 +326,7 @@ impl From for PaymentIntentUpdateInternal { payment_confirm_source, updated_by, session_expiry, + fingerprint_id, } => Self { amount: Some(amount), currency: Some(currency), @@ -339,6 +347,7 @@ impl From for PaymentIntentUpdateInternal { payment_confirm_source, updated_by, session_expiry, + fingerprint_id, ..Default::default() }, PaymentIntentUpdate::MetadataUpdate { diff --git a/crates/diesel_models/src/query.rs b/crates/diesel_models/src/query.rs index 3a3dee47a8..3a0a008b76 100644 --- a/crates/diesel_models/src/query.rs +++ b/crates/diesel_models/src/query.rs @@ -1,11 +1,14 @@ pub mod address; pub mod api_keys; +pub mod blocklist_lookup; pub mod business_profile; mod capture; pub mod cards_info; pub mod configs; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; pub mod customers; pub mod dashboard_metadata; pub mod dispute; diff --git a/crates/diesel_models/src/query/blocklist.rs b/crates/diesel_models/src/query/blocklist.rs new file mode 100644 index 0000000000..e1ba5fa923 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist.rs @@ -0,0 +1,83 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist::{Blocklist, BlocklistNew}, + schema::blocklist::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl Blocklist { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn list_by_merchant_id_data_kind( + conn: &PgPooledConn, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::data_kind.eq(data_kind.to_owned())), + Some(limit), + Some(offset), + Some(dsl::created_at.desc()), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn list_by_merchant_id( + conn: &PgPooledConn, + merchant_id: &str, + ) -> StorageResult> { + generics::generic_filter::<::Table, _, _, _>( + conn, + dsl::merchant_id.eq(merchant_id.to_owned()), + None, + None, + Some(dsl::created_at.desc()), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn delete_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_delete_one_with_result::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/blocklist_fingerprint.rs b/crates/diesel_models/src/query/blocklist_fingerprint.rs new file mode 100644 index 0000000000..4f3d77e63a --- /dev/null +++ b/crates/diesel_models/src/query/blocklist_fingerprint.rs @@ -0,0 +1,33 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist_fingerprint::{BlocklistFingerprint, BlocklistFingerprintNew}, + schema::blocklist_fingerprint::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistFingerprintNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl BlocklistFingerprint { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint_id( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint_id: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint_id.eq(fingerprint_id.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/query/blocklist_lookup.rs b/crates/diesel_models/src/query/blocklist_lookup.rs new file mode 100644 index 0000000000..ea28c94e49 --- /dev/null +++ b/crates/diesel_models/src/query/blocklist_lookup.rs @@ -0,0 +1,48 @@ +use diesel::{associations::HasTable, BoolExpressionMethods, ExpressionMethods}; +use router_env::{instrument, tracing}; + +use super::generics; +use crate::{ + blocklist_lookup::{BlocklistLookup, BlocklistLookupNew}, + schema::blocklist_lookup::dsl, + PgPooledConn, StorageResult, +}; + +impl BlocklistLookupNew { + #[instrument(skip(conn))] + pub async fn insert(self, conn: &PgPooledConn) -> StorageResult { + generics::generic_insert(conn, self).await + } +} + +impl BlocklistLookup { + #[instrument(skip(conn))] + pub async fn find_by_merchant_id_fingerprint( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint: &str, + ) -> StorageResult { + generics::generic_find_one::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint.eq(fingerprint.to_owned())), + ) + .await + } + + #[instrument(skip(conn))] + pub async fn delete_by_merchant_id_fingerprint( + conn: &PgPooledConn, + merchant_id: &str, + fingerprint: &str, + ) -> StorageResult { + generics::generic_delete_one_with_result::<::Table, _, _>( + conn, + dsl::merchant_id + .eq(merchant_id.to_owned()) + .and(dsl::fingerprint.eq(fingerprint.to_owned())), + ) + .await + } +} diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index b29a362e3b..131d2b1826 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -57,6 +57,50 @@ diesel::table! { } } +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + fingerprint_id -> Varchar, + data_kind -> BlocklistDataKind, + metadata -> Nullable, + created_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist_fingerprint (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + #[max_length = 64] + fingerprint_id -> Varchar, + data_kind -> BlocklistDataKind, + encrypted_fingerprint -> Text, + created_at -> Timestamp, + } +} + +diesel::table! { + use diesel::sql_types::*; + use crate::enums::diesel_exports::*; + + blocklist_lookup (id) { + id -> Int4, + #[max_length = 64] + merchant_id -> Varchar, + fingerprint -> Text, + } +} + diesel::table! { use diesel::sql_types::*; use crate::enums::diesel_exports::*; @@ -709,6 +753,8 @@ diesel::table! { incremental_authorization_allowed -> Nullable, authorization_count -> Nullable, session_expiry -> Nullable, + #[max_length = 64] + fingerprint_id -> Nullable, } } @@ -1016,6 +1062,9 @@ diesel::table! { diesel::allow_tables_to_appear_in_same_query!( address, api_keys, + blocklist, + blocklist_fingerprint, + blocklist_lookup, business_profile, captures, cards_info, diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 5963110c63..63205ea68c 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -520,6 +520,7 @@ impl From for StripeErrorCode { connector_name, }, errors::ApiErrorResponse::DuplicatePaymentMethod => Self::DuplicatePaymentMethod, + errors::ApiErrorResponse::PaymentBlocked => Self::PaymentFailed, errors::ApiErrorResponse::ClientSecretInvalid => Self::PaymentIntentInvalidParameter { param: "client_secret".to_owned(), }, diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs index afe7618463..ed020b0c7e 100644 --- a/crates/router/src/consts.rs +++ b/crates/router/src/consts.rs @@ -27,6 +27,9 @@ pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; /// Payment intent default client secret expiry (in seconds) pub const DEFAULT_SESSION_EXPIRY: i64 = 15 * 60; +/// The length of a merchant fingerprint secret +pub const FINGERPRINT_SECRET_LENGTH: usize = 64; + // String literals pub(crate) const NO_ERROR_MESSAGE: &str = "No error message"; pub(crate) const NO_ERROR_CODE: &str = "No error code"; diff --git a/crates/router/src/core.rs b/crates/router/src/core.rs index 0bd197ee22..5ae4b0be33 100644 --- a/crates/router/src/core.rs +++ b/crates/router/src/core.rs @@ -1,6 +1,7 @@ pub mod admin; pub mod api_keys; pub mod api_locking; +pub mod blocklist; pub mod cache; pub mod cards_info; pub mod conditional_config; diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 2577bb83a3..e859358112 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -10,6 +10,7 @@ use common_utils::{ ext_traits::{AsyncExt, ConfigExt, Encode, ValueExt}, pii, }; +use diesel_models::configs; use error_stack::{report, FutureExt, IntoReport, ResultExt}; use futures::future::try_join_all; use masking::{PeekInterface, Secret}; @@ -141,6 +142,17 @@ pub async fn create_merchant_account( .transpose()? .map(Secret::new); + let fingerprint = Some(utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs")); + if let Some(fingerprint) = fingerprint { + db.insert_config(configs::ConfigNew { + key: format!("fingerprint_secret_{}", req.merchant_id), + config: fingerprint, + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Mot able to generate Merchant fingerprint")?; + }; + let organization_id = if let Some(organization_id) = req.organization_id.as_ref() { db.find_organization_by_org_id(organization_id) .await diff --git a/crates/router/src/core/blocklist.rs b/crates/router/src/core/blocklist.rs new file mode 100644 index 0000000000..8584560244 --- /dev/null +++ b/crates/router/src/core/blocklist.rs @@ -0,0 +1,41 @@ +pub mod transformers; +pub mod utils; + +use api_models::blocklist as api_blocklist; + +use crate::{ + core::errors::{self, RouterResponse}, + routes::AppState, + services, + types::domain, +}; + +pub async fn add_entry_to_blocklist( + state: AppState, + merchant_account: domain::MerchantAccount, + body: api_blocklist::AddToBlocklistRequest, +) -> RouterResponse { + utils::insert_entry_into_blocklist(&state, merchant_account.merchant_id, body) + .await + .map(services::ApplicationResponse::Json) +} + +pub async fn remove_entry_from_blocklist( + state: AppState, + merchant_account: domain::MerchantAccount, + body: api_blocklist::DeleteFromBlocklistRequest, +) -> RouterResponse { + utils::delete_entry_from_blocklist(&state, merchant_account.merchant_id, body) + .await + .map(services::ApplicationResponse::Json) +} + +pub async fn list_blocklist_entries( + state: AppState, + merchant_account: domain::MerchantAccount, + query: api_blocklist::ListBlocklistQuery, +) -> RouterResponse> { + utils::list_blocklist_entries_for_merchant(&state, merchant_account.merchant_id, query) + .await + .map(services::ApplicationResponse::Json) +} diff --git a/crates/router/src/core/blocklist/transformers.rs b/crates/router/src/core/blocklist/transformers.rs new file mode 100644 index 0000000000..2cb5f86a26 --- /dev/null +++ b/crates/router/src/core/blocklist/transformers.rs @@ -0,0 +1,13 @@ +use api_models::blocklist; + +use crate::types::{storage, transformers::ForeignFrom}; + +impl ForeignFrom for blocklist::AddToBlocklistResponse { + fn foreign_from(from: storage::Blocklist) -> Self { + Self { + fingerprint_id: from.fingerprint_id, + data_kind: from.data_kind, + created_at: from.created_at, + } + } +} diff --git a/crates/router/src/core/blocklist/utils.rs b/crates/router/src/core/blocklist/utils.rs new file mode 100644 index 0000000000..b7effaf63a --- /dev/null +++ b/crates/router/src/core/blocklist/utils.rs @@ -0,0 +1,359 @@ +use api_models::blocklist as api_blocklist; +use common_utils::crypto::{self, SignMessage}; +use error_stack::{IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; + +use super::{errors, AppState}; +use crate::{ + consts, + core::errors::{RouterResult, StorageErrorExt}, + types::{storage, transformers::ForeignInto}, + utils, +}; + +pub async fn delete_entry_from_blocklist( + state: &AppState, + merchant_id: String, + request: api_blocklist::DeleteFromBlocklistRequest, +) -> RouterResult { + let blocklist_entry = match request { + api_blocklist::DeleteFromBlocklistRequest::CardBin(bin) => { + delete_card_bin_blocklist_entry(state, &bin, &merchant_id).await? + } + + api_blocklist::DeleteFromBlocklistRequest::ExtendedCardBin(xbin) => { + delete_card_bin_blocklist_entry(state, &xbin, &merchant_id).await? + } + + api_blocklist::DeleteFromBlocklistRequest::Fingerprint(fingerprint_id) => { + let blocklist_fingerprint = state + .store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &merchant_id, + &fingerprint_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "blocklist record with given fingerprint id not found".to_string(), + })?; + + #[cfg(feature = "kms")] + let decrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(blocklist_fingerprint.encrypted_fingerprint) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to kms decrypt fingerprint")?; + + #[cfg(not(feature = "kms"))] + let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint; + + let blocklist_entry = state + .store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(&merchant_id, &fingerprint_id) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist record for the given fingerprint id was found" + .to_string(), + })?; + + state + .store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &merchant_id, + &decrypted_fingerprint, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist record for the given fingerprint id was found" + .to_string(), + })?; + + blocklist_entry + } + }; + + Ok(blocklist_entry.foreign_into()) +} + +pub async fn list_blocklist_entries_for_merchant( + state: &AppState, + merchant_id: String, + query: api_blocklist::ListBlocklistQuery, +) -> RouterResult> { + state + .store + .list_blocklist_entries_by_merchant_id_data_kind( + &merchant_id, + query.data_kind, + query.limit.into(), + query.offset.into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "no blocklist records found".to_string(), + }) + .map(|v| v.into_iter().map(ForeignInto::foreign_into).collect()) +} + +fn validate_card_bin(bin: &str) -> RouterResult<()> { + if bin.len() == 6 && bin.chars().all(|c| c.is_ascii_digit()) { + Ok(()) + } else { + Err(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "data".to_string(), + expected_format: "a 6 digit number".to_string(), + }) + .into_report() + } +} + +fn validate_extended_card_bin(bin: &str) -> RouterResult<()> { + if bin.len() == 8 && bin.chars().all(|c| c.is_ascii_digit()) { + Ok(()) + } else { + Err(errors::ApiErrorResponse::InvalidDataFormat { + field_name: "data".to_string(), + expected_format: "an 8 digit number".to_string(), + }) + .into_report() + } +} + +pub async fn insert_entry_into_blocklist( + state: &AppState, + merchant_id: String, + to_block: api_blocklist::AddToBlocklistRequest, +) -> RouterResult { + let blocklist_entry = match &to_block { + api_blocklist::AddToBlocklistRequest::CardBin(bin) => { + validate_card_bin(bin)?; + duplicate_check_insert_bin( + bin, + state, + &merchant_id, + common_enums::BlocklistDataKind::CardBin, + ) + .await? + } + + api_blocklist::AddToBlocklistRequest::ExtendedCardBin(bin) => { + validate_extended_card_bin(bin)?; + duplicate_check_insert_bin( + bin, + state, + &merchant_id, + common_enums::BlocklistDataKind::ExtendedCardBin, + ) + .await? + } + + api_blocklist::AddToBlocklistRequest::Fingerprint(fingerprint_id) => { + let blocklist_entry_result = state + .store + .find_blocklist_entry_by_merchant_id_fingerprint_id(&merchant_id, fingerprint_id) + .await; + + match blocklist_entry_result { + Ok(_) => { + return Err(errors::ApiErrorResponse::PreconditionFailed { + message: "data associated with the given fingerprint is already blocked" + .to_string(), + }) + .into_report(); + } + + // if it is a db not found error, we can proceed as normal + Err(inner) if inner.current_context().is_db_not_found() => {} + + err @ Err(_) => { + err.change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching blocklist entry from table")?; + } + } + + let blocklist_fingerprint = state + .store + .find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &merchant_id, + fingerprint_id, + ) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "fingerprint not found".to_string(), + })?; + + #[cfg(feature = "kms")] + let decrypted_fingerprint = kms::get_kms_client(&state.conf.kms) + .await + .decrypt(blocklist_fingerprint.encrypted_fingerprint) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to kms decrypt encrypted fingerprint")?; + + #[cfg(not(feature = "kms"))] + let decrypted_fingerprint = blocklist_fingerprint.encrypted_fingerprint; + + state + .store + .insert_blocklist_lookup_entry( + diesel_models::blocklist_lookup::BlocklistLookupNew { + merchant_id: merchant_id.clone(), + fingerprint: decrypted_fingerprint, + }, + ) + .await + .to_duplicate_response(errors::ApiErrorResponse::PreconditionFailed { + message: "the payment instrument associated with the given fingerprint is already in the blocklist".to_string(), + }) + .attach_printable("failed to add fingerprint to blocklist lookup")?; + + state + .store + .insert_blocklist_entry(storage::BlocklistNew { + merchant_id: merchant_id.clone(), + fingerprint_id: fingerprint_id.clone(), + data_kind: blocklist_fingerprint.data_kind, + metadata: None, + created_at: common_utils::date_time::now(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("failed to add fingerprint to pm blocklist")? + } + }; + + Ok(blocklist_entry.foreign_into()) +} + +pub async fn get_merchant_fingerprint_secret( + state: &AppState, + merchant_id: &str, +) -> RouterResult { + let key = get_merchant_fingerprint_secret_key(merchant_id); + let config_fetch_result = state.store.find_config_by_key(&key).await; + + match config_fetch_result { + Ok(config) => Ok(config.config), + + Err(e) if e.current_context().is_db_not_found() => { + let new_fingerprint_secret = + utils::generate_id(consts::FINGERPRINT_SECRET_LENGTH, "fs"); + let new_config = storage::ConfigNew { + key, + config: new_fingerprint_secret.clone(), + }; + + state + .store + .insert_config(new_config) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to create new fingerprint secret for merchant")?; + + Ok(new_fingerprint_secret) + } + + Err(e) => Err(e) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error fetching merchant fingerprint secret"), + } +} + +pub fn get_merchant_fingerprint_secret_key(merchant_id: &str) -> String { + format!("fingerprint_secret_{merchant_id}") +} + +async fn duplicate_check_insert_bin( + bin: &str, + state: &AppState, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, +) -> RouterResult { + let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?; + let bin_fingerprint = crypto::HmacSha512::sign_message( + &crypto::HmacSha512, + merchant_secret.clone().as_bytes(), + bin.as_bytes(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error in bin hash creation")?; + + let encoded_fingerprint = hex::encode(bin_fingerprint.clone()); + + let blocklist_entry_result = state + .store + .find_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin) + .await; + + match blocklist_entry_result { + Ok(_) => { + return Err(errors::ApiErrorResponse::PreconditionFailed { + message: "provided bin is already blocked".to_string(), + }) + .into_report(); + } + + Err(e) if e.current_context().is_db_not_found() => {} + + err @ Err(_) => { + return err + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to fetch blocklist entry"); + } + } + + // Checking for duplicacy + state + .store + .insert_blocklist_lookup_entry(diesel_models::blocklist_lookup::BlocklistLookupNew { + merchant_id: merchant_id.to_string(), + fingerprint: encoded_fingerprint.clone(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting blocklist lookup entry")?; + + state + .store + .insert_blocklist_entry(storage::BlocklistNew { + merchant_id: merchant_id.to_string(), + fingerprint_id: bin.to_string(), + data_kind, + metadata: None, + created_at: common_utils::date_time::now(), + }) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error inserting pm blocklist item") +} + +async fn delete_card_bin_blocklist_entry( + state: &AppState, + bin: &str, + merchant_id: &str, +) -> RouterResult { + let merchant_secret = get_merchant_fingerprint_secret(state, merchant_id).await?; + let bin_fingerprint = crypto::HmacSha512 + .sign_message(merchant_secret.as_bytes(), bin.as_bytes()) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("error when hashing card bin")?; + let encoded_fingerprint = hex::encode(bin_fingerprint); + + state + .store + .delete_blocklist_lookup_entry_by_merchant_id_fingerprint(merchant_id, &encoded_fingerprint) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "could not find a blocklist entry for the given bin".to_string(), + })?; + + state + .store + .delete_blocklist_entry_by_merchant_id_fingerprint_id(merchant_id, bin) + .await + .to_not_found_response(errors::ApiErrorResponse::GenericNotFoundError { + message: "could not find a blocklist entry for the given bin".to_string(), + }) +} diff --git a/crates/router/src/core/errors/api_error_response.rs b/crates/router/src/core/errors/api_error_response.rs index f94504cf27..54ec4ec1e2 100644 --- a/crates/router/src/core/errors/api_error_response.rs +++ b/crates/router/src/core/errors/api_error_response.rs @@ -186,6 +186,8 @@ 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::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 fa9a518579..ff764cafed 100644 --- a/crates/router/src/core/errors/transformers.rs +++ b/crates/router/src/core/errors/transformers.rs @@ -187,6 +187,7 @@ 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::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/helpers.rs b/crates/router/src/core/payments/helpers.rs index ec6371f310..003c09b738 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2586,6 +2586,7 @@ mod tests { modified_at: common_utils::date_time::now(), last_synced: None, setup_future_usage: None, + fingerprint_id: None, off_session: None, client_secret: Some("1".to_string()), active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()), @@ -2638,6 +2639,7 @@ mod tests { statement_descriptor_suffix: None, created_at: common_utils::date_time::now().saturating_sub(time::Duration::seconds(20)), modified_at: common_utils::date_time::now(), + fingerprint_id: None, last_synced: None, setup_future_usage: None, off_session: None, @@ -2695,6 +2697,7 @@ mod tests { setup_future_usage: None, off_session: None, client_secret: None, + fingerprint_id: None, active_attempt: data_models::RemoteStorageObject::ForeignID("nopes".to_string()), business_country: None, business_label: None, diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 00ae8da6ae..c81145c5de 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -2,23 +2,30 @@ use std::marker::PhantomData; use api_models::enums::FrmSuggestion; use async_trait::async_trait; -use common_utils::ext_traits::{AsyncExt, Encode}; +use common_utils::{ + crypto::{self, SignMessage}, + ext_traits::{AsyncExt, Encode}, +}; use error_stack::{report, IntoReport, ResultExt}; +#[cfg(feature = "kms")] +use external_services::kms; use futures::FutureExt; use router_derive::PaymentOperation; -use router_env::{instrument, tracing}; +use router_env::{instrument, logger, 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}, payment_methods::PaymentMethodRetrieve, payments::{ self, helpers, operations, populate_surcharge_details, CustomerDetails, PaymentAddress, PaymentData, }, - utils::{self as core_utils}, + utils as core_utils, }, db::StorageInterface, routes::AppState, @@ -620,32 +627,34 @@ 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 (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 (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 connector = payment_data.payment_attempt.connector.clone(); let merchant_connector_id = payment_data.payment_attempt.merchant_connector_id.clone(); @@ -709,6 +718,157 @@ impl 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 = "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), + ); + } + } + let surcharge_amount = payment_data .surcharge_details .as_ref() @@ -789,6 +949,7 @@ impl metadata: m_metadata, payment_confirm_source: header_payload.payment_confirm_source, updated_by: m_storage_scheme, + fingerprint_id, session_expiry, }, storage_scheme, @@ -838,6 +999,11 @@ 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 09ec436ed0..2b25a74deb 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -825,6 +825,7 @@ impl PaymentCreate { request_incremental_authorization, incremental_authorization_allowed: None, authorization_count: None, + fingerprint_id: None, session_expiry: Some(session_expiry), }) } diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index afb83d38dc..e002b92d18 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -617,6 +617,7 @@ impl metadata, payment_confirm_source: None, updated_by: storage_scheme.to_string(), + fingerprint_id: None, session_expiry, }, storage_scheme, diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 359373e469..5a3a322fb1 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -706,6 +706,7 @@ where .set_incremental_authorization_allowed( payment_intent.incremental_authorization_allowed, ) + .set_fingerprint(payment_intent.fingerprint_id) .set_authorization_count(payment_intent.authorization_count) .set_incremental_authorizations(incremental_authorizations_response) .to_owned(), diff --git a/crates/router/src/db.rs b/crates/router/src/db.rs index 5beace9cbb..b9d346b7a7 100644 --- a/crates/router/src/db.rs +++ b/crates/router/src/db.rs @@ -1,6 +1,9 @@ pub mod address; pub mod api_keys; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; +pub mod blocklist_lookup; pub mod business_profile; pub mod cache; pub mod capture; @@ -68,6 +71,7 @@ pub trait StorageInterface: + dyn_clone::DynClone + address::AddressInterface + api_keys::ApiKeyInterface + + blocklist_lookup::BlocklistLookupInterface + configs::ConfigInterface + capture::CaptureInterface + customers::CustomerInterface @@ -85,6 +89,8 @@ pub trait StorageInterface: + PaymentAttemptInterface + PaymentIntentInterface + payment_method::PaymentMethodInterface + + blocklist::BlocklistInterface + + blocklist_fingerprint::BlocklistFingerprintInterface + scheduler::SchedulerInterface + payout_attempt::PayoutAttemptInterface + payouts::PayoutsInterface diff --git a/crates/router/src/db/blocklist.rs b/crates/router/src/db/blocklist.rs new file mode 100644 index 0000000000..c263bef63c --- /dev/null +++ b/crates/router/src/db/blocklist.rs @@ -0,0 +1,203 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistInterface { + async fn insert_blocklist_entry( + &self, + pm_blocklist_new: storage::BlocklistNew, + ) -> CustomResult; + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; + + async fn list_blocklist_entries_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError>; + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError>; +} + +#[async_trait::async_trait] +impl BlocklistInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + pm_blocklist + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::find_by_merchant_id_fingerprint_id(&conn, merchant_id, fingerprint_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::list_by_merchant_id(&conn, merchant_id) + .await + .map_err(Into::into) + .into_report() + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + merchant_id: &str, + data_kind: common_enums::BlocklistDataKind, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::list_by_merchant_id_data_kind( + &conn, + merchant_id, + data_kind, + limit, + offset, + ) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::Blocklist::delete_by_merchant_id_fingerprint_id(&conn, merchant_id, fingerprint_id) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + _pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + _merchant_id: &str, + _data_kind: common_enums::BlocklistDataKind, + _limit: i64, + _offset: i64, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_entry( + &self, + _pm_blocklist: storage::BlocklistNew, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn find_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn delete_blocklist_entry_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn list_blocklist_entries_by_merchant_id_data_kind( + &self, + _merchant_id: &str, + _data_kind: common_enums::BlocklistDataKind, + _limit: i64, + _offset: i64, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::KafkaError)? + } + + async fn list_blocklist_entries_by_merchant_id( + &self, + _merchant_id: &str, + ) -> CustomResult, errors::StorageError> { + Err(errors::StorageError::KafkaError)? + } +} diff --git a/crates/router/src/db/blocklist_fingerprint.rs b/crates/router/src/db/blocklist_fingerprint.rs new file mode 100644 index 0000000000..9da7c7d8fb --- /dev/null +++ b/crates/router/src/db/blocklist_fingerprint.rs @@ -0,0 +1,95 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistFingerprintInterface { + async fn insert_blocklist_fingerprint_entry( + &self, + pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult; + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + pm_fingerprint_new + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + merchant_id: &str, + fingerprint_id: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistFingerprint::find_by_merchant_id_fingerprint_id( + &conn, + merchant_id, + fingerprint_id, + ) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + _pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistFingerprintInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_fingerprint_entry( + &self, + _pm_fingerprint_new: storage::BlocklistFingerprintNew, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn find_blocklist_fingerprint_by_merchant_id_fingerprint_id( + &self, + _merchant_id: &str, + _fingerprint_id: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } +} diff --git a/crates/router/src/db/blocklist_lookup.rs b/crates/router/src/db/blocklist_lookup.rs new file mode 100644 index 0000000000..0dfd81c8b8 --- /dev/null +++ b/crates/router/src/db/blocklist_lookup.rs @@ -0,0 +1,125 @@ +use error_stack::IntoReport; +use router_env::{instrument, tracing}; +use storage_impl::MockDb; + +use super::Store; +use crate::{ + connection, + core::errors::{self, CustomResult}, + db::kafka_store::KafkaStore, + types::storage, +}; + +#[async_trait::async_trait] +pub trait BlocklistLookupInterface { + async fn insert_blocklist_lookup_entry( + &self, + blocklist_lookup_new: storage::BlocklistLookupNew, + ) -> CustomResult; + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult; + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult; +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for Store { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + blocklist_lookup_entry + .insert(&conn) + .await + .map_err(Into::into) + .into_report() + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistLookup::find_by_merchant_id_fingerprint(&conn, merchant_id, fingerprint) + .await + .map_err(Into::into) + .into_report() + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + merchant_id: &str, + fingerprint: &str, + ) -> CustomResult { + let conn = connection::pg_connection_write(self).await?; + storage::BlocklistLookup::delete_by_merchant_id_fingerprint(&conn, merchant_id, fingerprint) + .await + .map_err(Into::into) + .into_report() + } +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for MockDb { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + _blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::MockDbError)? + } +} + +#[async_trait::async_trait] +impl BlocklistLookupInterface for KafkaStore { + #[instrument(skip_all)] + async fn insert_blocklist_lookup_entry( + &self, + _blocklist_lookup_entry: storage::BlocklistLookupNew, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn find_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } + + async fn delete_blocklist_lookup_entry_by_merchant_id_fingerprint( + &self, + _merchant_id: &str, + _fingerprint: &str, + ) -> CustomResult { + Err(errors::StorageError::KafkaError)? + } +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 3b4c7ce9b7..696198f215 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -129,9 +129,9 @@ pub fn mk_app( #[cfg(feature = "oltp")] { server_app = server_app - .service(routes::PaymentMethods::server(state.clone())) .service(routes::EphemeralKey::server(state.clone())) .service(routes::Webhooks::server(state.clone())) + .service(routes::PaymentMethods::server(state.clone())) } #[cfg(feature = "olap")] @@ -143,6 +143,7 @@ pub fn mk_app( .service(routes::Disputes::server(state.clone())) .service(routes::Analytics::server(state.clone())) .service(routes::Routing::server(state.clone())) + .service(routes::Blocklist::server(state.clone())) .service(routes::LockerMigrate::server(state.clone())) .service(routes::Gsm::server(state.clone())) .service(routes::PaymentLink::server(state.clone())) diff --git a/crates/router/src/routes.rs b/crates/router/src/routes.rs index ec718b2dde..d4bfabb6f9 100644 --- a/crates/router/src/routes.rs +++ b/crates/router/src/routes.rs @@ -1,6 +1,8 @@ pub mod admin; pub mod api_keys; pub mod app; +#[cfg(feature = "olap")] +pub mod blocklist; pub mod cache; pub mod cards_info; pub mod configs; @@ -42,14 +44,15 @@ pub mod webhooks; pub mod locker_migration; #[cfg(any(feature = "olap", feature = "oltp"))] pub mod pm_auth; +#[cfg(feature = "olap")] +pub use app::{Blocklist, Routing}; + #[cfg(feature = "dummy_connector")] pub use self::app::DummyConnector; #[cfg(any(feature = "olap", feature = "oltp"))] pub use self::app::Forex; #[cfg(feature = "payouts")] pub use self::app::Payouts; -#[cfg(feature = "olap")] -pub use self::app::Routing; #[cfg(all(feature = "olap", feature = "kms"))] pub use self::app::Verify; pub use self::app::{ diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 015e3305de..0b2acaf4e5 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -14,6 +14,8 @@ use scheduler::SchedulerInterface; use storage_impl::MockDb; use tokio::sync::oneshot; +#[cfg(feature = "olap")] +use super::blocklist; #[cfg(any(feature = "olap", feature = "oltp"))] use super::currency; #[cfg(feature = "dummy_connector")] @@ -566,6 +568,23 @@ impl PaymentMethods { } } +#[cfg(feature = "olap")] +pub struct Blocklist; + +#[cfg(feature = "olap")] +impl Blocklist { + pub fn server(state: AppState) -> Scope { + web::scope("/blocklist") + .app_data(web::Data::new(state)) + .service( + web::resource("") + .route(web::get().to(blocklist::list_blocked_payment_methods)) + .route(web::post().to(blocklist::add_entry_to_blocklist)) + .route(web::delete().to(blocklist::remove_entry_from_blocklist)), + ) + } +} + pub struct MerchantAccount; #[cfg(feature = "olap")] diff --git a/crates/router/src/routes/blocklist.rs b/crates/router/src/routes/blocklist.rs new file mode 100644 index 0000000000..7c268dddee --- /dev/null +++ b/crates/router/src/routes/blocklist.rs @@ -0,0 +1,81 @@ +use actix_web::{web, HttpRequest, HttpResponse}; +use api_models::blocklist as api_blocklist; +use router_env::Flow; + +use crate::{ + core::{api_locking, blocklist}, + routes::AppState, + services::{api, authentication as auth, authorization::permissions::Permission}, +}; + +pub async fn add_entry_to_blocklist( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::AddToBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, body| { + blocklist::add_entry_to_blocklist(state, auth.merchant_account, body) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn remove_entry_from_blocklist( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::DeleteFromBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: auth::AuthenticationData, body| { + blocklist::remove_entry_from_blocklist(state, auth.merchant_account, body) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountWrite), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} + +pub async fn list_blocked_payment_methods( + state: web::Data, + req: HttpRequest, + query_payload: web::Query, +) -> HttpResponse { + let flow = Flow::ListBlocklist; + Box::pin(api::server_wrap( + flow, + state, + &req, + query_payload.into_inner(), + |state, auth: auth::AuthenticationData, query| { + blocklist::list_blocklist_entries(state, auth.merchant_account, query) + }, + auth::auth_type( + &auth::ApiKeyAuth, + &auth::JWTAuth(Permission::MerchantAccountRead), + req.headers(), + ), + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 10f408f3d4..55c6cbc23d 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -24,6 +24,7 @@ pub enum ApiIdentifier { ApiKeys, PaymentLink, Routing, + Blocklist, Forex, RustLockerMigration, Gsm, @@ -57,6 +58,10 @@ impl From for ApiIdentifier { Flow::RetrieveForexFlow => Self::Forex, + Flow::AddToBlocklist => Self::Blocklist, + Flow::DeleteFromBlocklist => Self::Blocklist, + Flow::ListBlocklist => Self::Blocklist, + Flow::MerchantConnectorsCreate | Flow::MerchantConnectorsRetrieve | Flow::MerchantConnectorsUpdate diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 56d3272b94..b93cbbbbba 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -1,6 +1,9 @@ pub mod address; pub mod api_keys; pub mod authorization; +pub mod blocklist; +pub mod blocklist_fingerprint; +pub mod blocklist_lookup; pub mod business_profile; pub mod capture; pub mod cards_info; @@ -43,7 +46,8 @@ pub use diesel_models::{ProcessTracker, ProcessTrackerNew, ProcessTrackerUpdate} pub use scheduler::db::process_tracker; pub use self::{ - address::*, api_keys::*, authorization::*, capture::*, cards_info::*, configs::*, customers::*, + address::*, api_keys::*, authorization::*, blocklist::*, blocklist_fingerprint::*, + blocklist_lookup::*, capture::*, cards_info::*, configs::*, customers::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, diff --git a/crates/router/src/types/storage/blocklist.rs b/crates/router/src/types/storage/blocklist.rs new file mode 100644 index 0000000000..7e7648dd4a --- /dev/null +++ b/crates/router/src/types/storage/blocklist.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist::{Blocklist, BlocklistNew}; diff --git a/crates/router/src/types/storage/blocklist_fingerprint.rs b/crates/router/src/types/storage/blocklist_fingerprint.rs new file mode 100644 index 0000000000..092d881e3f --- /dev/null +++ b/crates/router/src/types/storage/blocklist_fingerprint.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist_fingerprint::{BlocklistFingerprint, BlocklistFingerprintNew}; diff --git a/crates/router/src/types/storage/blocklist_lookup.rs b/crates/router/src/types/storage/blocklist_lookup.rs new file mode 100644 index 0000000000..978708ff7c --- /dev/null +++ b/crates/router/src/types/storage/blocklist_lookup.rs @@ -0,0 +1 @@ +pub use diesel_models::blocklist_lookup::{BlocklistLookup, BlocklistLookupNew}; diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 33f1e21153..dcf635595e 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -199,6 +199,7 @@ pub async fn generate_sample_data( request_incremental_authorization: Default::default(), incremental_authorization_allowed: Default::default(), authorization_count: Default::default(), + fingerprint_id: None, session_expiry: Some(session_expiry), }; let payment_attempt = PaymentAttemptBatchNew { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index e37e15443b..a6ac1b1e0a 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -185,6 +185,12 @@ pub enum Flow { RoutingUpdateDefaultConfig, /// Routing delete config RoutingDeleteConfig, + /// Add record to blocklist + AddToBlocklist, + /// Delete record from blocklist + DeleteFromBlocklist, + /// List entries from blocklist + ListBlocklist, /// Incoming Webhook Receive IncomingWebhookReceive, /// Validate payment method flow diff --git a/crates/storage_impl/src/errors.rs b/crates/storage_impl/src/errors.rs index 50173bb1c7..ac3a04e85b 100644 --- a/crates/storage_impl/src/errors.rs +++ b/crates/storage_impl/src/errors.rs @@ -55,6 +55,8 @@ pub enum StorageError { SerializationFailed, #[error("MockDb error")] MockDbError, + #[error("Kafka error")] + KafkaError, #[error("Customer with this id is Redacted")] CustomerRedacted, #[error("Deserialization failure")] @@ -103,6 +105,7 @@ impl Into for &StorageError { StorageError::KVError => DataStorageError::KVError, StorageError::SerializationFailed => DataStorageError::SerializationFailed, StorageError::MockDbError => DataStorageError::MockDbError, + StorageError::KafkaError => DataStorageError::KafkaError, StorageError::CustomerRedacted => DataStorageError::CustomerRedacted, StorageError::DeserializationFailed => DataStorageError::DeserializationFailed, StorageError::EncryptionError => DataStorageError::EncryptionError, diff --git a/crates/storage_impl/src/mock_db/payment_intent.rs b/crates/storage_impl/src/mock_db/payment_intent.rs index ee8676106f..3f892ed9fa 100644 --- a/crates/storage_impl/src/mock_db/payment_intent.rs +++ b/crates/storage_impl/src/mock_db/payment_intent.rs @@ -109,6 +109,7 @@ impl PaymentIntentInterface for MockDb { request_incremental_authorization: new.request_incremental_authorization, incremental_authorization_allowed: new.incremental_authorization_allowed, authorization_count: new.authorization_count, + fingerprint_id: new.fingerprint_id, session_expiry: new.session_expiry, }; payment_intents.push(payment_intent.clone()); diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 07d70c9056..8d20dfe0f3 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -101,6 +101,7 @@ impl PaymentIntentInterface for KVRouterStore { request_incremental_authorization: new.request_incremental_authorization, incremental_authorization_allowed: new.incremental_authorization_allowed, authorization_count: new.authorization_count, + fingerprint_id: new.fingerprint_id.clone(), session_expiry: new.session_expiry, }; let redis_entry = kv::TypedSql { @@ -769,6 +770,7 @@ impl DataModelExt for PaymentIntentNew { request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, authorization_count: self.authorization_count, + fingerprint_id: self.fingerprint_id, session_expiry: self.session_expiry, } } @@ -813,6 +815,7 @@ impl DataModelExt for PaymentIntentNew { request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, authorization_count: storage_model.authorization_count, + fingerprint_id: storage_model.fingerprint_id, session_expiry: storage_model.session_expiry, } } @@ -862,6 +865,7 @@ impl DataModelExt for PaymentIntent { request_incremental_authorization: self.request_incremental_authorization, incremental_authorization_allowed: self.incremental_authorization_allowed, authorization_count: self.authorization_count, + fingerprint_id: self.fingerprint_id, session_expiry: self.session_expiry, } } @@ -907,6 +911,7 @@ impl DataModelExt for PaymentIntent { request_incremental_authorization: storage_model.request_incremental_authorization, incremental_authorization_allowed: storage_model.incremental_authorization_allowed, authorization_count: storage_model.authorization_count, + fingerprint_id: storage_model.fingerprint_id, session_expiry: storage_model.session_expiry, } } @@ -990,6 +995,7 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, } => DieselPaymentIntentUpdate::Update { amount, @@ -1009,6 +1015,7 @@ impl DataModelExt for PaymentIntentUpdate { metadata, payment_confirm_source, updated_by, + fingerprint_id, session_expiry, }, Self::PaymentAttemptAndAttemptCountUpdate { diff --git a/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql b/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql new file mode 100644 index 0000000000..74c450622a --- /dev/null +++ b/migrations/2023-12-11-075542_create_pm_fingerprint_table/down.sql @@ -0,0 +1,5 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist_fingerprint; + +DROP TYPE "BlocklistDataKind"; diff --git a/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql b/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql new file mode 100644 index 0000000000..417d779200 --- /dev/null +++ b/migrations/2023-12-11-075542_create_pm_fingerprint_table/up.sql @@ -0,0 +1,19 @@ +-- Your SQL goes here + +CREATE TYPE "BlocklistDataKind" AS ENUM ( + 'payment_method', + 'card_bin', + 'extended_card_bin' +); + +CREATE TABLE blocklist_fingerprint ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint_id VARCHAR(64) NOT NULL, + data_kind "BlocklistDataKind" NOT NULL, + encrypted_fingerprint TEXT NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX blocklist_fingerprint_merchant_id_fingerprint_id_index +ON blocklist_fingerprint (merchant_id, fingerprint_id); diff --git a/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql b/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql new file mode 100644 index 0000000000..cd7d412aad --- /dev/null +++ b/migrations/2023-12-12-112941_create_pm_blocklist_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist; diff --git a/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql b/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql new file mode 100644 index 0000000000..6d921dd78c --- /dev/null +++ b/migrations/2023-12-12-112941_create_pm_blocklist_table/up.sql @@ -0,0 +1,13 @@ +-- Your SQL goes here + +CREATE TABLE blocklist ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint_id VARCHAR(64) NOT NULL, + data_kind "BlocklistDataKind" NOT NULL, + metadata JSONB, + created_at TIMESTAMP NOT NULL +); + +CREATE UNIQUE INDEX blocklist_unique_fingerprint_id_index ON blocklist (merchant_id, fingerprint_id); +CREATE INDEX blocklist_merchant_id_data_kind_created_at_index ON blocklist (merchant_id, data_kind, created_at DESC); diff --git a/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql new file mode 100644 index 0000000000..46b871b6ee --- /dev/null +++ b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payment_intent DROP COLUMN IF EXISTS fingerprint_id; diff --git a/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql new file mode 100644 index 0000000000..831fb7b6ff --- /dev/null +++ b/migrations/2023-12-12-113330_add_fingerprint_id_in_payment_intent/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS fingerprint_id VARCHAR(64); diff --git a/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql b/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql new file mode 100644 index 0000000000..d2363f547a --- /dev/null +++ b/migrations/2023-12-18-062613_create_blocklist_lookup_table/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE blocklist_lookup; diff --git a/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql b/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql new file mode 100644 index 0000000000..8af3e209fc --- /dev/null +++ b/migrations/2023-12-18-062613_create_blocklist_lookup_table/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here + +CREATE TABLE blocklist_lookup ( + id SERIAL PRIMARY KEY, + merchant_id VARCHAR(64) NOT NULL, + fingerprint TEXT NOT NULL +); + +CREATE UNIQUE INDEX blocklist_lookup_merchant_id_fingerprint_index ON blocklist_lookup (merchant_id, fingerprint); diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 4423d1177c..7a2b5504e0 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -10726,6 +10726,11 @@ }, "description": "List of incremental authorizations happened to the payment", "nullable": true + }, + "fingerprint": { + "type": "string", + "description": "Payment Fingerprint", + "nullable": true } } },