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 {
26 role == "admin" || role == "superadmin" || role == "syndic" || role == "community.moderator"
27 }
28
29 async fn resolve_author_name(&self, user_id: Uuid) -> String {
31 match self.user_repo.find_by_id(user_id).await {
32 Ok(Some(user)) => format!("{} {}", user.first_name, user.last_name),
33 _ => "Unknown Author".to_string(),
34 }
35 }
36
37 pub async fn create_notice(
43 &self,
44 user_id: Uuid,
45 _organization_id: Uuid,
46 dto: CreateNoticeDto,
47 ) -> Result<NoticeResponseDto, String> {
48 let notice = Notice::new(
50 dto.building_id,
51 user_id,
52 dto.notice_type,
53 dto.category,
54 dto.title,
55 dto.content,
56 dto.event_date,
57 dto.event_location,
58 dto.contact_info,
59 )?;
60
61 let mut notice = notice;
63 if let Some(expires_at) = dto.expires_at {
64 notice.set_expiration(Some(expires_at))?;
65 }
66
67 let created = self.notice_repo.create(¬ice).await?;
69
70 let author_name = self.resolve_author_name(user_id).await;
71 Ok(NoticeResponseDto::from_notice(created, author_name))
72 }
73
74 pub async fn get_notice(&self, notice_id: Uuid) -> Result<NoticeResponseDto, String> {
76 let notice = self
77 .notice_repo
78 .find_by_id(notice_id)
79 .await?
80 .ok_or("Notice not found".to_string())?;
81
82 let author_name = self.resolve_author_name(notice.author_id).await;
83 Ok(NoticeResponseDto::from_notice(notice, author_name))
84 }
85
86 pub async fn list_building_notices(
91 &self,
92 building_id: Uuid,
93 ) -> Result<Vec<NoticeSummaryDto>, String> {
94 let notices = self.notice_repo.find_by_building(building_id).await?;
95 self.enrich_notices_summary(notices).await
96 }
97
98 pub async fn list_published_notices(
103 &self,
104 building_id: Uuid,
105 ) -> Result<Vec<NoticeSummaryDto>, String> {
106 let notices = self
107 .notice_repo
108 .find_published_by_building(building_id)
109 .await?;
110 self.enrich_notices_summary(notices).await
111 }
112
113 pub async fn list_pinned_notices(
115 &self,
116 building_id: Uuid,
117 ) -> Result<Vec<NoticeSummaryDto>, String> {
118 let notices = self
119 .notice_repo
120 .find_pinned_by_building(building_id)
121 .await?;
122 self.enrich_notices_summary(notices).await
123 }
124
125 pub async fn list_notices_by_type(
127 &self,
128 building_id: Uuid,
129 notice_type: NoticeType,
130 ) -> Result<Vec<NoticeSummaryDto>, String> {
131 let notices = self
132 .notice_repo
133 .find_by_type(building_id, notice_type)
134 .await?;
135 self.enrich_notices_summary(notices).await
136 }
137
138 pub async fn list_notices_by_category(
140 &self,
141 building_id: Uuid,
142 category: NoticeCategory,
143 ) -> Result<Vec<NoticeSummaryDto>, String> {
144 let notices = self
145 .notice_repo
146 .find_by_category(building_id, category)
147 .await?;
148 self.enrich_notices_summary(notices).await
149 }
150
151 pub async fn list_notices_by_status(
153 &self,
154 building_id: Uuid,
155 status: NoticeStatus,
156 ) -> Result<Vec<NoticeSummaryDto>, String> {
157 let notices = self.notice_repo.find_by_status(building_id, status).await?;
158 self.enrich_notices_summary(notices).await
159 }
160
161 pub async fn list_author_notices(
163 &self,
164 author_id: Uuid,
165 ) -> Result<Vec<NoticeSummaryDto>, String> {
166 let notices = self.notice_repo.find_by_author(author_id).await?;
167 self.enrich_notices_summary(notices).await
168 }
169
170 pub async fn update_notice(
176 &self,
177 notice_id: Uuid,
178 user_id: Uuid,
179 _organization_id: Uuid,
180 dto: UpdateNoticeDto,
181 ) -> Result<NoticeResponseDto, String> {
182 let mut notice = self
183 .notice_repo
184 .find_by_id(notice_id)
185 .await?
186 .ok_or("Notice not found".to_string())?;
187
188 if notice.author_id != user_id {
190 return Err("Unauthorized: only author can update notice".to_string());
191 }
192
193 notice.update_content(
195 dto.title,
196 dto.content,
197 dto.category,
198 dto.event_date,
199 dto.event_location,
200 dto.contact_info,
201 dto.expires_at,
202 )?;
203
204 let updated = self.notice_repo.update(¬ice).await?;
206
207 self.get_notice(updated.id).await
209 }
210
211 pub async fn publish_notice(
216 &self,
217 notice_id: Uuid,
218 user_id: Uuid,
219 _organization_id: Uuid,
220 ) -> Result<NoticeResponseDto, String> {
221 let mut notice = self
222 .notice_repo
223 .find_by_id(notice_id)
224 .await?
225 .ok_or("Notice not found".to_string())?;
226
227 if notice.author_id != user_id {
229 return Err("Unauthorized: only author can publish notice".to_string());
230 }
231
232 notice.publish()?;
234
235 let updated = self.notice_repo.update(¬ice).await?;
237
238 self.get_notice(updated.id).await
240 }
241
242 pub async fn archive_notice(
250 &self,
251 notice_id: Uuid,
252 user_id: Uuid,
253 _organization_id: Uuid,
254 actor_role: &str,
255 reason: Option<String>,
256 ) -> Result<NoticeResponseDto, String> {
257 let mut notice = self
258 .notice_repo
259 .find_by_id(notice_id)
260 .await?
261 .ok_or("Notice not found".to_string())?;
262
263 let is_author = notice.author_id == user_id;
265 let is_admin = Self::is_building_admin(actor_role);
266
267 if !is_author && !is_admin {
268 return Err(
269 "Unauthorized: only author or building admin can archive notice".to_string(),
270 );
271 }
272
273 if !is_author && is_admin {
275 reason
276 .filter(|r| !r.trim().is_empty())
277 .ok_or_else(|| crate::application::error::MOTIF_MODERATION_REQUIS.to_string())?;
278 }
279
280 notice.archive()?;
282
283 let updated = self.notice_repo.update(¬ice).await?;
285
286 self.get_notice(updated.id).await
288 }
289
290 pub async fn pin_notice(
295 &self,
296 notice_id: Uuid,
297 actor_role: &str,
298 ) -> Result<NoticeResponseDto, String> {
299 if !Self::is_building_admin(actor_role) {
301 return Err(
302 "Unauthorized: only building admin (admin, superadmin, or syndic) can pin notices"
303 .to_string(),
304 );
305 }
306
307 let mut notice = self
308 .notice_repo
309 .find_by_id(notice_id)
310 .await?
311 .ok_or("Notice not found".to_string())?;
312
313 notice.pin()?;
315
316 let updated = self.notice_repo.update(¬ice).await?;
318
319 self.get_notice(updated.id).await
321 }
322
323 pub async fn unpin_notice(
328 &self,
329 notice_id: Uuid,
330 actor_role: &str,
331 ) -> Result<NoticeResponseDto, String> {
332 if !Self::is_building_admin(actor_role) {
334 return Err("Unauthorized: only building admin (admin, superadmin, or syndic) can unpin notices".to_string());
335 }
336
337 let mut notice = self
338 .notice_repo
339 .find_by_id(notice_id)
340 .await?
341 .ok_or("Notice not found".to_string())?;
342
343 notice.unpin()?;
345
346 let updated = self.notice_repo.update(¬ice).await?;
348
349 self.get_notice(updated.id).await
351 }
352
353 pub async fn set_expiration(
358 &self,
359 notice_id: Uuid,
360 user_id: Uuid,
361 _organization_id: Uuid,
362 dto: SetExpirationDto,
363 ) -> Result<NoticeResponseDto, String> {
364 let mut notice = self
365 .notice_repo
366 .find_by_id(notice_id)
367 .await?
368 .ok_or("Notice not found".to_string())?;
369
370 if notice.author_id != user_id {
372 return Err("Unauthorized: only author can set expiration".to_string());
373 }
374
375 notice.set_expiration(dto.expires_at)?;
377
378 let updated = self.notice_repo.update(¬ice).await?;
380
381 self.get_notice(updated.id).await
383 }
384
385 pub async fn delete_notice(
391 &self,
392 notice_id: Uuid,
393 user_id: Uuid,
394 _organization_id: Uuid,
395 ) -> Result<(), String> {
396 let notice = self
397 .notice_repo
398 .find_by_id(notice_id)
399 .await?
400 .ok_or("Notice not found".to_string())?;
401
402 if notice.author_id != user_id {
404 return Err("Unauthorized: only author can delete notice".to_string());
405 }
406
407 match notice.status {
409 NoticeStatus::Published | NoticeStatus::Archived => {
410 return Err(format!(
411 "Cannot delete notice in status {:?}. Archive it first.",
412 notice.status
413 ));
414 }
415 _ => {}
416 }
417
418 self.notice_repo.delete(notice_id).await?;
420
421 Ok(())
422 }
423
424 pub async fn auto_expire_notices(&self, building_id: Uuid) -> Result<Vec<Uuid>, String> {
431 let expired_notices = self.notice_repo.find_expired(building_id).await?;
432
433 let mut expired_ids = Vec::new();
434
435 for mut notice in expired_notices {
436 if let Err(e) = notice.expire() {
438 log::warn!("Failed to expire notice {}: {}. Skipping.", notice.id, e);
439 continue;
440 }
441
442 match self.notice_repo.update(¬ice).await {
444 Ok(_) => {
445 expired_ids.push(notice.id);
446 log::info!("Auto-expired notice: {}", notice.id);
447 }
448 Err(e) => {
449 log::error!("Failed to update expired notice {}: {}", notice.id, e);
450 }
451 }
452 }
453
454 Ok(expired_ids)
455 }
456
457 pub async fn get_statistics(&self, building_id: Uuid) -> Result<NoticeStatistics, String> {
459 let total_count = self.notice_repo.count_by_building(building_id).await?;
460 let published_count = self
461 .notice_repo
462 .count_published_by_building(building_id)
463 .await?;
464 let pinned_count = self
465 .notice_repo
466 .count_pinned_by_building(building_id)
467 .await?;
468
469 Ok(NoticeStatistics {
470 total_count,
471 published_count,
472 pinned_count,
473 })
474 }
475
476 async fn enrich_notices_summary(
478 &self,
479 notices: Vec<Notice>,
480 ) -> Result<Vec<NoticeSummaryDto>, String> {
481 let mut enriched = Vec::new();
482
483 for notice in notices {
484 let author_name = self.resolve_author_name(notice.author_id).await;
485 enriched.push(NoticeSummaryDto::from_notice(notice, author_name));
486 }
487
488 Ok(enriched)
489 }
490}
491
492#[derive(Debug, serde::Serialize)]
494pub struct NoticeStatistics {
495 pub total_count: i64,
496 pub published_count: i64,
497 pub pinned_count: i64,
498}
499
500#[cfg(test)]
501mod tests {
502 use super::*;
503 use crate::application::ports::{NoticeRepository, UserRepository};
504 use crate::domain::entities::{
505 Notice, NoticeCategory, NoticeStatus, NoticeType, User, UserRole,
506 };
507 use async_trait::async_trait;
508 use chrono::Utc;
509 use std::collections::HashMap;
510 use std::sync::Mutex;
511 use uuid::Uuid;
512
513 struct MockNoticeRepo {
516 notices: Mutex<HashMap<Uuid, Notice>>,
517 }
518
519 impl MockNoticeRepo {
520 fn new() -> Self {
521 Self {
522 notices: Mutex::new(HashMap::new()),
523 }
524 }
525
526 fn with_notice(notice: Notice) -> Self {
527 let mut map = HashMap::new();
528 map.insert(notice.id, notice);
529 Self {
530 notices: Mutex::new(map),
531 }
532 }
533 }
534
535 #[async_trait]
536 impl NoticeRepository for MockNoticeRepo {
537 async fn create(&self, notice: &Notice) -> Result<Notice, String> {
538 let mut store = self.notices.lock().unwrap();
539 store.insert(notice.id, notice.clone());
540 Ok(notice.clone())
541 }
542
543 async fn find_by_id(&self, id: Uuid) -> Result<Option<Notice>, String> {
544 let store = self.notices.lock().unwrap();
545 Ok(store.get(&id).cloned())
546 }
547
548 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Notice>, String> {
549 let store = self.notices.lock().unwrap();
550 Ok(store
551 .values()
552 .filter(|n| n.building_id == building_id)
553 .cloned()
554 .collect())
555 }
556
557 async fn find_published_by_building(
558 &self,
559 building_id: Uuid,
560 ) -> Result<Vec<Notice>, String> {
561 let store = self.notices.lock().unwrap();
562 Ok(store
563 .values()
564 .filter(|n| n.building_id == building_id && n.status == NoticeStatus::Published)
565 .cloned()
566 .collect())
567 }
568
569 async fn find_pinned_by_building(&self, building_id: Uuid) -> 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.is_pinned)
574 .cloned()
575 .collect())
576 }
577
578 async fn find_by_type(
579 &self,
580 building_id: Uuid,
581 notice_type: NoticeType,
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.notice_type == notice_type)
587 .cloned()
588 .collect())
589 }
590
591 async fn find_by_category(
592 &self,
593 building_id: Uuid,
594 category: NoticeCategory,
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.category == category)
600 .cloned()
601 .collect())
602 }
603
604 async fn find_by_status(
605 &self,
606 building_id: Uuid,
607 status: NoticeStatus,
608 ) -> Result<Vec<Notice>, String> {
609 let store = self.notices.lock().unwrap();
610 Ok(store
611 .values()
612 .filter(|n| n.building_id == building_id && n.status == status)
613 .cloned()
614 .collect())
615 }
616
617 async fn find_by_author(&self, author_id: Uuid) -> Result<Vec<Notice>, String> {
618 let store = self.notices.lock().unwrap();
619 Ok(store
620 .values()
621 .filter(|n| n.author_id == author_id)
622 .cloned()
623 .collect())
624 }
625
626 async fn find_expired(&self, building_id: Uuid) -> Result<Vec<Notice>, String> {
627 let store = self.notices.lock().unwrap();
628 Ok(store
629 .values()
630 .filter(|n| {
631 n.building_id == building_id
632 && n.status == NoticeStatus::Published
633 && n.is_expired()
634 })
635 .cloned()
636 .collect())
637 }
638
639 async fn update(&self, notice: &Notice) -> Result<Notice, String> {
640 let mut store = self.notices.lock().unwrap();
641 store.insert(notice.id, notice.clone());
642 Ok(notice.clone())
643 }
644
645 async fn delete(&self, id: Uuid) -> Result<(), String> {
646 let mut store = self.notices.lock().unwrap();
647 store.remove(&id);
648 Ok(())
649 }
650
651 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
652 let store = self.notices.lock().unwrap();
653 Ok(store
654 .values()
655 .filter(|n| n.building_id == building_id)
656 .count() as i64)
657 }
658
659 async fn count_published_by_building(&self, building_id: Uuid) -> Result<i64, String> {
660 let store = self.notices.lock().unwrap();
661 Ok(store
662 .values()
663 .filter(|n| n.building_id == building_id && n.status == NoticeStatus::Published)
664 .count() as i64)
665 }
666
667 async fn count_pinned_by_building(&self, building_id: Uuid) -> Result<i64, String> {
668 let store = self.notices.lock().unwrap();
669 Ok(store
670 .values()
671 .filter(|n| n.building_id == building_id && n.is_pinned)
672 .count() as i64)
673 }
674 }
675
676 struct MockUserRepo {
679 users: Mutex<HashMap<Uuid, User>>,
680 }
681
682 impl MockUserRepo {
683 fn filtrer(
687 store: &HashMap<Uuid, User>,
688 recherche: Option<String>,
689 role: Option<String>,
690 ) -> Vec<User> {
691 let terme = recherche
692 .map(|r| r.trim().to_lowercase())
693 .filter(|r| !r.is_empty());
694 let role = role
695 .map(|r| r.trim().to_string())
696 .filter(|r| !r.is_empty() && r != "all");
697
698 let mut retenus: Vec<User> = store
699 .values()
700 .filter(|u| match &terme {
701 None => true,
702 Some(t) => {
703 u.email.to_lowercase().contains(t)
704 || u.first_name.to_lowercase().contains(t)
705 || u.last_name.to_lowercase().contains(t)
706 }
707 })
708 .filter(|u| match &role {
709 None => true,
710 Some(r) => u.role.to_string() == *r,
711 })
712 .cloned()
713 .collect();
714 retenus.sort_by_key(|u| std::cmp::Reverse(u.created_at));
717 retenus
718 }
719
720 fn new() -> Self {
721 Self {
722 users: Mutex::new(HashMap::new()),
723 }
724 }
725
726 fn with_user(user: User) -> Self {
727 let mut map = HashMap::new();
728 map.insert(user.id, user);
729 Self {
730 users: Mutex::new(map),
731 }
732 }
733 }
734
735 #[async_trait]
736 impl UserRepository for MockUserRepo {
737 async fn create(&self, user: &User) -> Result<User, String> {
738 let mut store = self.users.lock().unwrap();
739 store.insert(user.id, user.clone());
740 Ok(user.clone())
741 }
742
743 async fn find_page(
747 &self,
748 recherche: Option<String>,
749 role: Option<String>,
750 limit: i64,
751 offset: i64,
752 ) -> Result<Vec<User>, String> {
753 let retenus = Self::filtrer(&self.users.lock().unwrap(), recherche, role);
754 Ok(retenus
755 .into_iter()
756 .skip(offset.max(0) as usize)
757 .take(limit.max(0) as usize)
758 .collect())
759 }
760
761 async fn count_matching(
762 &self,
763 recherche: Option<String>,
764 role: Option<String>,
765 ) -> Result<i64, String> {
766 let retenus = Self::filtrer(&self.users.lock().unwrap(), recherche, role);
767 Ok(retenus.len() as i64)
768 }
769
770 async fn find_by_id(&self, id: Uuid) -> Result<Option<User>, String> {
771 let store = self.users.lock().unwrap();
772 Ok(store.get(&id).cloned())
773 }
774
775 async fn find_by_email(&self, email: &str) -> Result<Option<User>, String> {
776 let store = self.users.lock().unwrap();
777 Ok(store.values().find(|u| u.email == email).cloned())
778 }
779
780 async fn find_all(&self) -> Result<Vec<User>, String> {
781 let store = self.users.lock().unwrap();
782 Ok(store.values().cloned().collect())
783 }
784
785 async fn find_by_organization(&self, org_id: Uuid) -> Result<Vec<User>, String> {
786 let store = self.users.lock().unwrap();
787 Ok(store
788 .values()
789 .filter(|u| u.organization_id == Some(org_id))
790 .cloned()
791 .collect())
792 }
793
794 async fn update(&self, user: &User) -> Result<User, String> {
795 let mut store = self.users.lock().unwrap();
796 store.insert(user.id, user.clone());
797 Ok(user.clone())
798 }
799
800 async fn update_password(&self, _id: Uuid, _h: &str) -> Result<bool, String> {
801 Ok(true)
802 }
803
804 async fn activate(&self, id: Uuid) -> Result<Option<User>, String> {
805 let mut store = self.users.lock().unwrap();
806 if let Some(u) = store.get_mut(&id) {
807 u.is_active = true;
808 return Ok(Some(u.clone()));
809 }
810 Ok(None)
811 }
812
813 async fn deactivate(&self, id: Uuid) -> Result<Option<User>, String> {
814 let mut store = self.users.lock().unwrap();
815 if let Some(u) = store.get_mut(&id) {
816 u.is_active = false;
817 return Ok(Some(u.clone()));
818 }
819 Ok(None)
820 }
821
822 async fn delete(&self, id: Uuid) -> Result<bool, String> {
823 let mut store = self.users.lock().unwrap();
824 Ok(store.remove(&id).is_some())
825 }
826
827 async fn count_by_organization(&self, org_id: Uuid) -> Result<i64, String> {
828 let store = self.users.lock().unwrap();
829 Ok(store
830 .values()
831 .filter(|u| u.organization_id == Some(org_id))
832 .count() as i64)
833 }
834 }
835
836 fn make_user(id: Uuid) -> User {
839 User {
840 id,
841 email: format!("user-{}@example.com", &id.to_string()[..8]),
842 password_hash: "hash".to_string(),
843 first_name: "Jean".to_string(),
844 last_name: "Dupont".to_string(),
845 role: UserRole::Owner,
846 organization_id: Some(Uuid::new_v4()),
847 is_active: true,
848 processing_restricted: false,
849 processing_restricted_at: None,
850 marketing_opt_out: false,
851 marketing_opt_out_at: None,
852 created_at: Utc::now(),
853 updated_at: Utc::now(),
854 }
855 }
856
857 fn make_draft_notice(building_id: Uuid, author_id: Uuid) -> Notice {
858 Notice::new(
859 building_id,
860 author_id,
861 NoticeType::Announcement,
862 NoticeCategory::General,
863 "Test Notice Title".to_string(),
864 "This is a test notice content for unit testing.".to_string(),
865 None,
866 None,
867 None,
868 )
869 .unwrap()
870 }
871
872 fn make_published_notice(building_id: Uuid, author_id: Uuid) -> Notice {
873 let mut notice = make_draft_notice(building_id, author_id);
874 notice.publish().unwrap();
875 notice
876 }
877
878 #[tokio::test]
881 async fn test_create_notice_success() {
882 let user_id = Uuid::new_v4();
883 let org_id = Uuid::new_v4();
884 let building_id = Uuid::new_v4();
885 let user = make_user(user_id);
886
887 let uc = NoticeUseCases::new(
888 Arc::new(MockNoticeRepo::new()),
889 Arc::new(MockUserRepo::with_user(user)),
890 );
891
892 let dto = CreateNoticeDto {
893 building_id,
894 notice_type: NoticeType::Announcement,
895 category: NoticeCategory::General,
896 title: "Important Building Notice".to_string(),
897 content: "Please be aware of upcoming maintenance work.".to_string(),
898 event_date: None,
899 event_location: None,
900 contact_info: None,
901 expires_at: None,
902 };
903
904 let result = uc.create_notice(user_id, org_id, dto).await;
905 assert!(result.is_ok());
906 let resp = result.unwrap();
907 assert_eq!(resp.title, "Important Building Notice");
908 assert_eq!(resp.status, NoticeStatus::Draft);
909 assert_eq!(resp.author_id, user_id);
910 assert_eq!(resp.author_name, "Jean Dupont");
911 assert!(!resp.is_pinned);
912 }
913
914 #[tokio::test]
925 async fn happy_syndic_sans_fiche_coproprietaire_peut_creer_une_annonce() {
926 let user_id = Uuid::new_v4(); let org_id = Uuid::new_v4();
928 let building_id = Uuid::new_v4();
929 let mut syndic = make_user(user_id);
930 syndic.role = UserRole::Syndic;
931
932 let uc = NoticeUseCases::new(
933 Arc::new(MockNoticeRepo::new()),
934 Arc::new(MockUserRepo::with_user(syndic)),
935 );
936
937 let dto = CreateNoticeDto {
938 building_id,
939 notice_type: NoticeType::Announcement,
940 category: NoticeCategory::General,
941 title: "Entretien des communs".to_string(),
942 content: "Les communs seront entretenus la semaine prochaine.".to_string(),
943 event_date: None,
944 event_location: None,
945 contact_info: None,
946 expires_at: None,
947 };
948
949 let result = uc.create_notice(user_id, org_id, dto).await;
950 assert!(
951 result.is_ok(),
952 "un syndic sans fiche de copropriétaire doit pouvoir créer une \
953 annonce : {:?}",
954 result.err()
955 );
956 assert_eq!(result.unwrap().author_id, user_id);
957 }
958
959 #[tokio::test]
960 async fn test_get_notice_success() {
961 let user_id = Uuid::new_v4();
962 let building_id = Uuid::new_v4();
963 let notice = make_draft_notice(building_id, user_id);
964 let notice_id = notice.id;
965 let user = make_user(user_id);
966
967 let uc = NoticeUseCases::new(
968 Arc::new(MockNoticeRepo::with_notice(notice)),
969 Arc::new(MockUserRepo::with_user(user)),
970 );
971
972 let result = uc.get_notice(notice_id).await;
973 assert!(result.is_ok());
974 let resp = result.unwrap();
975 assert_eq!(resp.id, notice_id);
976 assert_eq!(resp.author_name, "Jean Dupont");
977 }
978
979 #[tokio::test]
980 async fn test_get_notice_not_found() {
981 let uc = NoticeUseCases::new(
982 Arc::new(MockNoticeRepo::new()),
983 Arc::new(MockUserRepo::new()),
984 );
985
986 let result = uc.get_notice(Uuid::new_v4()).await;
987 assert!(result.is_err());
988 assert_eq!(result.unwrap_err(), "Notice not found");
989 }
990
991 #[tokio::test]
992 async fn test_publish_notice_success() {
993 let user_id = Uuid::new_v4();
994 let org_id = Uuid::new_v4();
995 let building_id = Uuid::new_v4();
996 let notice = make_draft_notice(building_id, user_id);
997 let notice_id = notice.id;
998 let user = make_user(user_id);
999
1000 let uc = NoticeUseCases::new(
1001 Arc::new(MockNoticeRepo::with_notice(notice)),
1002 Arc::new(MockUserRepo::with_user(user)),
1003 );
1004
1005 let result = uc.publish_notice(notice_id, user_id, org_id).await;
1006 assert!(result.is_ok());
1007 let resp = result.unwrap();
1008 assert_eq!(resp.status, NoticeStatus::Published);
1009 assert!(resp.published_at.is_some());
1010 }
1011
1012 #[tokio::test]
1013 async fn test_publish_notice_unauthorized() {
1014 let author_id = Uuid::new_v4();
1015 let other_user_id = Uuid::new_v4();
1016 let org_id = Uuid::new_v4();
1017 let building_id = Uuid::new_v4();
1018 let notice = make_draft_notice(building_id, author_id);
1019 let notice_id = notice.id;
1020
1021 let uc = NoticeUseCases::new(
1022 Arc::new(MockNoticeRepo::with_notice(notice)),
1023 Arc::new(MockUserRepo::new()),
1024 );
1025
1026 let result = uc.publish_notice(notice_id, other_user_id, org_id).await;
1027 assert!(result.is_err());
1028 assert!(result
1029 .unwrap_err()
1030 .contains("Unauthorized: only author can publish notice"));
1031 }
1032
1033 #[tokio::test]
1034 async fn test_archive_notice_by_author() {
1035 let user_id = Uuid::new_v4();
1036 let org_id = Uuid::new_v4();
1037 let building_id = Uuid::new_v4();
1038 let notice = make_published_notice(building_id, user_id);
1039 let notice_id = notice.id;
1040 let user = make_user(user_id);
1041
1042 let uc = NoticeUseCases::new(
1043 Arc::new(MockNoticeRepo::with_notice(notice)),
1044 Arc::new(MockUserRepo::with_user(user)),
1045 );
1046
1047 let result = uc
1048 .archive_notice(notice_id, user_id, org_id, "owner", None)
1049 .await;
1050 assert!(result.is_ok());
1051 let resp = result.unwrap();
1052 assert_eq!(resp.status, NoticeStatus::Archived);
1053 assert!(resp.archived_at.is_some());
1054 }
1055
1056 #[tokio::test]
1057 async fn test_archive_notice_by_admin() {
1058 let author_id = Uuid::new_v4();
1059 let admin_id = Uuid::new_v4();
1060 let org_id = Uuid::new_v4();
1061 let building_id = Uuid::new_v4();
1062 let notice = make_published_notice(building_id, author_id);
1063 let notice_id = notice.id;
1064 let admin_user = make_user(admin_id);
1065
1066 let uc = NoticeUseCases::new(
1067 Arc::new(MockNoticeRepo::with_notice(notice)),
1068 Arc::new(MockUserRepo::with_user(admin_user)),
1069 );
1070
1071 let result = uc
1076 .archive_notice(
1077 notice_id,
1078 admin_id,
1079 org_id,
1080 "admin",
1081 Some("Contenu obsolète signalé".to_string()),
1082 )
1083 .await;
1084 assert!(result.is_ok());
1085 assert_eq!(result.unwrap().status, NoticeStatus::Archived);
1086 }
1087
1088 #[tokio::test]
1089 async fn test_archive_notice_unauthorized_non_author_non_admin() {
1090 let author_id = Uuid::new_v4();
1091 let other_user_id = Uuid::new_v4();
1092 let org_id = Uuid::new_v4();
1093 let building_id = Uuid::new_v4();
1094 let notice = make_published_notice(building_id, author_id);
1095 let notice_id = notice.id;
1096
1097 let uc = NoticeUseCases::new(
1098 Arc::new(MockNoticeRepo::with_notice(notice)),
1099 Arc::new(MockUserRepo::new()),
1100 );
1101
1102 let result = uc
1103 .archive_notice(notice_id, other_user_id, org_id, "owner", None)
1104 .await;
1105 assert!(result.is_err());
1106 assert!(result
1107 .unwrap_err()
1108 .contains("Unauthorized: only author or building admin can archive notice"));
1109 }
1110
1111 #[tokio::test]
1116 async fn negative_moderation_dune_annonce_sans_motif_est_refusee() {
1117 let author_id = Uuid::new_v4();
1118 let syndic_id = Uuid::new_v4();
1119 let org_id = Uuid::new_v4();
1120 let building_id = Uuid::new_v4();
1121 let notice = make_published_notice(building_id, author_id);
1122 let notice_id = notice.id;
1123
1124 let uc = NoticeUseCases::new(
1125 Arc::new(MockNoticeRepo::with_notice(notice)),
1126 Arc::new(MockUserRepo::new()),
1127 );
1128
1129 let result = uc
1130 .archive_notice(notice_id, syndic_id, org_id, "syndic", None)
1131 .await;
1132 assert!(result.is_err());
1133 assert_eq!(
1134 result.unwrap_err(),
1135 crate::application::error::MOTIF_MODERATION_REQUIS
1136 );
1137
1138 let result_blank = uc
1139 .archive_notice(
1140 notice_id,
1141 syndic_id,
1142 org_id,
1143 "syndic",
1144 Some(" ".to_string()),
1145 )
1146 .await;
1147 assert!(result_blank.is_err());
1148 assert_eq!(
1149 result_blank.unwrap_err(),
1150 crate::application::error::MOTIF_MODERATION_REQUIS
1151 );
1152 }
1153
1154 #[tokio::test]
1155 async fn happy_community_moderator_archive_avec_motif() {
1156 let author_id = Uuid::new_v4();
1160 let moderator_id = Uuid::new_v4();
1161 let org_id = Uuid::new_v4();
1162 let building_id = Uuid::new_v4();
1163 let notice = make_published_notice(building_id, author_id);
1164 let notice_id = notice.id;
1165
1166 let uc = NoticeUseCases::new(
1167 Arc::new(MockNoticeRepo::with_notice(notice)),
1168 Arc::new(MockUserRepo::new()),
1169 );
1170
1171 let result = uc
1172 .archive_notice(
1173 notice_id,
1174 moderator_id,
1175 org_id,
1176 "community.moderator",
1177 Some("Annonce en doublon".to_string()),
1178 )
1179 .await;
1180 assert!(
1181 result.is_ok(),
1182 "un community.moderator doit pouvoir archiver avec motif : {:?}",
1183 result.err()
1184 );
1185 assert_eq!(result.unwrap().status, NoticeStatus::Archived);
1186 }
1187
1188 #[tokio::test]
1189 async fn test_pin_notice_admin_success() {
1190 let user_id = Uuid::new_v4();
1191 let building_id = Uuid::new_v4();
1192 let notice = make_published_notice(building_id, user_id);
1193 let notice_id = notice.id;
1194 let user = make_user(user_id);
1195
1196 let uc = NoticeUseCases::new(
1197 Arc::new(MockNoticeRepo::with_notice(notice)),
1198 Arc::new(MockUserRepo::with_user(user)),
1199 );
1200
1201 let result = uc.pin_notice(notice_id, "syndic").await;
1202 assert!(result.is_ok());
1203 assert!(result.unwrap().is_pinned);
1204 }
1205
1206 #[tokio::test]
1207 async fn test_pin_notice_unauthorized_owner() {
1208 let user_id = Uuid::new_v4();
1209 let building_id = Uuid::new_v4();
1210 let notice = make_published_notice(building_id, user_id);
1211 let notice_id = notice.id;
1212
1213 let uc = NoticeUseCases::new(
1214 Arc::new(MockNoticeRepo::with_notice(notice)),
1215 Arc::new(MockUserRepo::new()),
1216 );
1217
1218 let result = uc.pin_notice(notice_id, "owner").await;
1219 assert!(result.is_err());
1220 assert!(result
1221 .unwrap_err()
1222 .contains("Unauthorized: only building admin"));
1223 }
1224
1225 #[tokio::test]
1226 async fn test_delete_notice_success_draft() {
1227 let user_id = Uuid::new_v4();
1228 let org_id = Uuid::new_v4();
1229 let building_id = Uuid::new_v4();
1230 let notice = make_draft_notice(building_id, user_id);
1231 let notice_id = notice.id;
1232
1233 let uc = NoticeUseCases::new(
1234 Arc::new(MockNoticeRepo::with_notice(notice)),
1235 Arc::new(MockUserRepo::new()),
1236 );
1237
1238 let result = uc.delete_notice(notice_id, user_id, org_id).await;
1239 assert!(result.is_ok());
1240 }
1241
1242 #[tokio::test]
1243 async fn test_delete_notice_blocked_for_published() {
1244 let user_id = Uuid::new_v4();
1245 let org_id = Uuid::new_v4();
1246 let building_id = Uuid::new_v4();
1247 let notice = make_published_notice(building_id, user_id);
1248 let notice_id = notice.id;
1249
1250 let uc = NoticeUseCases::new(
1251 Arc::new(MockNoticeRepo::with_notice(notice)),
1252 Arc::new(MockUserRepo::new()),
1253 );
1254
1255 let result = uc.delete_notice(notice_id, user_id, org_id).await;
1256 assert!(result.is_err());
1257 assert!(result
1258 .unwrap_err()
1259 .contains("Cannot delete notice in status"));
1260 }
1261}