mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 01:27:31 +08:00
feat(events): add hashed customer_email and feature_metadata (#5220)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1577,6 +1577,7 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"constant_time_eq 0.3.0",
|
"constant_time_eq 0.3.0",
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1958,6 +1959,7 @@ name = "common_utils"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"blake3",
|
||||||
"bytes 1.6.0",
|
"bytes 1.6.0",
|
||||||
"common_enums",
|
"common_enums",
|
||||||
"diesel",
|
"diesel",
|
||||||
|
|||||||
@ -9485,6 +9485,14 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"search_tags": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/RedirectResponse"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"nullable": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -82,6 +82,7 @@ audit_events_topic = "topic" # Kafka topic to be used for Payment
|
|||||||
payout_analytics_topic = "topic" # Kafka topic to be used for Payouts and PayoutAttempt events
|
payout_analytics_topic = "topic" # Kafka topic to be used for Payouts and PayoutAttempt events
|
||||||
consolidated_events_topic = "topic" # Kafka topic to be used for Consolidated events
|
consolidated_events_topic = "topic" # Kafka topic to be used for Consolidated events
|
||||||
authentication_analytics_topic = "topic" # Kafka topic to be used for Authentication events
|
authentication_analytics_topic = "topic" # Kafka topic to be used for Authentication events
|
||||||
|
fraud_check_analytics_topic = "topic" # Kafka topic to be used for Fraud Check events
|
||||||
|
|
||||||
# File storage configuration
|
# File storage configuration
|
||||||
[file_storage]
|
[file_storage]
|
||||||
|
|||||||
@ -51,7 +51,18 @@ pub async fn msearch_results(
|
|||||||
if let Some(customer_email) = filters.customer_email {
|
if let Some(customer_email) = filters.customer_email {
|
||||||
if !customer_email.is_empty() {
|
if !customer_email.is_empty() {
|
||||||
query_builder
|
query_builder
|
||||||
.add_filter_clause("customer_email.keyword".to_string(), customer_email.clone())
|
.add_filter_clause(
|
||||||
|
"customer_email.keyword".to_string(),
|
||||||
|
customer_email
|
||||||
|
.iter()
|
||||||
|
.filter_map(|email| {
|
||||||
|
// TODO: Add trait based inputs instead of converting this to strings
|
||||||
|
serde_json::to_value(email)
|
||||||
|
.ok()
|
||||||
|
.and_then(|a| a.as_str().map(|a| a.to_string()))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
.switch()?;
|
.switch()?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -147,7 +158,18 @@ pub async fn search_results(
|
|||||||
if let Some(customer_email) = filters.customer_email {
|
if let Some(customer_email) = filters.customer_email {
|
||||||
if !customer_email.is_empty() {
|
if !customer_email.is_empty() {
|
||||||
query_builder
|
query_builder
|
||||||
.add_filter_clause("customer_email.keyword".to_string(), customer_email.clone())
|
.add_filter_clause(
|
||||||
|
"customer_email.keyword".to_string(),
|
||||||
|
customer_email
|
||||||
|
.iter()
|
||||||
|
.filter_map(|email| {
|
||||||
|
// TODO: Add trait based inputs instead of converting this to strings
|
||||||
|
serde_json::to_value(email)
|
||||||
|
.ok()
|
||||||
|
.and_then(|a| a.as_str().map(|a| a.to_string()))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
.switch()?;
|
.switch()?;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
use common_utils::hashing::HashedString;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
@ -5,7 +6,7 @@ pub struct SearchFilters {
|
|||||||
pub payment_method: Option<Vec<String>>,
|
pub payment_method: Option<Vec<String>>,
|
||||||
pub currency: Option<Vec<String>>,
|
pub currency: Option<Vec<String>>,
|
||||||
pub status: Option<Vec<String>>,
|
pub status: Option<Vec<String>>,
|
||||||
pub customer_email: Option<Vec<String>>,
|
pub customer_email: Option<Vec<HashedString<common_utils::pii::EmailStrategy>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
|||||||
@ -4821,8 +4821,11 @@ pub struct PaymentsStartRequest {
|
|||||||
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)]
|
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||||
pub struct FeatureMetadata {
|
pub struct FeatureMetadata {
|
||||||
/// Redirection response coming in request as metadata field only for redirection scenarios
|
/// Redirection response coming in request as metadata field only for redirection scenarios
|
||||||
#[schema(value_type = Option<RedirectResponse>)]
|
|
||||||
pub redirect_response: Option<RedirectResponse>,
|
pub redirect_response: Option<RedirectResponse>,
|
||||||
|
// TODO: Convert this to hashedstrings to avoid PII sensitive data
|
||||||
|
/// Additional tags to be used for global search
|
||||||
|
#[schema(value_type = Option<RedirectResponse>)]
|
||||||
|
pub search_tags: Option<Vec<Secret<String>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
///frm message is an object sent inside the payments response...when frm is invoked, its value is Some(...), else its None
|
///frm message is an object sent inside the payments response...when frm is invoked, its value is Some(...), else its None
|
||||||
|
|||||||
@ -44,6 +44,8 @@ tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"], optional
|
|||||||
url = { version = "2.5.0", features = ["serde"] }
|
url = { version = "2.5.0", features = ["serde"] }
|
||||||
utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order"] }
|
utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order"] }
|
||||||
uuid = { version = "1.8.0", features = ["v7"] }
|
uuid = { version = "1.8.0", features = ["v7"] }
|
||||||
|
blake3 = { version = "1.5.1", features = ["serde"] }
|
||||||
|
|
||||||
|
|
||||||
# First party crates
|
# First party crates
|
||||||
rusty-money = { git = "https://github.com/varunsrin/rusty_money", rev = "bbc0150742a0fff905225ff11ee09388e9babdcc", features = ["iso", "crypto"] }
|
rusty-money = { git = "https://github.com/varunsrin/rusty_money", rev = "bbc0150742a0fff905225ff11ee09388e9babdcc", features = ["iso", "crypto"] }
|
||||||
|
|||||||
22
crates/common_utils/src/hashing.rs
Normal file
22
crates/common_utils/src/hashing.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use masking::{PeekInterface, Secret, Strategy};
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
/// Represents a hashed string using blake3's hashing strategy.
|
||||||
|
pub struct HashedString<T: Strategy<String>>(Secret<String, T>);
|
||||||
|
|
||||||
|
impl<T: Strategy<String>> Serialize for HashedString<T> {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
let hashed_value = blake3::hash(self.0.peek().as_bytes()).to_hex();
|
||||||
|
hashed_value.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Strategy<String>> From<Secret<String, T>> for HashedString<T> {
|
||||||
|
fn from(value: Secret<String, T>) -> Self {
|
||||||
|
Self(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -31,6 +31,8 @@ pub mod static_cache;
|
|||||||
pub mod types;
|
pub mod types;
|
||||||
pub mod validation;
|
pub mod validation;
|
||||||
|
|
||||||
|
/// Used for hashing
|
||||||
|
pub mod hashing;
|
||||||
#[cfg(feature = "metrics")]
|
#[cfg(feature = "metrics")]
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ use error_stack::ResultExt;
|
|||||||
use masking::{ExposeInterface, Secret, Strategy, WithType};
|
use masking::{ExposeInterface, Secret, Strategy, WithType};
|
||||||
#[cfg(feature = "logs")]
|
#[cfg(feature = "logs")]
|
||||||
use router_env::logger;
|
use router_env::logger;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
crypto::Encryptable,
|
crypto::Encryptable,
|
||||||
@ -205,7 +206,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Strategy for masking Email
|
/// Strategy for masking Email
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Copy, Clone, Deserialize)]
|
||||||
pub enum EmailStrategy {}
|
pub enum EmailStrategy {}
|
||||||
|
|
||||||
impl<T> Strategy<T> for EmailStrategy
|
impl<T> Strategy<T> for EmailStrategy
|
||||||
|
|||||||
@ -944,6 +944,7 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize {
|
|||||||
param: req.param.map(Secret::new),
|
param: req.param.map(Secret::new),
|
||||||
json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()),
|
json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()),
|
||||||
}),
|
}),
|
||||||
|
search_tags: None,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@ -1230,6 +1231,7 @@ impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize {
|
|||||||
req.json_payload.unwrap_or(serde_json::json!({})).into(),
|
req.json_payload.unwrap_or(serde_json::json!({})).into(),
|
||||||
),
|
),
|
||||||
}),
|
}),
|
||||||
|
search_tags: None,
|
||||||
}),
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use common_utils::{crypto::Encryptable, id_type, types::MinorUnit};
|
use common_utils::{crypto::Encryptable, hashing::HashedString, id_type, pii, types::MinorUnit};
|
||||||
use diesel_models::enums as storage_enums;
|
use diesel_models::enums as storage_enums;
|
||||||
use hyperswitch_domain_models::payments::PaymentIntent;
|
use hyperswitch_domain_models::payments::PaymentIntent;
|
||||||
use masking::Secret;
|
use masking::{PeekInterface, Secret};
|
||||||
|
use serde_json::Value;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[derive(serde::Serialize, Debug)]
|
#[derive(serde::Serialize, Debug)]
|
||||||
@ -33,7 +34,10 @@ pub struct KafkaPaymentIntent<'a> {
|
|||||||
pub business_label: Option<&'a String>,
|
pub business_label: Option<&'a String>,
|
||||||
pub attempt_count: i16,
|
pub attempt_count: i16,
|
||||||
pub payment_confirm_source: Option<storage_enums::PaymentSource>,
|
pub payment_confirm_source: Option<storage_enums::PaymentSource>,
|
||||||
pub billing_details: Option<Encryptable<Secret<serde_json::Value>>>,
|
pub customer_email: Option<HashedString<pii::EmailStrategy>>,
|
||||||
|
pub feature_metadata: Option<&'a Value>,
|
||||||
|
pub merchant_order_reference_id: Option<&'a String>,
|
||||||
|
pub billing_details: Option<Encryptable<Secret<Value>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> KafkaPaymentIntent<'a> {
|
impl<'a> KafkaPaymentIntent<'a> {
|
||||||
@ -63,7 +67,16 @@ impl<'a> KafkaPaymentIntent<'a> {
|
|||||||
business_label: intent.business_label.as_ref(),
|
business_label: intent.business_label.as_ref(),
|
||||||
attempt_count: intent.attempt_count,
|
attempt_count: intent.attempt_count,
|
||||||
payment_confirm_source: intent.payment_confirm_source,
|
payment_confirm_source: intent.payment_confirm_source,
|
||||||
billing_details: intent.billing_details.clone(),
|
customer_email: intent
|
||||||
|
.customer_details
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.get_inner().peek().as_object())
|
||||||
|
.and_then(|obj| obj.get("email"))
|
||||||
|
.and_then(|email| email.as_str())
|
||||||
|
.map(|email| HashedString::from(Secret::new(email.to_string()))),
|
||||||
|
feature_metadata: intent.feature_metadata.as_ref(),
|
||||||
|
merchant_order_reference_id: intent.merchant_order_reference_id.as_ref(),
|
||||||
|
billing_details: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
use common_utils::{crypto::Encryptable, id_type, types::MinorUnit};
|
use common_utils::{crypto::Encryptable, hashing::HashedString, id_type, pii, types::MinorUnit};
|
||||||
use diesel_models::enums as storage_enums;
|
use diesel_models::enums as storage_enums;
|
||||||
use hyperswitch_domain_models::payments::PaymentIntent;
|
use hyperswitch_domain_models::payments::PaymentIntent;
|
||||||
use masking::Secret;
|
use masking::{PeekInterface, Secret};
|
||||||
|
use serde_json::Value;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
#[serde_with::skip_serializing_none]
|
#[serde_with::skip_serializing_none]
|
||||||
@ -34,7 +35,10 @@ pub struct KafkaPaymentIntentEvent<'a> {
|
|||||||
pub business_label: Option<&'a String>,
|
pub business_label: Option<&'a String>,
|
||||||
pub attempt_count: i16,
|
pub attempt_count: i16,
|
||||||
pub payment_confirm_source: Option<storage_enums::PaymentSource>,
|
pub payment_confirm_source: Option<storage_enums::PaymentSource>,
|
||||||
pub billing_details: Option<Encryptable<Secret<serde_json::Value>>>,
|
pub customer_email: Option<HashedString<pii::EmailStrategy>>,
|
||||||
|
pub feature_metadata: Option<&'a Value>,
|
||||||
|
pub merchant_order_reference_id: Option<&'a String>,
|
||||||
|
pub billing_details: Option<Encryptable<Secret<Value>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> KafkaPaymentIntentEvent<'a> {
|
impl<'a> KafkaPaymentIntentEvent<'a> {
|
||||||
@ -64,7 +68,16 @@ impl<'a> KafkaPaymentIntentEvent<'a> {
|
|||||||
business_label: intent.business_label.as_ref(),
|
business_label: intent.business_label.as_ref(),
|
||||||
attempt_count: intent.attempt_count,
|
attempt_count: intent.attempt_count,
|
||||||
payment_confirm_source: intent.payment_confirm_source,
|
payment_confirm_source: intent.payment_confirm_source,
|
||||||
billing_details: intent.billing_details.clone(),
|
customer_email: intent
|
||||||
|
.customer_details
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.get_inner().peek().as_object())
|
||||||
|
.and_then(|obj| obj.get("email"))
|
||||||
|
.and_then(|email| email.as_str())
|
||||||
|
.map(|email| HashedString::from(Secret::new(email.to_string()))),
|
||||||
|
feature_metadata: intent.feature_metadata.as_ref(),
|
||||||
|
merchant_order_reference_id: intent.merchant_order_reference_id.as_ref(),
|
||||||
|
billing_details: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user