//! Notification delivery tracking — maps to `message_notification` table. //! //! Records when a message triggers a notification for a user (mention, reply, //! thread activity, etc.) and tracks the delivery/read lifecycle. //! Separate from `message_mention` which only covers @mentions. use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; /// A notification triggered for a user by a message. /// /// Maps to the `message_notification` table. Records when a message triggers /// a notification (mention, reply, thread activity) and tracks delivery/read. #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] pub struct MessageNotification { pub id: Uuid, pub message_id: Uuid, pub channel_id: Uuid, pub user_id: Uuid, /// "mention" | "reply" | "thread" | "watch" pub reason: String, /// "pending" | "delivered" | "read" | "dismissed" pub status: String, /// Channel of delivery: "push" | "email" | "in_app" pub delivery_channel: Option, pub delivered_at: Option>, pub read_at: Option>, pub created_at: DateTime, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum NotificationReason { #[default] Mention, Reply, Thread, Watch, } impl NotificationReason { pub fn as_str(&self) -> &'static str { match self { Self::Mention => "mention", Self::Reply => "reply", Self::Thread => "thread", Self::Watch => "watch", } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "snake_case")] pub enum NotificationStatus { #[default] Pending, Delivered, Read, Dismissed, } impl NotificationStatus { pub fn as_str(&self) -> &'static str { match self { Self::Pending => "pending", Self::Delivered => "delivered", Self::Read => "read", Self::Dismissed => "dismissed", } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_notification_serialize() { let n = MessageNotification { id: Uuid::now_v7(), message_id: Uuid::now_v7(), channel_id: Uuid::now_v7(), user_id: Uuid::now_v7(), reason: NotificationReason::Mention.as_str().into(), status: NotificationStatus::Pending.as_str().into(), delivery_channel: Some("push".into()), delivered_at: None, read_at: None, created_at: Utc::now(), }; let json = serde_json::to_value(&n).unwrap(); assert_eq!(json["reason"], "mention"); assert_eq!(json["status"], "pending"); } }