use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use sqlx::FromRow;
use std::fmt;
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct Notification {
pub notification_id: Uuid,
pub user_id: Uuid,
pub notification_type: NotificationType,
pub title: String,
pub message: String,
pub data: Option<JsonValue>,
pub is_read: bool,
pub read_at: Option<DateTime<Utc>>,
pub priority: NotificationPriority,
pub resource_type: Option<String>,
pub resource_id: Option<Uuid>,
pub action_url: Option<String>,
pub expires_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "snake_case")]
#[derive(Default)]
pub enum NotificationType {
OrderCreated,
OrderFilled,
OrderCancelled,
OrderExpired,
TradeExecuted,
PaymentReceived,
PaymentConfirmed,
PaymentExpired,
PaymentRefund,
TokenCreated,
TokenUpdated,
TokenPaused,
TokenPriceAlert,
NewTokenHolder,
CommitmentDeadlineApproaching,
CommitmentDeadlinePassed,
CommitmentVerified,
CommitmentRejected,
ReputationChanged,
KycSubmitted,
KycApproved,
KycRejected,
KycDocumentRequired,
ProposalCreated,
ProposalVoted,
ProposalPassed,
ProposalRejected,
LoginFromNewDevice,
PasswordChanged,
TwoFactorEnabled,
SecurityAlert,
AccountLocked,
MaintenanceScheduled,
SystemUpdate,
#[default]
AnnouncementGeneral,
AnnouncementImportant,
NewFollower,
NewMessage,
MentionedInComment,
}
impl fmt::Display for NotificationType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self)
}
}
#[derive(
Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq, PartialOrd, Ord,
)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum NotificationPriority {
Low,
#[default]
Normal,
High,
Urgent,
}
impl fmt::Display for NotificationPriority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NotificationPriority::Low => write!(f, "low"),
NotificationPriority::Normal => write!(f, "normal"),
NotificationPriority::High => write!(f, "high"),
NotificationPriority::Urgent => write!(f, "urgent"),
}
}
}
impl fmt::Display for Notification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"Notification({}, type={}, priority={})",
self.notification_id, self.notification_type, self.priority
)
}
}
impl Notification {
pub fn builder() -> NotificationBuilder {
NotificationBuilder::new()
}
pub fn is_expired(&self) -> bool {
if let Some(expires_at) = self.expires_at {
Utc::now() > expires_at
} else {
false
}
}
pub fn mark_as_read(&mut self) {
self.is_read = true;
self.read_at = Some(Utc::now());
}
pub fn age_minutes(&self) -> i64 {
(Utc::now() - self.created_at).num_minutes()
}
pub fn is_recent(&self) -> bool {
self.age_minutes() < 60
}
}
pub struct NotificationBuilder {
user_id: Option<Uuid>,
notification_type: Option<NotificationType>,
title: Option<String>,
message: Option<String>,
data: Option<JsonValue>,
priority: NotificationPriority,
resource_type: Option<String>,
resource_id: Option<Uuid>,
action_url: Option<String>,
expires_at: Option<DateTime<Utc>>,
}
impl NotificationBuilder {
pub fn new() -> Self {
Self {
user_id: None,
notification_type: None,
title: None,
message: None,
data: None,
priority: NotificationPriority::Normal,
resource_type: None,
resource_id: None,
action_url: None,
expires_at: None,
}
}
pub fn user_id(mut self, user_id: Uuid) -> Self {
self.user_id = Some(user_id);
self
}
pub fn notification_type(mut self, notification_type: NotificationType) -> Self {
self.notification_type = Some(notification_type);
self
}
pub fn title(mut self, title: String) -> Self {
self.title = Some(title);
self
}
pub fn message(mut self, message: String) -> Self {
self.message = Some(message);
self
}
pub fn data(mut self, data: JsonValue) -> Self {
self.data = Some(data);
self
}
pub fn priority(mut self, priority: NotificationPriority) -> Self {
self.priority = priority;
self
}
pub fn resource(mut self, resource_type: String, resource_id: Uuid) -> Self {
self.resource_type = Some(resource_type);
self.resource_id = Some(resource_id);
self
}
pub fn action_url(mut self, url: String) -> Self {
self.action_url = Some(url);
self
}
pub fn expires_at(mut self, expires_at: DateTime<Utc>) -> Self {
self.expires_at = Some(expires_at);
self
}
pub fn build(self) -> Result<Notification, String> {
Ok(Notification {
notification_id: Uuid::new_v4(),
user_id: self
.user_id
.ok_or_else(|| "User ID is required".to_string())?,
notification_type: self
.notification_type
.ok_or_else(|| "Notification type is required".to_string())?,
title: self.title.ok_or_else(|| "Title is required".to_string())?,
message: self
.message
.ok_or_else(|| "Message is required".to_string())?,
data: self.data,
is_read: false,
read_at: None,
priority: self.priority,
resource_type: self.resource_type,
resource_id: self.resource_id,
action_url: self.action_url,
expires_at: self.expires_at,
created_at: Utc::now(),
})
}
}
impl Default for NotificationBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
pub struct NotificationPreferences {
pub user_id: Uuid,
pub email_enabled: bool,
pub in_app_enabled: bool,
pub push_enabled: bool,
pub frequency: NotificationFrequency,
pub enabled_types: Option<JsonValue>,
pub quiet_hours_start: Option<chrono::NaiveTime>,
pub quiet_hours_end: Option<chrono::NaiveTime>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, PartialEq, Eq)]
#[sqlx(type_name = "varchar", rename_all = "lowercase")]
#[derive(Default)]
pub enum NotificationFrequency {
#[default]
Immediate,
Hourly,
Daily,
Weekly,
}
impl fmt::Display for NotificationFrequency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NotificationFrequency::Immediate => write!(f, "immediate"),
NotificationFrequency::Hourly => write!(f, "hourly"),
NotificationFrequency::Daily => write!(f, "daily"),
NotificationFrequency::Weekly => write!(f, "weekly"),
}
}
}
impl NotificationPreferences {
pub fn should_receive_now(&self, notification_type: NotificationType) -> bool {
if !self.email_enabled && !self.in_app_enabled && !self.push_enabled {
return false;
}
if let Some(ref enabled) = self.enabled_types {
if let Some(types) = enabled.as_array() {
let type_str = format!("{:?}", notification_type);
if !types.iter().any(|t| t.as_str() == Some(&type_str)) {
return false;
}
}
}
if let (Some(start), Some(end)) = (self.quiet_hours_start, self.quiet_hours_end) {
let now = Utc::now().time();
if start < end {
if now >= start && now < end {
return false;
}
} else {
if now >= start || now < end {
return false;
}
}
}
true
}
pub fn is_digest_mode(&self) -> bool {
!matches!(self.frequency, NotificationFrequency::Immediate)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmailTemplate {
pub template_name: String,
pub subject: String,
pub body_html: String,
pub body_text: String,
pub variables: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct NotificationStats {
pub user_id: Uuid,
pub total_notifications: i64,
pub unread_notifications: i64,
pub notifications_last_24h: i64,
pub most_common_type: Option<NotificationType>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_notification_builder() {
let notification = Notification::builder()
.user_id(Uuid::new_v4())
.notification_type(NotificationType::OrderFilled)
.title("Order Filled".to_string())
.message("Your order has been filled".to_string())
.priority(NotificationPriority::High)
.build()
.unwrap();
assert_eq!(
notification.notification_type,
NotificationType::OrderFilled
);
assert_eq!(notification.priority, NotificationPriority::High);
assert!(!notification.is_read);
}
#[test]
fn test_notification_expiration() {
let mut notification = Notification::builder()
.user_id(Uuid::new_v4())
.notification_type(NotificationType::OrderExpired)
.title("Test".to_string())
.message("Test message".to_string())
.expires_at(Utc::now() - chrono::Duration::hours(1))
.build()
.unwrap();
assert!(notification.is_expired());
notification.expires_at = Some(Utc::now() + chrono::Duration::hours(1));
assert!(!notification.is_expired());
}
#[test]
fn test_mark_as_read() {
let mut notification = Notification::builder()
.user_id(Uuid::new_v4())
.notification_type(NotificationType::TradeExecuted)
.title("Test".to_string())
.message("Test message".to_string())
.build()
.unwrap();
assert!(!notification.is_read);
assert!(notification.read_at.is_none());
notification.mark_as_read();
assert!(notification.is_read);
assert!(notification.read_at.is_some());
}
#[test]
fn test_notification_preferences() {
let prefs = NotificationPreferences {
user_id: Uuid::new_v4(),
email_enabled: true,
in_app_enabled: true,
push_enabled: false,
frequency: NotificationFrequency::Immediate,
enabled_types: None,
quiet_hours_start: None,
quiet_hours_end: None,
updated_at: Utc::now(),
};
assert!(!prefs.is_digest_mode());
assert!(prefs.should_receive_now(NotificationType::OrderFilled));
}
#[test]
fn test_digest_mode() {
let prefs = NotificationPreferences {
user_id: Uuid::new_v4(),
email_enabled: true,
in_app_enabled: true,
push_enabled: false,
frequency: NotificationFrequency::Daily,
enabled_types: None,
quiet_hours_start: None,
quiet_hours_end: None,
updated_at: Utc::now(),
};
assert!(prefs.is_digest_mode());
}
}