use uuid::Uuid;
use crate::infrastructure::persistence::trace_repository::{
MailingTraceCounts, TraceRepository,
};
#[derive(Debug, Clone, Default, PartialEq)]
pub struct MailingStats {
pub mailing_id: Uuid,
pub total: i64,
pub sent: i64,
pub delivered: i64,
pub opened: i64,
pub clicked: i64,
pub replied: i64,
pub bounced: i64,
pub errored: i64,
pub canceled: i64,
pub opened_ratio: Option<i64>,
pub clicks_ratio: Option<i64>,
pub replied_ratio: Option<i64>,
}
pub const SHARED_SOURCE_CAVEAT: &str =
"this engagement source serves several campaigns — the grouped totals are the \
source's, not any single campaign's";
#[derive(Debug, Clone, Default, PartialEq)]
pub struct SourceGroupedStats {
pub source_id: Uuid,
pub campaigns: i64,
pub mailings: i64,
pub total: i64,
pub sent: i64,
pub opened: i64,
pub clicked: i64,
pub replied: i64,
pub shared_source: bool,
pub shared_source_caveat: Option<&'static str>,
}
impl From<(Uuid, MailingTraceCounts)> for MailingStats {
fn from((mailing_id, c): (Uuid, MailingTraceCounts)) -> Self {
Self {
mailing_id,
total: c.total,
sent: c.sent,
delivered: c.delivered,
opened: c.opened,
clicked: c.clicked,
replied: c.replied,
bounced: c.bounced,
errored: c.errored,
canceled: c.canceled,
opened_ratio: c.opened_ratio(),
clicks_ratio: c.clicks_ratio(),
replied_ratio: c.replied_ratio(),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum MailingStatsError {
#[error("db: {0}")]
Db(#[from] sqlx::Error),
#[error("not found: {0}")]
NotFound(String),
}
impl MailingStatsError {
pub fn code(&self) -> &'static str {
match self {
Self::Db(_) => "mailing_db_error",
Self::NotFound(_) => "not_found",
}
}
pub fn http_status(&self) -> u16 {
match self {
Self::Db(_) => 500,
Self::NotFound(_) => 404,
}
}
}
pub struct MailingStatsReadService {
pool: sqlx::PgPool,
}
impl MailingStatsReadService {
pub fn new(pool: sqlx::PgPool) -> Self {
Self { pool }
}
pub async fn stats_for_mailings(
&self,
mailing_ids: &[Uuid],
) -> Result<Vec<MailingStats>, MailingStatsError> {
let mut tx = self.pool.begin().await?;
let rows = TraceRepository::counts_for_mailings(&mut tx, mailing_ids).await?;
tx.commit().await?;
Ok(rows.into_iter().map(MailingStats::from).collect())
}
pub async fn stats_for_mailing(
&self,
mailing_id: Uuid,
) -> Result<MailingStats, MailingStatsError> {
let all = self.stats_for_mailings(&[mailing_id]).await?;
all.into_iter()
.next()
.ok_or_else(|| MailingStatsError::NotFound(format!("mailing {mailing_id}")))
}
pub async fn source_grouped_stats(
&self,
mailing_ids: &[Uuid],
) -> Result<Vec<SourceGroupedStats>, MailingStatsError> {
let mut tx = self.pool.begin().await?;
let rows = TraceRepository::source_grouped_counts(&mut tx, mailing_ids).await?;
tx.commit().await?;
Ok(rows
.into_iter()
.map(|(source_id, campaigns, mailings, total, sent, opened, clicked, replied)| {
let shared_source = campaigns > 1;
SourceGroupedStats {
source_id,
campaigns,
mailings,
total,
sent,
opened,
clicked,
replied,
shared_source,
shared_source_caveat: shared_source.then_some(SHARED_SOURCE_CAVEAT),
}
})
.collect())
}
}