koprogo_api/domain/entities/
notice.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Notice type for community board
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum NoticeType {
8    /// General announcement (info, rules, reminders)
9    Announcement,
10    /// Community event (party, meeting, workshop)
11    Event,
12    /// Lost and found items
13    LostAndFound,
14    /// Classified ad (buy, sell, services)
15    ClassifiedAd,
16}
17
18/// Notice category for filtering
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub enum NoticeCategory {
21    /// General information
22    General,
23    /// Maintenance and repairs
24    Maintenance,
25    /// Social events and activities
26    Social,
27    /// Security and safety
28    Security,
29    /// Environment and recycling
30    Environment,
31    /// Parking and transportation
32    Parking,
33    /// Other category
34    Other,
35}
36
37/// Notice status workflow
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub enum NoticeStatus {
40    /// Draft (not visible to others)
41    Draft,
42    /// Published (visible to all building members)
43    Published,
44    /// Archived (moved to history)
45    Archived,
46    /// Expired (automatically expired based on expires_at)
47    Expired,
48}
49
50/// Community notice board entry
51///
52/// Represents an announcement, event, lost & found item, or classified ad
53/// posted on the building's community board.
54///
55/// # Business Rules
56/// - Title must be 5-255 characters
57/// - Content must be non-empty (max 10,000 characters)
58/// - Draft notices cannot be pinned
59/// - Only published notices are visible to building members
60/// - Expired notices are automatically marked as Expired
61/// - Events must have event_date and event_location
62/// - Lost & Found and Classified Ads should have contact_info
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Notice {
65    pub id: Uuid,
66    pub building_id: Uuid,
67    pub author_id: Uuid, // Owner who created the notice
68    pub notice_type: NoticeType,
69    pub category: NoticeCategory,
70    pub title: String,
71    pub content: String,
72    pub status: NoticeStatus,
73    pub is_pinned: bool, // Pin important notices to top
74    pub published_at: Option<DateTime<Utc>>,
75    pub expires_at: Option<DateTime<Utc>>,
76    pub archived_at: Option<DateTime<Utc>>,
77    // Event-specific fields
78    pub event_date: Option<DateTime<Utc>>,
79    pub event_location: Option<String>,
80    // Contact info for LostAndFound and ClassifiedAd
81    pub contact_info: Option<String>,
82    pub created_at: DateTime<Utc>,
83    pub updated_at: DateTime<Utc>,
84}
85
86impl Notice {
87    /// Create a new notice (Draft status)
88    ///
89    /// # Validation
90    /// - Title must be 5-255 characters
91    /// - Content must be non-empty (max 10,000 characters)
92    /// - Events must have event_date and event_location
93    pub fn new(
94        building_id: Uuid,
95        author_id: Uuid,
96        notice_type: NoticeType,
97        category: NoticeCategory,
98        title: String,
99        content: String,
100        event_date: Option<DateTime<Utc>>,
101        event_location: Option<String>,
102        contact_info: Option<String>,
103    ) -> Result<Self, String> {
104        // Validate title
105        if title.len() < 5 {
106            return Err("Notice title must be at least 5 characters".to_string());
107        }
108        if title.len() > 255 {
109            return Err("Notice title cannot exceed 255 characters".to_string());
110        }
111
112        // Validate content
113        if content.trim().is_empty() {
114            return Err("Notice content cannot be empty".to_string());
115        }
116        if content.len() > 10_000 {
117            return Err("Notice content cannot exceed 10,000 characters".to_string());
118        }
119
120        // Validate event fields
121        if notice_type == NoticeType::Event {
122            if event_date.is_none() {
123                return Err("Event notices must have an event_date".to_string());
124            }
125            if event_location.is_none() || event_location.as_ref().unwrap().trim().is_empty() {
126                return Err("Event notices must have an event_location".to_string());
127            }
128        }
129
130        let now = Utc::now();
131
132        Ok(Self {
133            id: Uuid::new_v4(),
134            building_id,
135            author_id,
136            notice_type,
137            category,
138            title,
139            content,
140            status: NoticeStatus::Draft,
141            is_pinned: false,
142            published_at: None,
143            expires_at: None,
144            archived_at: None,
145            event_date,
146            event_location,
147            contact_info,
148            created_at: now,
149            updated_at: now,
150        })
151    }
152
153    /// Publish a draft notice
154    ///
155    /// # Transitions
156    /// Draft → Published
157    ///
158    /// # Business Rules
159    /// - Only Draft notices can be published
160    /// - Sets published_at timestamp
161    pub fn publish(&mut self) -> Result<(), String> {
162        if self.status != NoticeStatus::Draft {
163            return Err(format!(
164                "Cannot publish notice in status {:?}. Only Draft notices can be published.",
165                self.status
166            ));
167        }
168
169        self.status = NoticeStatus::Published;
170        self.published_at = Some(Utc::now());
171        self.updated_at = Utc::now();
172        Ok(())
173    }
174
175    /// Archive a notice
176    ///
177    /// # Transitions
178    /// Published → Archived
179    /// Expired → Archived
180    ///
181    /// # Business Rules
182    /// - Only Published or Expired notices can be archived
183    /// - Sets archived_at timestamp
184    /// - Unpins notice if pinned
185    pub fn archive(&mut self) -> Result<(), String> {
186        match self.status {
187            NoticeStatus::Published | NoticeStatus::Expired => {
188                self.status = NoticeStatus::Archived;
189                self.archived_at = Some(Utc::now());
190                self.is_pinned = false; // Unpin when archiving
191                self.updated_at = Utc::now();
192                Ok(())
193            }
194            _ => Err(format!(
195                "Cannot archive notice in status {:?}. Only Published or Expired notices can be archived.",
196                self.status
197            )),
198        }
199    }
200
201    /// Mark notice as expired
202    ///
203    /// # Transitions
204    /// Published → Expired
205    ///
206    /// # Business Rules
207    /// - Only Published notices can expire
208    /// - Unpins notice if pinned
209    pub fn expire(&mut self) -> Result<(), String> {
210        if self.status != NoticeStatus::Published {
211            return Err(format!(
212                "Cannot expire notice in status {:?}. Only Published notices can expire.",
213                self.status
214            ));
215        }
216
217        self.status = NoticeStatus::Expired;
218        self.is_pinned = false; // Unpin when expiring
219        self.updated_at = Utc::now();
220        Ok(())
221    }
222
223    /// Pin notice to top of board
224    ///
225    /// # Business Rules
226    /// - Only Published notices can be pinned
227    /// - Draft, Archived, Expired notices cannot be pinned
228    pub fn pin(&mut self) -> Result<(), String> {
229        if self.status != NoticeStatus::Published {
230            return Err(format!(
231                "Cannot pin notice in status {:?}. Only Published notices can be pinned.",
232                self.status
233            ));
234        }
235
236        if self.is_pinned {
237            return Err("Notice is already pinned".to_string());
238        }
239
240        self.is_pinned = true;
241        self.updated_at = Utc::now();
242        Ok(())
243    }
244
245    /// Unpin notice
246    pub fn unpin(&mut self) -> Result<(), String> {
247        if !self.is_pinned {
248            return Err("Notice is not pinned".to_string());
249        }
250
251        self.is_pinned = false;
252        self.updated_at = Utc::now();
253        Ok(())
254    }
255
256    /// Check if notice is expired
257    ///
258    /// Returns true if expires_at is set and is in the past
259    pub fn is_expired(&self) -> bool {
260        if let Some(expires_at) = self.expires_at {
261            Utc::now() > expires_at
262        } else {
263            false
264        }
265    }
266
267    /// Update notice content
268    ///
269    /// # Business Rules
270    /// - Only Draft notices can be updated
271    /// - Same validation as new()
272    pub fn update_content(
273        &mut self,
274        title: Option<String>,
275        content: Option<String>,
276        category: Option<NoticeCategory>,
277        event_date: Option<Option<DateTime<Utc>>>,
278        event_location: Option<Option<String>>,
279        contact_info: Option<Option<String>>,
280        expires_at: Option<Option<DateTime<Utc>>>,
281    ) -> Result<(), String> {
282        if self.status != NoticeStatus::Draft {
283            return Err(format!(
284                "Cannot update notice in status {:?}. Only Draft notices can be updated.",
285                self.status
286            ));
287        }
288
289        // Update title if provided
290        if let Some(new_title) = title {
291            if new_title.len() < 5 {
292                return Err("Notice title must be at least 5 characters".to_string());
293            }
294            if new_title.len() > 255 {
295                return Err("Notice title cannot exceed 255 characters".to_string());
296            }
297            self.title = new_title;
298        }
299
300        // Update content if provided
301        if let Some(new_content) = content {
302            if new_content.trim().is_empty() {
303                return Err("Notice content cannot be empty".to_string());
304            }
305            if new_content.len() > 10_000 {
306                return Err("Notice content cannot exceed 10,000 characters".to_string());
307            }
308            self.content = new_content;
309        }
310
311        // Update category if provided
312        if let Some(new_category) = category {
313            self.category = new_category;
314        }
315
316        // Update event fields if provided
317        if let Some(new_event_date) = event_date {
318            self.event_date = new_event_date;
319        }
320        if let Some(new_event_location) = event_location {
321            self.event_location = new_event_location;
322        }
323
324        // Validate event fields for Event type
325        if self.notice_type == NoticeType::Event {
326            if self.event_date.is_none() {
327                return Err("Event notices must have an event_date".to_string());
328            }
329            if self.event_location.is_none()
330                || self.event_location.as_ref().unwrap().trim().is_empty()
331            {
332                return Err("Event notices must have an event_location".to_string());
333            }
334        }
335
336        // Update contact info if provided
337        if let Some(new_contact_info) = contact_info {
338            self.contact_info = new_contact_info;
339        }
340
341        // Update expires_at if provided
342        if let Some(new_expires_at) = expires_at {
343            self.expires_at = new_expires_at;
344        }
345
346        self.updated_at = Utc::now();
347        Ok(())
348    }
349
350    /// Set expiration date
351    ///
352    /// # Business Rules
353    /// - Expiration date must be in the future
354    /// - Can be set for Draft or Published notices
355    pub fn set_expiration(&mut self, expires_at: Option<DateTime<Utc>>) -> Result<(), String> {
356        if let Some(expiration) = expires_at {
357            if expiration <= Utc::now() {
358                return Err("Expiration date must be in the future".to_string());
359            }
360        }
361
362        self.expires_at = expires_at;
363        self.updated_at = Utc::now();
364        Ok(())
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use chrono::Duration;
372
373    #[test]
374    fn test_create_announcement_success() {
375        let building_id = Uuid::new_v4();
376        let author_id = Uuid::new_v4();
377
378        let notice = Notice::new(
379            building_id,
380            author_id,
381            NoticeType::Announcement,
382            NoticeCategory::General,
383            "Important Announcement".to_string(),
384            "This is an important announcement for all residents.".to_string(),
385            None,
386            None,
387            None,
388        );
389
390        assert!(notice.is_ok());
391        let notice = notice.unwrap();
392        assert_eq!(notice.building_id, building_id);
393        assert_eq!(notice.author_id, author_id);
394        assert_eq!(notice.status, NoticeStatus::Draft);
395        assert!(!notice.is_pinned);
396    }
397
398    #[test]
399    fn test_create_event_success() {
400        let building_id = Uuid::new_v4();
401        let author_id = Uuid::new_v4();
402        let event_date = Utc::now() + Duration::days(7);
403
404        let notice = Notice::new(
405            building_id,
406            author_id,
407            NoticeType::Event,
408            NoticeCategory::Social,
409            "Summer BBQ Party".to_string(),
410            "Join us for a fun summer BBQ party!".to_string(),
411            Some(event_date),
412            Some("Garden courtyard".to_string()),
413            Some("contact@example.com".to_string()),
414        );
415
416        assert!(notice.is_ok());
417        let notice = notice.unwrap();
418        assert_eq!(notice.notice_type, NoticeType::Event);
419        assert!(notice.event_date.is_some());
420        assert_eq!(notice.event_location, Some("Garden courtyard".to_string()));
421    }
422
423    #[test]
424    fn test_create_event_without_date_fails() {
425        let building_id = Uuid::new_v4();
426        let author_id = Uuid::new_v4();
427
428        let result = Notice::new(
429            building_id,
430            author_id,
431            NoticeType::Event,
432            NoticeCategory::Social,
433            "Summer BBQ Party".to_string(),
434            "Join us for a fun summer BBQ party!".to_string(),
435            None, // Missing event_date
436            Some("Garden courtyard".to_string()),
437            None,
438        );
439
440        assert!(result.is_err());
441        assert_eq!(result.unwrap_err(), "Event notices must have an event_date");
442    }
443
444    #[test]
445    fn test_create_event_without_location_fails() {
446        let building_id = Uuid::new_v4();
447        let author_id = Uuid::new_v4();
448        let event_date = Utc::now() + Duration::days(7);
449
450        let result = Notice::new(
451            building_id,
452            author_id,
453            NoticeType::Event,
454            NoticeCategory::Social,
455            "Summer BBQ Party".to_string(),
456            "Join us for a fun summer BBQ party!".to_string(),
457            Some(event_date),
458            None, // Missing event_location
459            None,
460        );
461
462        assert!(result.is_err());
463        assert_eq!(
464            result.unwrap_err(),
465            "Event notices must have an event_location"
466        );
467    }
468
469    #[test]
470    fn test_title_too_short_fails() {
471        let building_id = Uuid::new_v4();
472        let author_id = Uuid::new_v4();
473
474        let result = Notice::new(
475            building_id,
476            author_id,
477            NoticeType::Announcement,
478            NoticeCategory::General,
479            "Hi".to_string(), // Too short (< 5 chars)
480            "This is the content.".to_string(),
481            None,
482            None,
483            None,
484        );
485
486        assert!(result.is_err());
487        assert_eq!(
488            result.unwrap_err(),
489            "Notice title must be at least 5 characters"
490        );
491    }
492
493    #[test]
494    fn test_title_too_long_fails() {
495        let building_id = Uuid::new_v4();
496        let author_id = Uuid::new_v4();
497        let long_title = "A".repeat(256); // 256 chars (> 255)
498
499        let result = Notice::new(
500            building_id,
501            author_id,
502            NoticeType::Announcement,
503            NoticeCategory::General,
504            long_title,
505            "This is the content.".to_string(),
506            None,
507            None,
508            None,
509        );
510
511        assert!(result.is_err());
512        assert_eq!(
513            result.unwrap_err(),
514            "Notice title cannot exceed 255 characters"
515        );
516    }
517
518    #[test]
519    fn test_empty_content_fails() {
520        let building_id = Uuid::new_v4();
521        let author_id = Uuid::new_v4();
522
523        let result = Notice::new(
524            building_id,
525            author_id,
526            NoticeType::Announcement,
527            NoticeCategory::General,
528            "Valid Title".to_string(),
529            "   ".to_string(), // Empty/whitespace content
530            None,
531            None,
532            None,
533        );
534
535        assert!(result.is_err());
536        assert_eq!(result.unwrap_err(), "Notice content cannot be empty");
537    }
538
539    #[test]
540    fn test_publish_draft_success() {
541        let building_id = Uuid::new_v4();
542        let author_id = Uuid::new_v4();
543
544        let mut notice = Notice::new(
545            building_id,
546            author_id,
547            NoticeType::Announcement,
548            NoticeCategory::General,
549            "Important Announcement".to_string(),
550            "This is an important announcement.".to_string(),
551            None,
552            None,
553            None,
554        )
555        .unwrap();
556
557        assert_eq!(notice.status, NoticeStatus::Draft);
558        assert!(notice.published_at.is_none());
559
560        let result = notice.publish();
561        assert!(result.is_ok());
562        assert_eq!(notice.status, NoticeStatus::Published);
563        assert!(notice.published_at.is_some());
564    }
565
566    #[test]
567    fn test_publish_non_draft_fails() {
568        let building_id = Uuid::new_v4();
569        let author_id = Uuid::new_v4();
570
571        let mut notice = Notice::new(
572            building_id,
573            author_id,
574            NoticeType::Announcement,
575            NoticeCategory::General,
576            "Important Announcement".to_string(),
577            "This is an important announcement.".to_string(),
578            None,
579            None,
580            None,
581        )
582        .unwrap();
583
584        notice.publish().unwrap();
585        assert_eq!(notice.status, NoticeStatus::Published);
586
587        // Try to publish again
588        let result = notice.publish();
589        assert!(result.is_err());
590        assert!(result
591            .unwrap_err()
592            .contains("Only Draft notices can be published"));
593    }
594
595    #[test]
596    fn test_archive_published_success() {
597        let building_id = Uuid::new_v4();
598        let author_id = Uuid::new_v4();
599
600        let mut notice = Notice::new(
601            building_id,
602            author_id,
603            NoticeType::Announcement,
604            NoticeCategory::General,
605            "Important Announcement".to_string(),
606            "This is an important announcement.".to_string(),
607            None,
608            None,
609            None,
610        )
611        .unwrap();
612
613        notice.publish().unwrap();
614        notice.pin().unwrap();
615        assert!(notice.is_pinned);
616
617        let result = notice.archive();
618        assert!(result.is_ok());
619        assert_eq!(notice.status, NoticeStatus::Archived);
620        assert!(notice.archived_at.is_some());
621        assert!(!notice.is_pinned); // Should be unpinned
622    }
623
624    #[test]
625    fn test_archive_expired_success() {
626        let building_id = Uuid::new_v4();
627        let author_id = Uuid::new_v4();
628
629        let mut notice = Notice::new(
630            building_id,
631            author_id,
632            NoticeType::Announcement,
633            NoticeCategory::General,
634            "Important Announcement".to_string(),
635            "This is an important announcement.".to_string(),
636            None,
637            None,
638            None,
639        )
640        .unwrap();
641
642        notice.publish().unwrap();
643        notice.expire().unwrap();
644        assert_eq!(notice.status, NoticeStatus::Expired);
645
646        let result = notice.archive();
647        assert!(result.is_ok());
648        assert_eq!(notice.status, NoticeStatus::Archived);
649    }
650
651    #[test]
652    fn test_pin_published_success() {
653        let building_id = Uuid::new_v4();
654        let author_id = Uuid::new_v4();
655
656        let mut notice = Notice::new(
657            building_id,
658            author_id,
659            NoticeType::Announcement,
660            NoticeCategory::General,
661            "Important Announcement".to_string(),
662            "This is an important announcement.".to_string(),
663            None,
664            None,
665            None,
666        )
667        .unwrap();
668
669        notice.publish().unwrap();
670        assert!(!notice.is_pinned);
671
672        let result = notice.pin();
673        assert!(result.is_ok());
674        assert!(notice.is_pinned);
675    }
676
677    #[test]
678    fn test_pin_draft_fails() {
679        let building_id = Uuid::new_v4();
680        let author_id = Uuid::new_v4();
681
682        let mut notice = Notice::new(
683            building_id,
684            author_id,
685            NoticeType::Announcement,
686            NoticeCategory::General,
687            "Important Announcement".to_string(),
688            "This is an important announcement.".to_string(),
689            None,
690            None,
691            None,
692        )
693        .unwrap();
694
695        assert_eq!(notice.status, NoticeStatus::Draft);
696
697        let result = notice.pin();
698        assert!(result.is_err());
699        assert!(result
700            .unwrap_err()
701            .contains("Only Published notices can be pinned"));
702    }
703
704    #[test]
705    fn test_unpin_success() {
706        let building_id = Uuid::new_v4();
707        let author_id = Uuid::new_v4();
708
709        let mut notice = Notice::new(
710            building_id,
711            author_id,
712            NoticeType::Announcement,
713            NoticeCategory::General,
714            "Important Announcement".to_string(),
715            "This is an important announcement.".to_string(),
716            None,
717            None,
718            None,
719        )
720        .unwrap();
721
722        notice.publish().unwrap();
723        notice.pin().unwrap();
724        assert!(notice.is_pinned);
725
726        let result = notice.unpin();
727        assert!(result.is_ok());
728        assert!(!notice.is_pinned);
729    }
730
731    #[test]
732    fn test_is_expired() {
733        let building_id = Uuid::new_v4();
734        let author_id = Uuid::new_v4();
735
736        let mut notice = Notice::new(
737            building_id,
738            author_id,
739            NoticeType::Announcement,
740            NoticeCategory::General,
741            "Important Announcement".to_string(),
742            "This is an important announcement.".to_string(),
743            None,
744            None,
745            None,
746        )
747        .unwrap();
748
749        // No expiration set
750        assert!(!notice.is_expired());
751
752        // Set expiration in the past
753        notice.expires_at = Some(Utc::now() - Duration::days(1));
754        assert!(notice.is_expired());
755
756        // Set expiration in the future
757        notice.expires_at = Some(Utc::now() + Duration::days(1));
758        assert!(!notice.is_expired());
759    }
760
761    #[test]
762    fn test_update_content_draft_success() {
763        let building_id = Uuid::new_v4();
764        let author_id = Uuid::new_v4();
765
766        let mut notice = Notice::new(
767            building_id,
768            author_id,
769            NoticeType::Announcement,
770            NoticeCategory::General,
771            "Original Title".to_string(),
772            "Original content.".to_string(),
773            None,
774            None,
775            None,
776        )
777        .unwrap();
778
779        let result = notice.update_content(
780            Some("Updated Title".to_string()),
781            Some("Updated content.".to_string()),
782            Some(NoticeCategory::Maintenance),
783            None,
784            None,
785            None,
786            None,
787        );
788
789        assert!(result.is_ok());
790        assert_eq!(notice.title, "Updated Title");
791        assert_eq!(notice.content, "Updated content.");
792        assert_eq!(notice.category, NoticeCategory::Maintenance);
793    }
794
795    #[test]
796    fn test_update_content_published_fails() {
797        let building_id = Uuid::new_v4();
798        let author_id = Uuid::new_v4();
799
800        let mut notice = Notice::new(
801            building_id,
802            author_id,
803            NoticeType::Announcement,
804            NoticeCategory::General,
805            "Original Title".to_string(),
806            "Original content.".to_string(),
807            None,
808            None,
809            None,
810        )
811        .unwrap();
812
813        notice.publish().unwrap();
814
815        let result = notice.update_content(
816            Some("Updated Title".to_string()),
817            None,
818            None,
819            None,
820            None,
821            None,
822            None,
823        );
824
825        assert!(result.is_err());
826        assert!(result
827            .unwrap_err()
828            .contains("Only Draft notices can be updated"));
829    }
830
831    #[test]
832    fn test_set_expiration_future_success() {
833        let building_id = Uuid::new_v4();
834        let author_id = Uuid::new_v4();
835
836        let mut notice = Notice::new(
837            building_id,
838            author_id,
839            NoticeType::Announcement,
840            NoticeCategory::General,
841            "Important Announcement".to_string(),
842            "This is an important announcement.".to_string(),
843            None,
844            None,
845            None,
846        )
847        .unwrap();
848
849        let future_date = Utc::now() + Duration::days(7);
850        let result = notice.set_expiration(Some(future_date));
851
852        assert!(result.is_ok());
853        assert_eq!(notice.expires_at, Some(future_date));
854    }
855
856    #[test]
857    fn test_set_expiration_past_fails() {
858        let building_id = Uuid::new_v4();
859        let author_id = Uuid::new_v4();
860
861        let mut notice = Notice::new(
862            building_id,
863            author_id,
864            NoticeType::Announcement,
865            NoticeCategory::General,
866            "Important Announcement".to_string(),
867            "This is an important announcement.".to_string(),
868            None,
869            None,
870            None,
871        )
872        .unwrap();
873
874        let past_date = Utc::now() - Duration::days(1);
875        let result = notice.set_expiration(Some(past_date));
876
877        assert!(result.is_err());
878        assert_eq!(result.unwrap_err(), "Expiration date must be in the future");
879    }
880
881    #[test]
882    fn test_expire_published_success() {
883        let building_id = Uuid::new_v4();
884        let author_id = Uuid::new_v4();
885
886        let mut notice = Notice::new(
887            building_id,
888            author_id,
889            NoticeType::Announcement,
890            NoticeCategory::General,
891            "Important Announcement".to_string(),
892            "This is an important announcement.".to_string(),
893            None,
894            None,
895            None,
896        )
897        .unwrap();
898
899        notice.publish().unwrap();
900        notice.pin().unwrap();
901        assert!(notice.is_pinned);
902
903        let result = notice.expire();
904        assert!(result.is_ok());
905        assert_eq!(notice.status, NoticeStatus::Expired);
906        assert!(!notice.is_pinned); // Should be unpinned
907    }
908}