koprogo_api/application/use_cases/
notice_use_cases.rs

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    /// Check if user has building admin privileges (admin, superadmin, or syndic)
23    fn is_building_admin(role: &str) -> bool {
24        role == "admin" || role == "superadmin" || role == "syndic"
25    }
26
27    /// Resolve user_id to display name via user lookup
28    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    /// Create a new notice (Draft status)
36    ///
37    /// # Authorization
38    /// - Any authenticated user in the organization can post a notice
39    ///   (syndic, admin, owner — all are valid authors)
40    pub async fn create_notice(
41        &self,
42        user_id: Uuid,
43        _organization_id: Uuid,
44        dto: CreateNoticeDto,
45    ) -> Result<NoticeResponseDto, String> {
46        // author_id is the user's own ID (notices.author_id now references users.id)
47        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        // Set expiration if provided
60        let mut notice = notice;
61        if let Some(expires_at) = dto.expires_at {
62            notice.set_expiration(Some(expires_at))?;
63        }
64
65        // Persist notice
66        let created = self.notice_repo.create(&notice).await?;
67
68        let author_name = self.resolve_author_name(user_id).await;
69        Ok(NoticeResponseDto::from_notice(created, author_name))
70    }
71
72    /// Get notice by ID with author name enrichment
73    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    /// List all notices for a building (all statuses)
85    ///
86    /// # Returns
87    /// - Notices sorted by pinned (DESC), created_at (DESC)
88    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    /// List published notices for a building (visible to members)
97    ///
98    /// # Returns
99    /// - Only Published notices, sorted by pinned (DESC), published_at (DESC)
100    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    /// List pinned notices for a building (important announcements)
112    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    /// List notices by type (Announcement, Event, LostAndFound, ClassifiedAd)
124    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    /// List notices by category (General, Maintenance, Social, etc.)
137    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    /// List notices by status (Draft, Published, Archived, Expired)
150    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    /// List all notices created by an author
160    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    /// Update a notice (Draft only)
169    ///
170    /// # Authorization
171    /// - Only author can update their notice
172    /// - Only Draft notices can be updated
173    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        // Authorization: only author can update
187        if notice.author_id != user_id {
188            return Err("Unauthorized: only author can update notice".to_string());
189        }
190
191        // Update content (domain validates Draft status)
192        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        // Persist changes
203        let updated = self.notice_repo.update(&notice).await?;
204
205        // Return enriched response
206        self.get_notice(updated.id).await
207    }
208
209    /// Publish a notice (Draft → Published)
210    ///
211    /// # Authorization
212    /// - Only author can publish their notice
213    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        // Authorization: only author can publish
226        if notice.author_id != user_id {
227            return Err("Unauthorized: only author can publish notice".to_string());
228        }
229
230        // Publish (domain validates state transition)
231        notice.publish()?;
232
233        // Persist changes
234        let updated = self.notice_repo.update(&notice).await?;
235
236        // Return enriched response
237        self.get_notice(updated.id).await
238    }
239
240    /// Archive a notice (Published/Expired → Archived)
241    ///
242    /// # Authorization
243    /// - Only author or building admin can archive
244    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        // Authorization: only author or building admin can archive
258        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        // Archive (domain validates state transition)
268        notice.archive()?;
269
270        // Persist changes
271        let updated = self.notice_repo.update(&notice).await?;
272
273        // Return enriched response
274        self.get_notice(updated.id).await
275    }
276
277    /// Pin a notice to top of board (Published only)
278    ///
279    /// # Authorization
280    /// - Only building admin (admin, superadmin, or syndic) can pin notices
281    pub async fn pin_notice(
282        &self,
283        notice_id: Uuid,
284        actor_role: &str,
285    ) -> Result<NoticeResponseDto, String> {
286        // Authorization: only building admin can pin
287        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        // Pin (domain validates Published status)
301        notice.pin()?;
302
303        // Persist changes
304        let updated = self.notice_repo.update(&notice).await?;
305
306        // Return enriched response
307        self.get_notice(updated.id).await
308    }
309
310    /// Unpin a notice
311    ///
312    /// # Authorization
313    /// - Only building admin (admin, superadmin, or syndic) can unpin notices
314    pub async fn unpin_notice(
315        &self,
316        notice_id: Uuid,
317        actor_role: &str,
318    ) -> Result<NoticeResponseDto, String> {
319        // Authorization: only building admin can unpin
320        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        // Unpin
331        notice.unpin()?;
332
333        // Persist changes
334        let updated = self.notice_repo.update(&notice).await?;
335
336        // Return enriched response
337        self.get_notice(updated.id).await
338    }
339
340    /// Set expiration date for a notice
341    ///
342    /// # Authorization
343    /// - Only author can set expiration
344    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        // Authorization: only author can set expiration
358        if notice.author_id != user_id {
359            return Err("Unauthorized: only author can set expiration".to_string());
360        }
361
362        // Set expiration (domain validates future date)
363        notice.set_expiration(dto.expires_at)?;
364
365        // Persist changes
366        let updated = self.notice_repo.update(&notice).await?;
367
368        // Return enriched response
369        self.get_notice(updated.id).await
370    }
371
372    /// Delete a notice
373    ///
374    /// # Authorization
375    /// - Only author can delete their notice
376    /// - Cannot delete Published/Archived notices (must archive first)
377    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        // Authorization: only author can delete
390        if notice.author_id != user_id {
391            return Err("Unauthorized: only author can delete notice".to_string());
392        }
393
394        // Business rule: cannot delete Published or Archived notices
395        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        // Delete notice
406        self.notice_repo.delete(notice_id).await?;
407
408        Ok(())
409    }
410
411    /// Automatically expire notices that have passed their expiration date
412    ///
413    /// # Background Job
414    /// - Should be called periodically (e.g., daily cron job)
415    /// - Finds all Published notices with expires_at in the past
416    /// - Transitions them to Expired status
417    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            // Expire notice (domain validates state transition)
424            if let Err(e) = notice.expire() {
425                log::warn!("Failed to expire notice {}: {}. Skipping.", notice.id, e);
426                continue;
427            }
428
429            // Persist changes
430            match self.notice_repo.update(&notice).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    /// Get notice statistics for a building
445    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    // Helper method to enrich notices with author names
464    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/// Notice statistics for a building
480#[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    // ─── Mock NoticeRepository ──────────────────────────────────────────
501
502    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    // ─── Mock UserRepository ────────────────────────────────────────────
664
665    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    // ─── Helpers ────────────────────────────────────────────────────────
738
739    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    // ─── Tests ──────────────────────────────────────────────────────────
780
781    #[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        // Admin (not the author) can archive
926        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}