koprogo_api/application/use_cases/
notice_use_cases.rs1use crate::application::dto::{
2 CreateNoticeDto, NoticeResponseDto, NoticeSummaryDto, SetExpirationDto, UpdateNoticeDto,
3};
4use crate::application::ports::{NoticeRepository, OwnerRepository};
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 owner_repo: Arc<dyn OwnerRepository>,
12}
13
14impl NoticeUseCases {
15 pub fn new(
16 notice_repo: Arc<dyn NoticeRepository>,
17 owner_repo: Arc<dyn OwnerRepository>,
18 ) -> Self {
19 Self {
20 notice_repo,
21 owner_repo,
22 }
23 }
24
25 fn is_building_admin(role: &str) -> bool {
27 role == "admin" || role == "superadmin" || role == "syndic"
28 }
29
30 pub async fn create_notice(
35 &self,
36 author_id: Uuid,
37 dto: CreateNoticeDto,
38 ) -> Result<NoticeResponseDto, String> {
39 let author = self
41 .owner_repo
42 .find_by_id(author_id)
43 .await?
44 .ok_or("Author not found".to_string())?;
45
46 let notice = Notice::new(
48 dto.building_id,
49 author_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 = format!("{} {}", author.first_name, author.last_name);
70 Ok(NoticeResponseDto::from_notice(created, author_name))
71 }
72
73 pub async fn get_notice(&self, notice_id: Uuid) -> Result<NoticeResponseDto, String> {
75 let notice = self
76 .notice_repo
77 .find_by_id(notice_id)
78 .await?
79 .ok_or("Notice not found".to_string())?;
80
81 let author = self
83 .owner_repo
84 .find_by_id(notice.author_id)
85 .await?
86 .ok_or("Author not found".to_string())?;
87
88 let author_name = format!("{} {}", author.first_name, author.last_name);
89 Ok(NoticeResponseDto::from_notice(notice, author_name))
90 }
91
92 pub async fn list_building_notices(
97 &self,
98 building_id: Uuid,
99 ) -> Result<Vec<NoticeSummaryDto>, String> {
100 let notices = self.notice_repo.find_by_building(building_id).await?;
101 self.enrich_notices_summary(notices).await
102 }
103
104 pub async fn list_published_notices(
109 &self,
110 building_id: Uuid,
111 ) -> Result<Vec<NoticeSummaryDto>, String> {
112 let notices = self
113 .notice_repo
114 .find_published_by_building(building_id)
115 .await?;
116 self.enrich_notices_summary(notices).await
117 }
118
119 pub async fn list_pinned_notices(
121 &self,
122 building_id: Uuid,
123 ) -> Result<Vec<NoticeSummaryDto>, String> {
124 let notices = self
125 .notice_repo
126 .find_pinned_by_building(building_id)
127 .await?;
128 self.enrich_notices_summary(notices).await
129 }
130
131 pub async fn list_notices_by_type(
133 &self,
134 building_id: Uuid,
135 notice_type: NoticeType,
136 ) -> Result<Vec<NoticeSummaryDto>, String> {
137 let notices = self
138 .notice_repo
139 .find_by_type(building_id, notice_type)
140 .await?;
141 self.enrich_notices_summary(notices).await
142 }
143
144 pub async fn list_notices_by_category(
146 &self,
147 building_id: Uuid,
148 category: NoticeCategory,
149 ) -> Result<Vec<NoticeSummaryDto>, String> {
150 let notices = self
151 .notice_repo
152 .find_by_category(building_id, category)
153 .await?;
154 self.enrich_notices_summary(notices).await
155 }
156
157 pub async fn list_notices_by_status(
159 &self,
160 building_id: Uuid,
161 status: NoticeStatus,
162 ) -> Result<Vec<NoticeSummaryDto>, String> {
163 let notices = self.notice_repo.find_by_status(building_id, status).await?;
164 self.enrich_notices_summary(notices).await
165 }
166
167 pub async fn list_author_notices(
169 &self,
170 author_id: Uuid,
171 ) -> Result<Vec<NoticeSummaryDto>, String> {
172 let notices = self.notice_repo.find_by_author(author_id).await?;
173 self.enrich_notices_summary(notices).await
174 }
175
176 pub async fn update_notice(
182 &self,
183 notice_id: Uuid,
184 actor_id: Uuid,
185 dto: UpdateNoticeDto,
186 ) -> Result<NoticeResponseDto, String> {
187 let mut notice = self
188 .notice_repo
189 .find_by_id(notice_id)
190 .await?
191 .ok_or("Notice not found".to_string())?;
192
193 if notice.author_id != actor_id {
195 return Err("Unauthorized: only author can update notice".to_string());
196 }
197
198 notice.update_content(
200 dto.title,
201 dto.content,
202 dto.category,
203 dto.event_date,
204 dto.event_location,
205 dto.contact_info,
206 dto.expires_at,
207 )?;
208
209 let updated = self.notice_repo.update(¬ice).await?;
211
212 self.get_notice(updated.id).await
214 }
215
216 pub async fn publish_notice(
221 &self,
222 notice_id: Uuid,
223 actor_id: Uuid,
224 ) -> Result<NoticeResponseDto, String> {
225 let mut notice = self
226 .notice_repo
227 .find_by_id(notice_id)
228 .await?
229 .ok_or("Notice not found".to_string())?;
230
231 if notice.author_id != actor_id {
233 return Err("Unauthorized: only author can publish notice".to_string());
234 }
235
236 notice.publish()?;
238
239 let updated = self.notice_repo.update(¬ice).await?;
241
242 self.get_notice(updated.id).await
244 }
245
246 pub async fn archive_notice(
251 &self,
252 notice_id: Uuid,
253 actor_id: Uuid,
254 actor_role: &str,
255 ) -> Result<NoticeResponseDto, String> {
256 let mut notice = self
257 .notice_repo
258 .find_by_id(notice_id)
259 .await?
260 .ok_or("Notice not found".to_string())?;
261
262 let is_author = notice.author_id == actor_id;
264 let is_admin = Self::is_building_admin(actor_role);
265
266 if !is_author && !is_admin {
267 return Err(
268 "Unauthorized: only author or building admin can archive notice".to_string(),
269 );
270 }
271
272 notice.archive()?;
274
275 let updated = self.notice_repo.update(¬ice).await?;
277
278 self.get_notice(updated.id).await
280 }
281
282 pub async fn pin_notice(
287 &self,
288 notice_id: Uuid,
289 actor_role: &str,
290 ) -> Result<NoticeResponseDto, String> {
291 if !Self::is_building_admin(actor_role) {
293 return Err(
294 "Unauthorized: only building admin (admin, superadmin, or syndic) can pin notices"
295 .to_string(),
296 );
297 }
298
299 let mut notice = self
300 .notice_repo
301 .find_by_id(notice_id)
302 .await?
303 .ok_or("Notice not found".to_string())?;
304
305 notice.pin()?;
307
308 let updated = self.notice_repo.update(¬ice).await?;
310
311 self.get_notice(updated.id).await
313 }
314
315 pub async fn unpin_notice(
320 &self,
321 notice_id: Uuid,
322 actor_role: &str,
323 ) -> Result<NoticeResponseDto, String> {
324 if !Self::is_building_admin(actor_role) {
326 return Err("Unauthorized: only building admin (admin, superadmin, or syndic) can unpin notices".to_string());
327 }
328
329 let mut notice = self
330 .notice_repo
331 .find_by_id(notice_id)
332 .await?
333 .ok_or("Notice not found".to_string())?;
334
335 notice.unpin()?;
337
338 let updated = self.notice_repo.update(¬ice).await?;
340
341 self.get_notice(updated.id).await
343 }
344
345 pub async fn set_expiration(
350 &self,
351 notice_id: Uuid,
352 actor_id: Uuid,
353 dto: SetExpirationDto,
354 ) -> Result<NoticeResponseDto, String> {
355 let mut notice = self
356 .notice_repo
357 .find_by_id(notice_id)
358 .await?
359 .ok_or("Notice not found".to_string())?;
360
361 if notice.author_id != actor_id {
363 return Err("Unauthorized: only author can set expiration".to_string());
364 }
365
366 notice.set_expiration(dto.expires_at)?;
368
369 let updated = self.notice_repo.update(¬ice).await?;
371
372 self.get_notice(updated.id).await
374 }
375
376 pub async fn delete_notice(&self, notice_id: Uuid, actor_id: Uuid) -> Result<(), String> {
382 let notice = self
383 .notice_repo
384 .find_by_id(notice_id)
385 .await?
386 .ok_or("Notice not found".to_string())?;
387
388 if notice.author_id != actor_id {
390 return Err("Unauthorized: only author can delete notice".to_string());
391 }
392
393 match notice.status {
395 NoticeStatus::Published | NoticeStatus::Archived => {
396 return Err(format!(
397 "Cannot delete notice in status {:?}. Archive it first.",
398 notice.status
399 ));
400 }
401 _ => {}
402 }
403
404 self.notice_repo.delete(notice_id).await?;
406
407 Ok(())
408 }
409
410 pub async fn auto_expire_notices(&self, building_id: Uuid) -> Result<Vec<Uuid>, String> {
417 let expired_notices = self.notice_repo.find_expired(building_id).await?;
418
419 let mut expired_ids = Vec::new();
420
421 for mut notice in expired_notices {
422 if let Err(e) = notice.expire() {
424 log::warn!("Failed to expire notice {}: {}. Skipping.", notice.id, e);
425 continue;
426 }
427
428 match self.notice_repo.update(¬ice).await {
430 Ok(_) => {
431 expired_ids.push(notice.id);
432 log::info!("Auto-expired notice: {}", notice.id);
433 }
434 Err(e) => {
435 log::error!("Failed to update expired notice {}: {}", notice.id, e);
436 }
437 }
438 }
439
440 Ok(expired_ids)
441 }
442
443 pub async fn get_statistics(&self, building_id: Uuid) -> Result<NoticeStatistics, String> {
445 let total_count = self.notice_repo.count_by_building(building_id).await?;
446 let published_count = self
447 .notice_repo
448 .count_published_by_building(building_id)
449 .await?;
450 let pinned_count = self
451 .notice_repo
452 .count_pinned_by_building(building_id)
453 .await?;
454
455 Ok(NoticeStatistics {
456 total_count,
457 published_count,
458 pinned_count,
459 })
460 }
461
462 async fn enrich_notices_summary(
464 &self,
465 notices: Vec<Notice>,
466 ) -> Result<Vec<NoticeSummaryDto>, String> {
467 let mut enriched = Vec::new();
468
469 for notice in notices {
470 let author = self.owner_repo.find_by_id(notice.author_id).await?;
472 let author_name = if let Some(owner) = author {
473 format!("{} {}", owner.first_name, owner.last_name)
474 } else {
475 "Unknown Author".to_string()
476 };
477
478 enriched.push(NoticeSummaryDto::from_notice(notice, author_name));
479 }
480
481 Ok(enriched)
482 }
483}
484
485#[derive(Debug, serde::Serialize)]
487pub struct NoticeStatistics {
488 pub total_count: i64,
489 pub published_count: i64,
490 pub pinned_count: i64,
491}