1use crate::application::dto::{
2 CreateNoticeDto, NoticeResponseDto, NoticeSummaryDto, SetExpirationDto, UpdateNoticeDto,
3};
4use crate::application::ports::{NoticeRepository, UserRepository};
5use crate::domain::entities::{Notice, NoticeCategory, NoticeStatus, NoticeType};
6use std::sync::Arc;
7use uuid::Uuid;
8
9pub struct NoticeUseCases {
10 notice_repo: Arc<dyn NoticeRepository>,
11 user_repo: Arc<dyn UserRepository>,
12}
13
14impl NoticeUseCases {
15 pub fn new(notice_repo: Arc<dyn NoticeRepository>, user_repo: Arc<dyn UserRepository>) -> Self {
16 Self {
17 notice_repo,
18 user_repo,
19 }
20 }
21
22 fn is_building_admin(role: &str) -> bool {
24 role == "admin" || role == "superadmin" || role == "syndic"
25 }
26
27 async fn resolve_author_name(&self, user_id: Uuid) -> String {
29 match self.user_repo.find_by_id(user_id).await {
30 Ok(Some(user)) => format!("{} {}", user.first_name, user.last_name),
31 _ => "Unknown Author".to_string(),
32 }
33 }
34
35 pub async fn create_notice(
41 &self,
42 user_id: Uuid,
43 _organization_id: Uuid,
44 dto: CreateNoticeDto,
45 ) -> Result<NoticeResponseDto, String> {
46 let notice = Notice::new(
48 dto.building_id,
49 user_id,
50 dto.notice_type,
51 dto.category,
52 dto.title,
53 dto.content,
54 dto.event_date,
55 dto.event_location,
56 dto.contact_info,
57 )?;
58
59 let mut notice = notice;
61 if let Some(expires_at) = dto.expires_at {
62 notice.set_expiration(Some(expires_at))?;
63 }
64
65 let created = self.notice_repo.create(¬ice).await?;
67
68 let author_name = self.resolve_author_name(user_id).await;
69 Ok(NoticeResponseDto::from_notice(created, author_name))
70 }
71
72 pub async fn get_notice(&self, notice_id: Uuid) -> Result<NoticeResponseDto, String> {
74 let notice = self
75 .notice_repo
76 .find_by_id(notice_id)
77 .await?
78 .ok_or("Notice not found".to_string())?;
79
80 let author_name = self.resolve_author_name(notice.author_id).await;
81 Ok(NoticeResponseDto::from_notice(notice, author_name))
82 }
83
84 pub async fn list_building_notices(
89 &self,
90 building_id: Uuid,
91 ) -> Result<Vec<NoticeSummaryDto>, String> {
92 let notices = self.notice_repo.find_by_building(building_id).await?;
93 self.enrich_notices_summary(notices).await
94 }
95
96 pub async fn list_published_notices(
101 &self,
102 building_id: Uuid,
103 ) -> Result<Vec<NoticeSummaryDto>, String> {
104 let notices = self
105 .notice_repo
106 .find_published_by_building(building_id)
107 .await?;
108 self.enrich_notices_summary(notices).await
109 }
110
111 pub async fn list_pinned_notices(
113 &self,
114 building_id: Uuid,
115 ) -> Result<Vec<NoticeSummaryDto>, String> {
116 let notices = self
117 .notice_repo
118 .find_pinned_by_building(building_id)
119 .await?;
120 self.enrich_notices_summary(notices).await
121 }
122
123 pub async fn list_notices_by_type(
125 &self,
126 building_id: Uuid,
127 notice_type: NoticeType,
128 ) -> Result<Vec<NoticeSummaryDto>, String> {
129 let notices = self
130 .notice_repo
131 .find_by_type(building_id, notice_type)
132 .await?;
133 self.enrich_notices_summary(notices).await
134 }
135
136 pub async fn list_notices_by_category(
138 &self,
139 building_id: Uuid,
140 category: NoticeCategory,
141 ) -> Result<Vec<NoticeSummaryDto>, String> {
142 let notices = self
143 .notice_repo
144 .find_by_category(building_id, category)
145 .await?;
146 self.enrich_notices_summary(notices).await
147 }
148
149 pub async fn list_notices_by_status(
151 &self,
152 building_id: Uuid,
153 status: NoticeStatus,
154 ) -> Result<Vec<NoticeSummaryDto>, String> {
155 let notices = self.notice_repo.find_by_status(building_id, status).await?;
156 self.enrich_notices_summary(notices).await
157 }
158
159 pub async fn list_author_notices(
161 &self,
162 author_id: Uuid,
163 ) -> Result<Vec<NoticeSummaryDto>, String> {
164 let notices = self.notice_repo.find_by_author(author_id).await?;
165 self.enrich_notices_summary(notices).await
166 }
167
168 pub async fn update_notice(
174 &self,
175 notice_id: Uuid,
176 user_id: Uuid,
177 _organization_id: Uuid,
178 dto: UpdateNoticeDto,
179 ) -> Result<NoticeResponseDto, String> {
180 let mut notice = self
181 .notice_repo
182 .find_by_id(notice_id)
183 .await?
184 .ok_or("Notice not found".to_string())?;
185
186 if notice.author_id != user_id {
188 return Err("Unauthorized: only author can update notice".to_string());
189 }
190
191 notice.update_content(
193 dto.title,
194 dto.content,
195 dto.category,
196 dto.event_date,
197 dto.event_location,
198 dto.contact_info,
199 dto.expires_at,
200 )?;
201
202 let updated = self.notice_repo.update(¬ice).await?;
204
205 self.get_notice(updated.id).await
207 }
208
209 pub async fn publish_notice(
214 &self,
215 notice_id: Uuid,
216 user_id: Uuid,
217 _organization_id: Uuid,
218 ) -> Result<NoticeResponseDto, String> {
219 let mut notice = self
220 .notice_repo
221 .find_by_id(notice_id)
222 .await?
223 .ok_or("Notice not found".to_string())?;
224
225 if notice.author_id != user_id {
227 return Err("Unauthorized: only author can publish notice".to_string());
228 }
229
230 notice.publish()?;
232
233 let updated = self.notice_repo.update(¬ice).await?;
235
236 self.get_notice(updated.id).await
238 }
239
240 pub async fn archive_notice(
245 &self,
246 notice_id: Uuid,
247 user_id: Uuid,
248 _organization_id: Uuid,
249 actor_role: &str,
250 ) -> Result<NoticeResponseDto, String> {
251 let mut notice = self
252 .notice_repo
253 .find_by_id(notice_id)
254 .await?
255 .ok_or("Notice not found".to_string())?;
256
257 let is_author = notice.author_id == user_id;
259 let is_admin = Self::is_building_admin(actor_role);
260
261 if !is_author && !is_admin {
262 return Err(
263 "Unauthorized: only author or building admin can archive notice".to_string(),
264 );
265 }
266
267 notice.archive()?;
269
270 let updated = self.notice_repo.update(¬ice).await?;
272
273 self.get_notice(updated.id).await
275 }
276
277 pub async fn pin_notice(
282 &self,
283 notice_id: Uuid,
284 actor_role: &str,
285 ) -> Result<NoticeResponseDto, String> {
286 if !Self::is_building_admin(actor_role) {
288 return Err(
289 "Unauthorized: only building admin (admin, superadmin, or syndic) can pin notices"
290 .to_string(),
291 );
292 }
293
294 let mut notice = self
295 .notice_repo
296 .find_by_id(notice_id)
297 .await?
298 .ok_or("Notice not found".to_string())?;
299
300 notice.pin()?;
302
303 let updated = self.notice_repo.update(¬ice).await?;
305
306 self.get_notice(updated.id).await
308 }
309
310 pub async fn unpin_notice(
315 &self,
316 notice_id: Uuid,
317 actor_role: &str,
318 ) -> Result<NoticeResponseDto, String> {
319 if !Self::is_building_admin(actor_role) {
321 return Err("Unauthorized: only building admin (admin, superadmin, or syndic) can unpin notices".to_string());
322 }
323
324 let mut notice = self
325 .notice_repo
326 .find_by_id(notice_id)
327 .await?
328 .ok_or("Notice not found".to_string())?;
329
330 notice.unpin()?;
332
333 let updated = self.notice_repo.update(¬ice).await?;
335
336 self.get_notice(updated.id).await
338 }
339
340 pub async fn set_expiration(
345 &self,
346 notice_id: Uuid,
347 user_id: Uuid,
348 _organization_id: Uuid,
349 dto: SetExpirationDto,
350 ) -> Result<NoticeResponseDto, String> {
351 let mut notice = self
352 .notice_repo
353 .find_by_id(notice_id)
354 .await?
355 .ok_or("Notice not found".to_string())?;
356
357 if notice.author_id != user_id {
359 return Err("Unauthorized: only author can set expiration".to_string());
360 }
361
362 notice.set_expiration(dto.expires_at)?;
364
365 let updated = self.notice_repo.update(¬ice).await?;
367
368 self.get_notice(updated.id).await
370 }
371
372 pub async fn delete_notice(
378 &self,
379 notice_id: Uuid,
380 user_id: Uuid,
381 _organization_id: Uuid,
382 ) -> Result<(), String> {
383 let notice = self
384 .notice_repo
385 .find_by_id(notice_id)
386 .await?
387 .ok_or("Notice not found".to_string())?;
388
389 if notice.author_id != user_id {
391 return Err("Unauthorized: only author can delete notice".to_string());
392 }
393
394 match notice.status {
396 NoticeStatus::Published | NoticeStatus::Archived => {
397 return Err(format!(
398 "Cannot delete notice in status {:?}. Archive it first.",
399 notice.status
400 ));
401 }
402 _ => {}
403 }
404
405 self.notice_repo.delete(notice_id).await?;
407
408 Ok(())
409 }
410
411 pub async fn auto_expire_notices(&self, building_id: Uuid) -> Result<Vec<Uuid>, String> {
418 let expired_notices = self.notice_repo.find_expired(building_id).await?;
419
420 let mut expired_ids = Vec::new();
421
422 for mut notice in expired_notices {
423 if let Err(e) = notice.expire() {
425 log::warn!("Failed to expire notice {}: {}. Skipping.", notice.id, e);
426 continue;
427 }
428
429 match self.notice_repo.update(¬ice).await {
431 Ok(_) => {
432 expired_ids.push(notice.id);
433 log::info!("Auto-expired notice: {}", notice.id);
434 }
435 Err(e) => {
436 log::error!("Failed to update expired notice {}: {}", notice.id, e);
437 }
438 }
439 }
440
441 Ok(expired_ids)
442 }
443
444 pub async fn get_statistics(&self, building_id: Uuid) -> Result<NoticeStatistics, String> {
446 let total_count = self.notice_repo.count_by_building(building_id).await?;
447 let published_count = self
448 .notice_repo
449 .count_published_by_building(building_id)
450 .await?;
451 let pinned_count = self
452 .notice_repo
453 .count_pinned_by_building(building_id)
454 .await?;
455
456 Ok(NoticeStatistics {
457 total_count,
458 published_count,
459 pinned_count,
460 })
461 }
462
463 async fn enrich_notices_summary(
465 &self,
466 notices: Vec<Notice>,
467 ) -> Result<Vec<NoticeSummaryDto>, String> {
468 let mut enriched = Vec::new();
469
470 for notice in notices {
471 let author_name = self.resolve_author_name(notice.author_id).await;
472 enriched.push(NoticeSummaryDto::from_notice(notice, author_name));
473 }
474
475 Ok(enriched)
476 }
477}
478
479#[derive(Debug, serde::Serialize)]
481pub struct NoticeStatistics {
482 pub total_count: i64,
483 pub published_count: i64,
484 pub pinned_count: i64,
485}
486
487#[cfg(test)]
488mod tests {
489 use super::*;
490 use crate::application::ports::{NoticeRepository, UserRepository};
491 use crate::domain::entities::{
492 Notice, NoticeCategory, NoticeStatus, NoticeType, User, UserRole,
493 };
494 use async_trait::async_trait;
495 use chrono::Utc;
496 use std::collections::HashMap;
497 use std::sync::Mutex;
498 use uuid::Uuid;
499
500 struct MockNoticeRepo {
503 notices: Mutex<HashMap<Uuid, Notice>>,
504 }
505
506 impl MockNoticeRepo {
507 fn new() -> Self {
508 Self {
509 notices: Mutex::new(HashMap::new()),
510 }
511 }
512
513 fn with_notice(notice: Notice) -> Self {
514 let mut map = HashMap::new();
515 map.insert(notice.id, notice);
516 Self {
517 notices: Mutex::new(map),
518 }
519 }
520 }
521
522 #[async_trait]
523 impl NoticeRepository for MockNoticeRepo {
524 async fn create(&self, notice: &Notice) -> Result<Notice, String> {
525 let mut store = self.notices.lock().unwrap();
526 store.insert(notice.id, notice.clone());
527 Ok(notice.clone())
528 }
529
530 async fn find_by_id(&self, id: Uuid) -> Result<Option<Notice>, String> {
531 let store = self.notices.lock().unwrap();
532 Ok(store.get(&id).cloned())
533 }
534
535 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Notice>, String> {
536 let store = self.notices.lock().unwrap();
537 Ok(store
538 .values()
539 .filter(|n| n.building_id == building_id)
540 .cloned()
541 .collect())
542 }
543
544 async fn find_published_by_building(
545 &self,
546 building_id: Uuid,
547 ) -> Result<Vec<Notice>, String> {
548 let store = self.notices.lock().unwrap();
549 Ok(store
550 .values()
551 .filter(|n| n.building_id == building_id && n.status == NoticeStatus::Published)
552 .cloned()
553 .collect())
554 }
555
556 async fn find_pinned_by_building(&self, building_id: Uuid) -> Result<Vec<Notice>, String> {
557 let store = self.notices.lock().unwrap();
558 Ok(store
559 .values()
560 .filter(|n| n.building_id == building_id && n.is_pinned)
561 .cloned()
562 .collect())
563 }
564
565 async fn find_by_type(
566 &self,
567 building_id: Uuid,
568 notice_type: NoticeType,
569 ) -> Result<Vec<Notice>, String> {
570 let store = self.notices.lock().unwrap();
571 Ok(store
572 .values()
573 .filter(|n| n.building_id == building_id && n.notice_type == notice_type)
574 .cloned()
575 .collect())
576 }
577
578 async fn find_by_category(
579 &self,
580 building_id: Uuid,
581 category: NoticeCategory,
582 ) -> Result<Vec<Notice>, String> {
583 let store = self.notices.lock().unwrap();
584 Ok(store
585 .values()
586 .filter(|n| n.building_id == building_id && n.category == category)
587 .cloned()
588 .collect())
589 }
590
591 async fn find_by_status(
592 &self,
593 building_id: Uuid,
594 status: NoticeStatus,
595 ) -> Result<Vec<Notice>, String> {
596 let store = self.notices.lock().unwrap();
597 Ok(store
598 .values()
599 .filter(|n| n.building_id == building_id && n.status == status)
600 .cloned()
601 .collect())
602 }
603
604 async fn find_by_author(&self, author_id: Uuid) -> Result<Vec<Notice>, String> {
605 let store = self.notices.lock().unwrap();
606 Ok(store
607 .values()
608 .filter(|n| n.author_id == author_id)
609 .cloned()
610 .collect())
611 }
612
613 async fn find_expired(&self, building_id: Uuid) -> Result<Vec<Notice>, String> {
614 let store = self.notices.lock().unwrap();
615 Ok(store
616 .values()
617 .filter(|n| {
618 n.building_id == building_id
619 && n.status == NoticeStatus::Published
620 && n.is_expired()
621 })
622 .cloned()
623 .collect())
624 }
625
626 async fn update(&self, notice: &Notice) -> Result<Notice, String> {
627 let mut store = self.notices.lock().unwrap();
628 store.insert(notice.id, notice.clone());
629 Ok(notice.clone())
630 }
631
632 async fn delete(&self, id: Uuid) -> Result<(), String> {
633 let mut store = self.notices.lock().unwrap();
634 store.remove(&id);
635 Ok(())
636 }
637
638 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
639 let store = self.notices.lock().unwrap();
640 Ok(store
641 .values()
642 .filter(|n| n.building_id == building_id)
643 .count() as i64)
644 }
645
646 async fn count_published_by_building(&self, building_id: Uuid) -> Result<i64, String> {
647 let store = self.notices.lock().unwrap();
648 Ok(store
649 .values()
650 .filter(|n| n.building_id == building_id && n.status == NoticeStatus::Published)
651 .count() as i64)
652 }
653
654 async fn count_pinned_by_building(&self, building_id: Uuid) -> Result<i64, String> {
655 let store = self.notices.lock().unwrap();
656 Ok(store
657 .values()
658 .filter(|n| n.building_id == building_id && n.is_pinned)
659 .count() as i64)
660 }
661 }
662
663 struct MockUserRepo {
666 users: Mutex<HashMap<Uuid, User>>,
667 }
668
669 impl MockUserRepo {
670 fn new() -> Self {
671 Self {
672 users: Mutex::new(HashMap::new()),
673 }
674 }
675
676 fn with_user(user: User) -> Self {
677 let mut map = HashMap::new();
678 map.insert(user.id, user);
679 Self {
680 users: Mutex::new(map),
681 }
682 }
683 }
684
685 #[async_trait]
686 impl UserRepository for MockUserRepo {
687 async fn create(&self, user: &User) -> Result<User, String> {
688 let mut store = self.users.lock().unwrap();
689 store.insert(user.id, user.clone());
690 Ok(user.clone())
691 }
692
693 async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, String> {
694 let store = self.users.lock().unwrap();
695 Ok(store.get(&id).cloned())
696 }
697
698 async fn find_by_email(&self, email: &str) -> Result<Option<User>, String> {
699 let store = self.users.lock().unwrap();
700 Ok(store.values().find(|u| u.email == email).cloned())
701 }
702
703 async fn find_all(&self) -> Result<Vec<User>, String> {
704 let store = self.users.lock().unwrap();
705 Ok(store.values().cloned().collect())
706 }
707
708 async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<User>, String> {
709 let store = self.users.lock().unwrap();
710 Ok(store
711 .values()
712 .filter(|u| u.organization_id == Some(org_id))
713 .cloned()
714 .collect())
715 }
716
717 async fn update(&self, user: &User) -> Result<User, String> {
718 let mut store = self.users.lock().unwrap();
719 store.insert(user.id, user.clone());
720 Ok(user.clone())
721 }
722
723 async fn delete(&self, id: Uuid) -> Result<bool, String> {
724 let mut store = self.users.lock().unwrap();
725 Ok(store.remove(&id).is_some())
726 }
727
728 async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String> {
729 let store = self.users.lock().unwrap();
730 Ok(store
731 .values()
732 .filter(|u| u.organization_id == Some(org_id))
733 .count() as i64)
734 }
735 }
736
737 fn make_user(id: Uuid) -> User {
740 User {
741 id,
742 email: format!("user-{}@example.com", &id.to_string()[..8]),
743 password_hash: "hash".to_string(),
744 first_name: "Jean".to_string(),
745 last_name: "Dupont".to_string(),
746 role: UserRole::Owner,
747 organization_id: Some(Uuid::new_v4()),
748 is_active: true,
749 processing_restricted: false,
750 processing_restricted_at: None,
751 marketing_opt_out: false,
752 marketing_opt_out_at: None,
753 created_at: Utc::now(),
754 updated_at: Utc::now(),
755 }
756 }
757
758 fn make_draft_notice(building_id: Uuid, author_id: Uuid) -> Notice {
759 Notice::new(
760 building_id,
761 author_id,
762 NoticeType::Announcement,
763 NoticeCategory::General,
764 "Test Notice Title".to_string(),
765 "This is a test notice content for unit testing.".to_string(),
766 None,
767 None,
768 None,
769 )
770 .unwrap()
771 }
772
773 fn make_published_notice(building_id: Uuid, author_id: Uuid) -> Notice {
774 let mut notice = make_draft_notice(building_id, author_id);
775 notice.publish().unwrap();
776 notice
777 }
778
779 #[tokio::test]
782 async fn test_create_notice_success() {
783 let user_id = Uuid::new_v4();
784 let org_id = Uuid::new_v4();
785 let building_id = Uuid::new_v4();
786 let user = make_user(user_id);
787
788 let uc = NoticeUseCases::new(
789 Arc::new(MockNoticeRepo::new()),
790 Arc::new(MockUserRepo::with_user(user)),
791 );
792
793 let dto = CreateNoticeDto {
794 building_id,
795 notice_type: NoticeType::Announcement,
796 category: NoticeCategory::General,
797 title: "Important Building Notice".to_string(),
798 content: "Please be aware of upcoming maintenance work.".to_string(),
799 event_date: None,
800 event_location: None,
801 contact_info: None,
802 expires_at: None,
803 };
804
805 let result = uc.create_notice(user_id, org_id, dto).await;
806 assert!(result.is_ok());
807 let resp = result.unwrap();
808 assert_eq!(resp.title, "Important Building Notice");
809 assert_eq!(resp.status, NoticeStatus::Draft);
810 assert_eq!(resp.author_id, user_id);
811 assert_eq!(resp.author_name, "Jean Dupont");
812 assert!(!resp.is_pinned);
813 }
814
815 #[tokio::test]
816 async fn test_get_notice_success() {
817 let user_id = Uuid::new_v4();
818 let building_id = Uuid::new_v4();
819 let notice = make_draft_notice(building_id, user_id);
820 let notice_id = notice.id;
821 let user = make_user(user_id);
822
823 let uc = NoticeUseCases::new(
824 Arc::new(MockNoticeRepo::with_notice(notice)),
825 Arc::new(MockUserRepo::with_user(user)),
826 );
827
828 let result = uc.get_notice(notice_id).await;
829 assert!(result.is_ok());
830 let resp = result.unwrap();
831 assert_eq!(resp.id, notice_id);
832 assert_eq!(resp.author_name, "Jean Dupont");
833 }
834
835 #[tokio::test]
836 async fn test_get_notice_not_found() {
837 let uc = NoticeUseCases::new(
838 Arc::new(MockNoticeRepo::new()),
839 Arc::new(MockUserRepo::new()),
840 );
841
842 let result = uc.get_notice(Uuid::new_v4()).await;
843 assert!(result.is_err());
844 assert_eq!(result.unwrap_err(), "Notice not found");
845 }
846
847 #[tokio::test]
848 async fn test_publish_notice_success() {
849 let user_id = Uuid::new_v4();
850 let org_id = Uuid::new_v4();
851 let building_id = Uuid::new_v4();
852 let notice = make_draft_notice(building_id, user_id);
853 let notice_id = notice.id;
854 let user = make_user(user_id);
855
856 let uc = NoticeUseCases::new(
857 Arc::new(MockNoticeRepo::with_notice(notice)),
858 Arc::new(MockUserRepo::with_user(user)),
859 );
860
861 let result = uc.publish_notice(notice_id, user_id, org_id).await;
862 assert!(result.is_ok());
863 let resp = result.unwrap();
864 assert_eq!(resp.status, NoticeStatus::Published);
865 assert!(resp.published_at.is_some());
866 }
867
868 #[tokio::test]
869 async fn test_publish_notice_unauthorized() {
870 let author_id = Uuid::new_v4();
871 let other_user_id = Uuid::new_v4();
872 let org_id = Uuid::new_v4();
873 let building_id = Uuid::new_v4();
874 let notice = make_draft_notice(building_id, author_id);
875 let notice_id = notice.id;
876
877 let uc = NoticeUseCases::new(
878 Arc::new(MockNoticeRepo::with_notice(notice)),
879 Arc::new(MockUserRepo::new()),
880 );
881
882 let result = uc.publish_notice(notice_id, other_user_id, org_id).await;
883 assert!(result.is_err());
884 assert!(result
885 .unwrap_err()
886 .contains("Unauthorized: only author can publish notice"));
887 }
888
889 #[tokio::test]
890 async fn test_archive_notice_by_author() {
891 let user_id = Uuid::new_v4();
892 let org_id = Uuid::new_v4();
893 let building_id = Uuid::new_v4();
894 let notice = make_published_notice(building_id, user_id);
895 let notice_id = notice.id;
896 let user = make_user(user_id);
897
898 let uc = NoticeUseCases::new(
899 Arc::new(MockNoticeRepo::with_notice(notice)),
900 Arc::new(MockUserRepo::with_user(user)),
901 );
902
903 let result = uc.archive_notice(notice_id, user_id, org_id, "owner").await;
904 assert!(result.is_ok());
905 let resp = result.unwrap();
906 assert_eq!(resp.status, NoticeStatus::Archived);
907 assert!(resp.archived_at.is_some());
908 }
909
910 #[tokio::test]
911 async fn test_archive_notice_by_admin() {
912 let author_id = Uuid::new_v4();
913 let admin_id = Uuid::new_v4();
914 let org_id = Uuid::new_v4();
915 let building_id = Uuid::new_v4();
916 let notice = make_published_notice(building_id, author_id);
917 let notice_id = notice.id;
918 let admin_user = make_user(admin_id);
919
920 let uc = NoticeUseCases::new(
921 Arc::new(MockNoticeRepo::with_notice(notice)),
922 Arc::new(MockUserRepo::with_user(admin_user)),
923 );
924
925 let result = uc
927 .archive_notice(notice_id, admin_id, org_id, "admin")
928 .await;
929 assert!(result.is_ok());
930 assert_eq!(result.unwrap().status, NoticeStatus::Archived);
931 }
932
933 #[tokio::test]
934 async fn test_archive_notice_unauthorized_non_author_non_admin() {
935 let author_id = Uuid::new_v4();
936 let other_user_id = Uuid::new_v4();
937 let org_id = Uuid::new_v4();
938 let building_id = Uuid::new_v4();
939 let notice = make_published_notice(building_id, author_id);
940 let notice_id = notice.id;
941
942 let uc = NoticeUseCases::new(
943 Arc::new(MockNoticeRepo::with_notice(notice)),
944 Arc::new(MockUserRepo::new()),
945 );
946
947 let result = uc
948 .archive_notice(notice_id, other_user_id, org_id, "owner")
949 .await;
950 assert!(result.is_err());
951 assert!(result
952 .unwrap_err()
953 .contains("Unauthorized: only author or building admin can archive notice"));
954 }
955
956 #[tokio::test]
957 async fn test_pin_notice_admin_success() {
958 let user_id = Uuid::new_v4();
959 let building_id = Uuid::new_v4();
960 let notice = make_published_notice(building_id, user_id);
961 let notice_id = notice.id;
962 let user = make_user(user_id);
963
964 let uc = NoticeUseCases::new(
965 Arc::new(MockNoticeRepo::with_notice(notice)),
966 Arc::new(MockUserRepo::with_user(user)),
967 );
968
969 let result = uc.pin_notice(notice_id, "syndic").await;
970 assert!(result.is_ok());
971 assert!(result.unwrap().is_pinned);
972 }
973
974 #[tokio::test]
975 async fn test_pin_notice_unauthorized_owner() {
976 let user_id = Uuid::new_v4();
977 let building_id = Uuid::new_v4();
978 let notice = make_published_notice(building_id, user_id);
979 let notice_id = notice.id;
980
981 let uc = NoticeUseCases::new(
982 Arc::new(MockNoticeRepo::with_notice(notice)),
983 Arc::new(MockUserRepo::new()),
984 );
985
986 let result = uc.pin_notice(notice_id, "owner").await;
987 assert!(result.is_err());
988 assert!(result
989 .unwrap_err()
990 .contains("Unauthorized: only building admin"));
991 }
992
993 #[tokio::test]
994 async fn test_delete_notice_success_draft() {
995 let user_id = Uuid::new_v4();
996 let org_id = Uuid::new_v4();
997 let building_id = Uuid::new_v4();
998 let notice = make_draft_notice(building_id, user_id);
999 let notice_id = notice.id;
1000
1001 let uc = NoticeUseCases::new(
1002 Arc::new(MockNoticeRepo::with_notice(notice)),
1003 Arc::new(MockUserRepo::new()),
1004 );
1005
1006 let result = uc.delete_notice(notice_id, user_id, org_id).await;
1007 assert!(result.is_ok());
1008 }
1009
1010 #[tokio::test]
1011 async fn test_delete_notice_blocked_for_published() {
1012 let user_id = Uuid::new_v4();
1013 let org_id = Uuid::new_v4();
1014 let building_id = Uuid::new_v4();
1015 let notice = make_published_notice(building_id, user_id);
1016 let notice_id = notice.id;
1017
1018 let uc = NoticeUseCases::new(
1019 Arc::new(MockNoticeRepo::with_notice(notice)),
1020 Arc::new(MockUserRepo::new()),
1021 );
1022
1023 let result = uc.delete_notice(notice_id, user_id, org_id).await;
1024 assert!(result.is_err());
1025 assert!(result
1026 .unwrap_err()
1027 .contains("Cannot delete notice in status"));
1028 }
1029}