koprogo_api/infrastructure/web/handlers/
notice_handlers.rs

1use crate::application::dto::{CreateNoticeDto, SetExpirationDto, UpdateNoticeDto};
2use crate::domain::entities::{NoticeCategory, NoticeStatus, NoticeType};
3use crate::infrastructure::web::app_state::AppState;
4use crate::infrastructure::web::middleware::AuthenticatedUser;
5use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
6use uuid::Uuid;
7
8/// Create a new notice (Draft status)
9///
10/// POST /notices
11#[post("/notices")]
12pub async fn create_notice(
13    data: web::Data<AppState>,
14    auth: AuthenticatedUser,
15    request: web::Json<CreateNoticeDto>,
16) -> impl Responder {
17    match data
18        .notice_use_cases
19        .create_notice(auth.user_id, request.into_inner())
20        .await
21    {
22        Ok(notice) => HttpResponse::Created().json(notice),
23        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
24    }
25}
26
27/// Get notice by ID with author name enrichment
28///
29/// GET /notices/:id
30#[get("/notices/{id}")]
31pub async fn get_notice(data: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
32    match data.notice_use_cases.get_notice(id.into_inner()).await {
33        Ok(notice) => HttpResponse::Ok().json(notice),
34        Err(e) => {
35            if e.contains("not found") {
36                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
37            } else {
38                HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
39            }
40        }
41    }
42}
43
44/// List all notices for a building (all statuses)
45///
46/// GET /buildings/:building_id/notices
47#[get("/buildings/{building_id}/notices")]
48pub async fn list_building_notices(
49    data: web::Data<AppState>,
50    building_id: web::Path<Uuid>,
51) -> impl Responder {
52    match data
53        .notice_use_cases
54        .list_building_notices(building_id.into_inner())
55        .await
56    {
57        Ok(notices) => HttpResponse::Ok().json(notices),
58        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
59    }
60}
61
62/// List published notices for a building (visible to members)
63///
64/// GET /buildings/:building_id/notices/published
65#[get("/buildings/{building_id}/notices/published")]
66pub async fn list_published_notices(
67    data: web::Data<AppState>,
68    building_id: web::Path<Uuid>,
69) -> impl Responder {
70    match data
71        .notice_use_cases
72        .list_published_notices(building_id.into_inner())
73        .await
74    {
75        Ok(notices) => HttpResponse::Ok().json(notices),
76        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
77    }
78}
79
80/// List pinned notices for a building (important announcements)
81///
82/// GET /buildings/:building_id/notices/pinned
83#[get("/buildings/{building_id}/notices/pinned")]
84pub async fn list_pinned_notices(
85    data: web::Data<AppState>,
86    building_id: web::Path<Uuid>,
87) -> impl Responder {
88    match data
89        .notice_use_cases
90        .list_pinned_notices(building_id.into_inner())
91        .await
92    {
93        Ok(notices) => HttpResponse::Ok().json(notices),
94        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
95    }
96}
97
98/// List notices by type (Announcement, Event, LostAndFound, ClassifiedAd)
99///
100/// GET /buildings/:building_id/notices/type/:notice_type
101#[get("/buildings/{building_id}/notices/type/{notice_type}")]
102pub async fn list_notices_by_type(
103    data: web::Data<AppState>,
104    path: web::Path<(Uuid, String)>,
105) -> impl Responder {
106    let (building_id, notice_type_str) = path.into_inner();
107
108    // Parse notice type
109    let notice_type = match serde_json::from_str::<NoticeType>(&format!("\"{}\"", notice_type_str))
110    {
111        Ok(nt) => nt,
112        Err(_) => {
113            return HttpResponse::BadRequest().json(serde_json::json!({
114                "error": format!("Invalid notice type: {}. Valid types: Announcement, Event, LostAndFound, ClassifiedAd", notice_type_str)
115            }))
116        }
117    };
118
119    match data
120        .notice_use_cases
121        .list_notices_by_type(building_id, notice_type)
122        .await
123    {
124        Ok(notices) => HttpResponse::Ok().json(notices),
125        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
126    }
127}
128
129/// List notices by category (General, Maintenance, Social, etc.)
130///
131/// GET /buildings/:building_id/notices/category/:category
132#[get("/buildings/{building_id}/notices/category/{category}")]
133pub async fn list_notices_by_category(
134    data: web::Data<AppState>,
135    path: web::Path<(Uuid, String)>,
136) -> impl Responder {
137    let (building_id, category_str) = path.into_inner();
138
139    // Parse category
140    let category = match serde_json::from_str::<NoticeCategory>(&format!("\"{}\"", category_str)) {
141        Ok(c) => c,
142        Err(_) => {
143            return HttpResponse::BadRequest().json(serde_json::json!({
144                "error": format!("Invalid category: {}. Valid categories: General, Maintenance, Social, Security, Environment, Parking, Other", category_str)
145            }))
146        }
147    };
148
149    match data
150        .notice_use_cases
151        .list_notices_by_category(building_id, category)
152        .await
153    {
154        Ok(notices) => HttpResponse::Ok().json(notices),
155        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
156    }
157}
158
159/// List notices by status (Draft, Published, Archived, Expired)
160///
161/// GET /buildings/:building_id/notices/status/:status
162#[get("/buildings/{building_id}/notices/status/{status}")]
163pub async fn list_notices_by_status(
164    data: web::Data<AppState>,
165    path: web::Path<(Uuid, String)>,
166) -> impl Responder {
167    let (building_id, status_str) = path.into_inner();
168
169    // Parse status
170    let status = match serde_json::from_str::<NoticeStatus>(&format!("\"{}\"", status_str)) {
171        Ok(s) => s,
172        Err(_) => {
173            return HttpResponse::BadRequest().json(serde_json::json!({
174                "error": format!("Invalid status: {}. Valid statuses: Draft, Published, Archived, Expired", status_str)
175            }))
176        }
177    };
178
179    match data
180        .notice_use_cases
181        .list_notices_by_status(building_id, status)
182        .await
183    {
184        Ok(notices) => HttpResponse::Ok().json(notices),
185        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
186    }
187}
188
189/// List all notices created by an author
190///
191/// GET /owners/:author_id/notices
192#[get("/owners/{author_id}/notices")]
193pub async fn list_author_notices(
194    data: web::Data<AppState>,
195    author_id: web::Path<Uuid>,
196) -> impl Responder {
197    match data
198        .notice_use_cases
199        .list_author_notices(author_id.into_inner())
200        .await
201    {
202        Ok(notices) => HttpResponse::Ok().json(notices),
203        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
204    }
205}
206
207/// Update a notice (Draft only)
208///
209/// PUT /notices/:id
210#[put("/notices/{id}")]
211pub async fn update_notice(
212    data: web::Data<AppState>,
213    auth: AuthenticatedUser,
214    id: web::Path<Uuid>,
215    request: web::Json<UpdateNoticeDto>,
216) -> impl Responder {
217    match data
218        .notice_use_cases
219        .update_notice(id.into_inner(), auth.user_id, request.into_inner())
220        .await
221    {
222        Ok(notice) => HttpResponse::Ok().json(notice),
223        Err(e) => {
224            if e.contains("Unauthorized") {
225                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
226            } else if e.contains("not found") {
227                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
228            } else {
229                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
230            }
231        }
232    }
233}
234
235/// Publish a notice (Draft → Published)
236///
237/// POST /notices/:id/publish
238#[post("/notices/{id}/publish")]
239pub async fn publish_notice(
240    data: web::Data<AppState>,
241    auth: AuthenticatedUser,
242    id: web::Path<Uuid>,
243) -> impl Responder {
244    match data
245        .notice_use_cases
246        .publish_notice(id.into_inner(), auth.user_id)
247        .await
248    {
249        Ok(notice) => HttpResponse::Ok().json(notice),
250        Err(e) => {
251            if e.contains("Unauthorized") {
252                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
253            } else if e.contains("not found") {
254                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
255            } else {
256                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
257            }
258        }
259    }
260}
261
262/// Archive a notice (Published/Expired → Archived)
263///
264/// POST /notices/:id/archive
265#[post("/notices/{id}/archive")]
266pub async fn archive_notice(
267    data: web::Data<AppState>,
268    auth: AuthenticatedUser,
269    id: web::Path<Uuid>,
270) -> impl Responder {
271    match data
272        .notice_use_cases
273        .archive_notice(id.into_inner(), auth.user_id, &auth.role)
274        .await
275    {
276        Ok(notice) => HttpResponse::Ok().json(notice),
277        Err(e) => {
278            if e.contains("Unauthorized") {
279                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
280            } else if e.contains("not found") {
281                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
282            } else {
283                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
284            }
285        }
286    }
287}
288
289/// Pin a notice to top of board (Published only)
290///
291/// POST /notices/:id/pin
292#[post("/notices/{id}/pin")]
293pub async fn pin_notice(
294    data: web::Data<AppState>,
295    auth: AuthenticatedUser,
296    id: web::Path<Uuid>,
297) -> impl Responder {
298    match data
299        .notice_use_cases
300        .pin_notice(id.into_inner(), &auth.role)
301        .await
302    {
303        Ok(notice) => HttpResponse::Ok().json(notice),
304        Err(e) => {
305            if e.contains("Unauthorized") {
306                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
307            } else if e.contains("not found") {
308                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
309            } else {
310                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
311            }
312        }
313    }
314}
315
316/// Unpin a notice
317///
318/// POST /notices/:id/unpin
319#[post("/notices/{id}/unpin")]
320pub async fn unpin_notice(
321    data: web::Data<AppState>,
322    auth: AuthenticatedUser,
323    id: web::Path<Uuid>,
324) -> impl Responder {
325    match data
326        .notice_use_cases
327        .unpin_notice(id.into_inner(), &auth.role)
328        .await
329    {
330        Ok(notice) => HttpResponse::Ok().json(notice),
331        Err(e) => {
332            if e.contains("Unauthorized") {
333                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
334            } else if e.contains("not found") {
335                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
336            } else {
337                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
338            }
339        }
340    }
341}
342
343/// Set expiration date for a notice
344///
345/// PUT /notices/:id/expiration
346#[put("/notices/{id}/expiration")]
347pub async fn set_expiration(
348    data: web::Data<AppState>,
349    auth: AuthenticatedUser,
350    id: web::Path<Uuid>,
351    request: web::Json<SetExpirationDto>,
352) -> impl Responder {
353    match data
354        .notice_use_cases
355        .set_expiration(id.into_inner(), auth.user_id, request.into_inner())
356        .await
357    {
358        Ok(notice) => HttpResponse::Ok().json(notice),
359        Err(e) => {
360            if e.contains("Unauthorized") {
361                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
362            } else if e.contains("not found") {
363                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
364            } else {
365                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
366            }
367        }
368    }
369}
370
371/// Delete a notice
372///
373/// DELETE /notices/:id
374#[delete("/notices/{id}")]
375pub async fn delete_notice(
376    data: web::Data<AppState>,
377    auth: AuthenticatedUser,
378    id: web::Path<Uuid>,
379) -> impl Responder {
380    match data
381        .notice_use_cases
382        .delete_notice(id.into_inner(), auth.user_id)
383        .await
384    {
385        Ok(_) => HttpResponse::NoContent().finish(),
386        Err(e) => {
387            if e.contains("Unauthorized") {
388                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
389            } else if e.contains("not found") {
390                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
391            } else {
392                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
393            }
394        }
395    }
396}
397
398/// Get notice statistics for a building
399///
400/// GET /buildings/:building_id/notices/statistics
401#[get("/buildings/{building_id}/notices/statistics")]
402pub async fn get_notice_statistics(
403    data: web::Data<AppState>,
404    building_id: web::Path<Uuid>,
405) -> impl Responder {
406    match data
407        .notice_use_cases
408        .get_statistics(building_id.into_inner())
409        .await
410    {
411        Ok(stats) => HttpResponse::Ok().json(stats),
412        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
413    }
414}