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