use chrono::Utc; use db::database::AppDatabase; use models::projects::project_members; use queue::ProjectRoomEvent; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; use uuid::Uuid; use crate::error::RoomError; pub fn notify_project_members( db: AppDatabase, project_id: Uuid, notification_type: crate::NotificationType, title: String, content: Option, related_room_id: Option, ) { let notification_type_inner = notification_type; let title_inner = title; let content_inner = content; let related_room_id_inner = related_room_id; let project_id_inner = project_id; tokio::spawn(async move { let members = match project_members::Entity::find() .filter(project_members::Column::Project.eq(project_id_inner)) .all(&db) .await { Ok(m) => m, Err(e) => { tracing::error!(project_id = %project_id_inner, error = %e, "notify_project_members: failed to fetch members"); return; } }; for member in members { let user_id = member.user; if let Err(e) = create_notification_sync( &db, notification_type_inner, user_id, title_inner.clone(), content_inner.clone(), related_room_id_inner, project_id_inner, ) .await { tracing::warn!(user_id = %user_id, project_id = %project_id_inner, error = %e, "notify_project_members: failed to create notification for user"); } } }); } async fn create_notification_sync( db: &AppDatabase, notification_type: crate::NotificationType, user_id: Uuid, title: String, content: Option, related_room_id: Option, project_id: Uuid, ) -> Result<(), RoomError> { use models::rooms::room_notifications; use sea_orm::{ActiveModelTrait, Set}; let notification_type_model = match notification_type { crate::NotificationType::Mention => room_notifications::NotificationType::Mention, crate::NotificationType::Invitation => room_notifications::NotificationType::Invitation, crate::NotificationType::RoleChange => room_notifications::NotificationType::RoleChange, crate::NotificationType::RoomCreated => room_notifications::NotificationType::RoomCreated, crate::NotificationType::RoomDeleted => room_notifications::NotificationType::RoomDeleted, crate::NotificationType::SystemAnnouncement => { room_notifications::NotificationType::SystemAnnouncement } crate::NotificationType::ProjectInvitation => { room_notifications::NotificationType::ProjectInvitation } }; let _model = room_notifications::ActiveModel { id: Set(Uuid::now_v7()), room: Set(related_room_id), project: Set(Some(project_id)), user_id: Set(Some(user_id)), notification_type: Set(notification_type_model), related_message_id: Set(None), related_user_id: Set(None), related_room_id: Set(related_room_id), title: Set(title), content: Set(content), metadata: Set(None), is_read: Set(false), is_archived: Set(false), created_at: Set(Utc::now()), read_at: Set(None), expires_at: Set(None), } .insert(db) .await .map_err(|e| RoomError::Database(e))?; Ok(()) } pub fn publish_room_event( queue: &queue::MessageProducer, project_id: Uuid, event_type: crate::RoomEventType, room_id: Option, message_id: Option, seq: Option, ) { let event = ProjectRoomEvent { event_type: event_type.as_str().into(), project_id, room_id, category_id: None, message_id, seq, timestamp: Utc::now(), }; // Fire-and-forget — caller doesn't need to await. let queue = queue.clone(); tokio::spawn(async move { queue.publish_project_room_event(project_id, event).await; }); }