feat(analytics): adding kafka dispute analytic events (#3549)

Co-authored-by: Sampras Lopes <lsampras@pm.me>
This commit is contained in:
harsh-sharma-juspay
2024-02-16 18:06:07 +05:30
committed by GitHub
parent fb254b8924
commit 39e2233982
8 changed files with 393 additions and 4 deletions

View File

@ -374,9 +374,15 @@ impl CustomerInterface for KafkaStore {
impl DisputeInterface for KafkaStore {
async fn insert_dispute(
&self,
dispute: storage::DisputeNew,
dispute_new: storage::DisputeNew,
) -> CustomResult<storage::Dispute, errors::StorageError> {
self.diesel_store.insert_dispute(dispute).await
let dispute = self.diesel_store.insert_dispute(dispute_new).await?;
if let Err(er) = self.kafka_producer.log_dispute(&dispute, None).await {
logger::error!(message="Failed to add analytics entry for Dispute {dispute:?}", error_message=?er);
};
Ok(dispute)
}
async fn find_by_merchant_id_payment_id_connector_dispute_id(
@ -419,7 +425,19 @@ impl DisputeInterface for KafkaStore {
this: storage::Dispute,
dispute: storage::DisputeUpdate,
) -> CustomResult<storage::Dispute, errors::StorageError> {
self.diesel_store.update_dispute(this, dispute).await
let dispute_new = self
.diesel_store
.update_dispute(this.clone(), dispute)
.await?;
if let Err(er) = self
.kafka_producer
.log_dispute(&dispute_new, Some(this))
.await
{
logger::error!(message="Failed to add analytics entry for Dispute {dispute_new:?}", error_message=?er);
};
Ok(dispute_new)
}
async fn find_disputes_by_merchant_id_payment_id(

View File

@ -8,6 +8,7 @@ use rdkafka::{
};
use crate::events::EventType;
mod dispute;
mod payment_attempt;
mod payment_intent;
mod refund;
@ -17,8 +18,10 @@ use serde::Serialize;
use time::OffsetDateTime;
use self::{
payment_attempt::KafkaPaymentAttempt, payment_intent::KafkaPaymentIntent, refund::KafkaRefund,
dispute::KafkaDispute, payment_attempt::KafkaPaymentAttempt,
payment_intent::KafkaPaymentIntent, refund::KafkaRefund,
};
use crate::types::storage::Dispute;
// Using message queue result here to avoid confusion with Kafka result provided by library
pub type MQResult<T> = CustomResult<T, KafkaError>;
@ -82,6 +85,7 @@ pub struct KafkaSettings {
api_logs_topic: String,
connector_logs_topic: String,
outgoing_webhook_logs_topic: String,
dispute_analytics_topic: String,
}
impl KafkaSettings {
@ -135,6 +139,12 @@ impl KafkaSettings {
},
)?;
common_utils::fp_utils::when(self.dispute_analytics_topic.is_default_or_empty(), || {
Err(ApplicationError::InvalidConfigurationValueError(
"Kafka Dispute Logs topic must not be empty".into(),
))
})?;
Ok(())
}
}
@ -148,6 +158,7 @@ pub struct KafkaProducer {
api_logs_topic: String,
connector_logs_topic: String,
outgoing_webhook_logs_topic: String,
dispute_analytics_topic: String,
}
struct RdKafkaProducer(ThreadedProducer<DefaultProducerContext>);
@ -186,6 +197,7 @@ impl KafkaProducer {
api_logs_topic: conf.api_logs_topic.clone(),
connector_logs_topic: conf.connector_logs_topic.clone(),
outgoing_webhook_logs_topic: conf.outgoing_webhook_logs_topic.clone(),
dispute_analytics_topic: conf.dispute_analytics_topic.clone(),
})
}
@ -306,6 +318,27 @@ impl KafkaProducer {
})
}
pub async fn log_dispute(
&self,
dispute: &Dispute,
old_dispute: Option<Dispute>,
) -> MQResult<()> {
if let Some(negative_event) = old_dispute {
self.log_kafka_event(
&self.dispute_analytics_topic,
&KafkaEvent::old(&KafkaDispute::from_storage(&negative_event)),
)
.attach_printable_lazy(|| {
format!("Failed to add negative dispute event {negative_event:?}")
})?;
};
self.log_kafka_event(
&self.dispute_analytics_topic,
&KafkaEvent::new(&KafkaDispute::from_storage(dispute)),
)
.attach_printable_lazy(|| format!("Failed to add positive dispute event {dispute:?}"))
}
pub fn get_topic(&self, event: EventType) -> &str {
match event {
EventType::ApiLogs => &self.api_logs_topic,

View File

@ -0,0 +1,76 @@
use diesel_models::enums as storage_enums;
use masking::Secret;
use time::OffsetDateTime;
use crate::types::storage::dispute::Dispute;
#[derive(serde::Serialize, Debug)]
pub struct KafkaDispute<'a> {
pub dispute_id: &'a String,
pub amount: &'a String,
pub currency: &'a String,
pub dispute_stage: &'a storage_enums::DisputeStage,
pub dispute_status: &'a storage_enums::DisputeStatus,
pub payment_id: &'a String,
pub attempt_id: &'a String,
pub merchant_id: &'a String,
pub connector_status: &'a String,
pub connector_dispute_id: &'a String,
pub connector_reason: Option<&'a String>,
pub connector_reason_code: Option<&'a String>,
#[serde(default, with = "time::serde::timestamp::option")]
pub challenge_required_by: Option<OffsetDateTime>,
#[serde(default, with = "time::serde::timestamp::option")]
pub connector_created_at: Option<OffsetDateTime>,
#[serde(default, with = "time::serde::timestamp::option")]
pub connector_updated_at: Option<OffsetDateTime>,
#[serde(default, with = "time::serde::timestamp")]
pub created_at: OffsetDateTime,
#[serde(default, with = "time::serde::timestamp")]
pub modified_at: OffsetDateTime,
pub connector: &'a String,
pub evidence: &'a Secret<serde_json::Value>,
pub profile_id: Option<&'a String>,
pub merchant_connector_id: Option<&'a String>,
}
impl<'a> KafkaDispute<'a> {
pub fn from_storage(dispute: &'a Dispute) -> Self {
Self {
dispute_id: &dispute.dispute_id,
amount: &dispute.amount,
currency: &dispute.currency,
dispute_stage: &dispute.dispute_stage,
dispute_status: &dispute.dispute_status,
payment_id: &dispute.payment_id,
attempt_id: &dispute.attempt_id,
merchant_id: &dispute.merchant_id,
connector_status: &dispute.connector_status,
connector_dispute_id: &dispute.connector_dispute_id,
connector_reason: dispute.connector_reason.as_ref(),
connector_reason_code: dispute.connector_reason_code.as_ref(),
challenge_required_by: dispute.challenge_required_by.map(|i| i.assume_utc()),
connector_created_at: dispute.connector_created_at.map(|i| i.assume_utc()),
connector_updated_at: dispute.connector_updated_at.map(|i| i.assume_utc()),
created_at: dispute.created_at.assume_utc(),
modified_at: dispute.modified_at.assume_utc(),
connector: &dispute.connector,
evidence: &dispute.evidence,
profile_id: dispute.profile_id.as_ref(),
merchant_connector_id: dispute.merchant_connector_id.as_ref(),
}
}
}
impl<'a> super::KafkaMessage for KafkaDispute<'a> {
fn key(&self) -> String {
format!(
"{}_{}_{}",
self.merchant_id, self.payment_id, self.dispute_id
)
}
fn creation_timestamp(&self) -> Option<i64> {
Some(self.modified_at.unix_timestamp())
}
}