1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
7pub enum NoticeType {
8 Announcement,
10 Event,
12 LostAndFound,
14 ClassifiedAd,
16}
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub enum NoticeCategory {
21 General,
23 Maintenance,
25 Social,
27 Security,
29 Environment,
31 Parking,
33 Other,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
39pub enum NoticeStatus {
40 Draft,
42 Published,
44 Archived,
46 Expired,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct Notice {
65 pub id: Uuid,
66 pub building_id: Uuid,
67 pub author_id: Uuid, 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, pub published_at: Option<DateTime<Utc>>,
75 pub expires_at: Option<DateTime<Utc>>,
76 pub archived_at: Option<DateTime<Utc>>,
77 pub event_date: Option<DateTime<Utc>>,
79 pub event_location: Option<String>,
80 pub contact_info: Option<String>,
82 pub created_at: DateTime<Utc>,
83 pub updated_at: DateTime<Utc>,
84}
85
86impl Notice {
87 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 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 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 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 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 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; 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 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; self.updated_at = Utc::now();
220 Ok(())
221 }
222
223 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 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 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 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 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 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 if let Some(new_category) = category {
313 self.category = new_category;
314 }
315
316 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 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 if let Some(new_contact_info) = contact_info {
338 self.contact_info = new_contact_info;
339 }
340
341 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 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, 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, 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(), "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); 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(), 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 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); }
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 assert!(!notice.is_expired());
751
752 notice.expires_at = Some(Utc::now() - Duration::days(1));
754 assert!(notice.is_expired());
755
756 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); }
908}