diff --git a/crates/worker/src/handlers.rs b/crates/worker/src/handlers.rs index af61628..37e04af 100644 --- a/crates/worker/src/handlers.rs +++ b/crates/worker/src/handlers.rs @@ -61,6 +61,26 @@ impl NotificationHandler { created_at: Utc::now(), }).await } + DomainEvent::ThoughtCreated { thought_id, user_id, in_reply_to_id } => { + let reply_to_id = match in_reply_to_id { + Some(id) => id, + None => return Ok(()), // not a reply + }; + let original = match self.thoughts.find_by_id(reply_to_id).await? { + Some(t) => t, + None => return Ok(()), // original deleted + }; + if original.user_id == *user_id { return Ok(()); } // no self-notifications + self.notifications.save(&Notification { + id: NotificationId::new(), + user_id: original.user_id, + notification_type: NotificationType::Reply, + from_user_id: Some(user_id.clone()), + thought_id: Some(thought_id.clone()), + read: false, + created_at: Utc::now(), + }).await + } // All other events: no notification needed in Plan 3 _ => Ok(()), } @@ -175,4 +195,81 @@ mod tests { assert_eq!(notifs[0].user_id, alice.id); assert!(matches!(notifs[0].notification_type, NotificationType::Follow)); } + + #[tokio::test] + async fn reply_creates_notification_for_original_author() { + let store = TestStore::default(); + let alice = alice(); + let bob_id = UserId::new(); + + let original = Thought::new_local( + ThoughtId::new(), alice.id.clone(), + Content::new_local("original thought").unwrap(), + None, Visibility::Public, None, false, + ); + store.users.lock().unwrap().push(alice.clone()); + store.thoughts.lock().unwrap().push(original.clone()); + + let handler = NotificationHandler { + thoughts: Arc::new(store.clone()), + notifications: Arc::new(store.clone()), + }; + + handler.handle(&DomainEvent::ThoughtCreated { + thought_id: ThoughtId::new(), + user_id: bob_id.clone(), + in_reply_to_id: Some(original.id.clone()), + }).await.unwrap(); + + let notifs = store.notifications.lock().unwrap(); + assert_eq!(notifs.len(), 1); + assert_eq!(notifs[0].user_id, alice.id); + assert!(matches!(notifs[0].notification_type, NotificationType::Reply)); + } + + #[tokio::test] + async fn self_reply_does_not_create_notification() { + let store = TestStore::default(); + let alice = alice(); + let original = Thought::new_local( + ThoughtId::new(), alice.id.clone(), + Content::new_local("original").unwrap(), + None, Visibility::Public, None, false, + ); + store.users.lock().unwrap().push(alice.clone()); + store.thoughts.lock().unwrap().push(original.clone()); + + let handler = NotificationHandler { + thoughts: Arc::new(store.clone()), + notifications: Arc::new(store.clone()), + }; + + handler.handle(&DomainEvent::ThoughtCreated { + thought_id: ThoughtId::new(), + user_id: alice.id.clone(), + in_reply_to_id: Some(original.id.clone()), + }).await.unwrap(); + + assert!(store.notifications.lock().unwrap().is_empty()); + } + + #[tokio::test] + async fn thought_without_reply_to_creates_no_notification() { + let store = TestStore::default(); + let alice = alice(); + store.users.lock().unwrap().push(alice.clone()); + + let handler = NotificationHandler { + thoughts: Arc::new(store.clone()), + notifications: Arc::new(store.clone()), + }; + + handler.handle(&DomainEvent::ThoughtCreated { + thought_id: ThoughtId::new(), + user_id: alice.id.clone(), + in_reply_to_id: None, + }).await.unwrap(); + + assert!(store.notifications.lock().unwrap().is_empty()); + } }