koprogo_api/infrastructure/web/handlers/
shared_object_handlers.rs

1use crate::application::dto::{BorrowObjectDto, CreateSharedObjectDto, UpdateSharedObjectDto};
2use crate::domain::entities::SharedObjectCategory;
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 shared object
9///
10/// POST /shared-objects
11#[post("/shared-objects")]
12pub async fn create_shared_object(
13    data: web::Data<AppState>,
14    auth: AuthenticatedUser,
15    request: web::Json<CreateSharedObjectDto>,
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        .shared_object_use_cases
25        .create_shared_object(auth.user_id, org_id, request.into_inner())
26        .await
27    {
28        Ok(object) => HttpResponse::Created().json(object),
29        Err(e) => HttpResponse::BadRequest().json(serde_json::json!({"error": e})),
30    }
31}
32
33/// Get shared object by ID with owner/borrower name enrichment
34///
35/// GET /shared-objects/:id
36#[get("/shared-objects/{id}")]
37pub async fn get_shared_object(data: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
38    match data
39        .shared_object_use_cases
40        .get_shared_object(id.into_inner())
41        .await
42    {
43        Ok(object) => HttpResponse::Ok().json(object),
44        Err(e) => {
45            if e.contains("not found") {
46                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
47            } else {
48                HttpResponse::InternalServerError().json(serde_json::json!({"error": e}))
49            }
50        }
51    }
52}
53
54/// List all shared objects for a building
55///
56/// GET /buildings/:building_id/shared-objects
57#[get("/buildings/{building_id}/shared-objects")]
58pub async fn list_building_objects(
59    data: web::Data<AppState>,
60    building_id: web::Path<Uuid>,
61) -> impl Responder {
62    match data
63        .shared_object_use_cases
64        .list_building_objects(building_id.into_inner())
65        .await
66    {
67        Ok(objects) => HttpResponse::Ok().json(objects),
68        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
69    }
70}
71
72/// List available shared objects for a building (marketplace view)
73///
74/// GET /buildings/:building_id/shared-objects/available
75#[get("/buildings/{building_id}/shared-objects/available")]
76pub async fn list_available_objects(
77    data: web::Data<AppState>,
78    building_id: web::Path<Uuid>,
79) -> impl Responder {
80    match data
81        .shared_object_use_cases
82        .list_available_objects(building_id.into_inner())
83        .await
84    {
85        Ok(objects) => HttpResponse::Ok().json(objects),
86        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
87    }
88}
89
90/// List borrowed shared objects for a building
91///
92/// GET /buildings/:building_id/shared-objects/borrowed
93#[get("/buildings/{building_id}/shared-objects/borrowed")]
94pub async fn list_borrowed_objects(
95    data: web::Data<AppState>,
96    building_id: web::Path<Uuid>,
97) -> impl Responder {
98    match data
99        .shared_object_use_cases
100        .list_borrowed_objects(building_id.into_inner())
101        .await
102    {
103        Ok(objects) => HttpResponse::Ok().json(objects),
104        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
105    }
106}
107
108/// List overdue shared objects for a building
109///
110/// GET /buildings/:building_id/shared-objects/overdue
111#[get("/buildings/{building_id}/shared-objects/overdue")]
112pub async fn list_overdue_objects(
113    data: web::Data<AppState>,
114    building_id: web::Path<Uuid>,
115) -> impl Responder {
116    match data
117        .shared_object_use_cases
118        .list_overdue_objects(building_id.into_inner())
119        .await
120    {
121        Ok(objects) => HttpResponse::Ok().json(objects),
122        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
123    }
124}
125
126/// List free/volunteer shared objects for a building
127///
128/// GET /buildings/:building_id/shared-objects/free
129#[get("/buildings/{building_id}/shared-objects/free")]
130pub async fn list_free_objects(
131    data: web::Data<AppState>,
132    building_id: web::Path<Uuid>,
133) -> impl Responder {
134    match data
135        .shared_object_use_cases
136        .list_free_objects(building_id.into_inner())
137        .await
138    {
139        Ok(objects) => HttpResponse::Ok().json(objects),
140        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
141    }
142}
143
144/// List shared objects by category (Tools, Books, Electronics, etc.)
145///
146/// GET /buildings/:building_id/shared-objects/category/:category
147#[get("/buildings/{building_id}/shared-objects/category/{category}")]
148pub async fn list_objects_by_category(
149    data: web::Data<AppState>,
150    path: web::Path<(Uuid, String)>,
151) -> impl Responder {
152    let (building_id, category_str) = path.into_inner();
153
154    // Parse object category
155    let category = match serde_json::from_str::<SharedObjectCategory>(&format!(
156        "\"{}\"",
157        category_str
158    )) {
159        Ok(c) => c,
160        Err(_) => {
161            return HttpResponse::BadRequest().json(serde_json::json!({
162                "error": format!("Invalid shared object category: {}. Valid categories: Tools, Books, Electronics, Sports, Gardening, Kitchen, Baby, Other", category_str)
163            }))
164        }
165    };
166
167    match data
168        .shared_object_use_cases
169        .list_objects_by_category(building_id, category)
170        .await
171    {
172        Ok(objects) => HttpResponse::Ok().json(objects),
173        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
174    }
175}
176
177/// List all shared objects created by an owner
178///
179/// GET /owners/:owner_id/shared-objects
180#[get("/owners/{owner_id}/shared-objects")]
181pub async fn list_owner_objects(
182    data: web::Data<AppState>,
183    owner_id: web::Path<Uuid>,
184) -> impl Responder {
185    match data
186        .shared_object_use_cases
187        .list_owner_objects(owner_id.into_inner())
188        .await
189    {
190        Ok(objects) => HttpResponse::Ok().json(objects),
191        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
192    }
193}
194
195/// List all shared objects currently borrowed by a user
196///
197/// GET /shared-objects/my-borrowed
198#[get("/shared-objects/my-borrowed")]
199pub async fn list_my_borrowed_objects(
200    data: web::Data<AppState>,
201    auth: AuthenticatedUser,
202) -> impl Responder {
203    let org_id = match auth.require_organization() {
204        Ok(id) => id,
205        Err(e) => {
206            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
207        }
208    };
209    match data
210        .shared_object_use_cases
211        .list_user_borrowed_objects(auth.user_id, org_id)
212        .await
213    {
214        Ok(objects) => HttpResponse::Ok().json(objects),
215        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
216    }
217}
218
219/// Update a shared object
220///
221/// PUT /shared-objects/:id
222#[put("/shared-objects/{id}")]
223pub async fn update_shared_object(
224    data: web::Data<AppState>,
225    auth: AuthenticatedUser,
226    id: web::Path<Uuid>,
227    request: web::Json<UpdateSharedObjectDto>,
228) -> impl Responder {
229    let org_id = match auth.require_organization() {
230        Ok(id) => id,
231        Err(e) => {
232            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
233        }
234    };
235    match data
236        .shared_object_use_cases
237        .update_shared_object(id.into_inner(), auth.user_id, org_id, request.into_inner())
238        .await
239    {
240        Ok(object) => HttpResponse::Ok().json(object),
241        Err(e) => {
242            if e.contains("Unauthorized") {
243                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
244            } else if e.contains("not found") {
245                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
246            } else {
247                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
248            }
249        }
250    }
251}
252
253/// Mark shared object as available
254///
255/// POST /shared-objects/:id/mark-available
256#[post("/shared-objects/{id}/mark-available")]
257pub async fn mark_object_available(
258    data: web::Data<AppState>,
259    auth: AuthenticatedUser,
260    id: web::Path<Uuid>,
261) -> impl Responder {
262    let org_id = match auth.require_organization() {
263        Ok(id) => id,
264        Err(e) => {
265            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
266        }
267    };
268    match data
269        .shared_object_use_cases
270        .mark_object_available(id.into_inner(), auth.user_id, org_id)
271        .await
272    {
273        Ok(object) => HttpResponse::Ok().json(object),
274        Err(e) => {
275            if e.contains("Unauthorized") {
276                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
277            } else if e.contains("not found") {
278                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
279            } else {
280                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
281            }
282        }
283    }
284}
285
286/// Mark shared object as unavailable
287///
288/// POST /shared-objects/:id/mark-unavailable
289#[post("/shared-objects/{id}/mark-unavailable")]
290pub async fn mark_object_unavailable(
291    data: web::Data<AppState>,
292    auth: AuthenticatedUser,
293    id: web::Path<Uuid>,
294) -> impl Responder {
295    let org_id = match auth.require_organization() {
296        Ok(id) => id,
297        Err(e) => {
298            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
299        }
300    };
301    match data
302        .shared_object_use_cases
303        .mark_object_unavailable(id.into_inner(), auth.user_id, org_id)
304        .await
305    {
306        Ok(object) => HttpResponse::Ok().json(object),
307        Err(e) => {
308            if e.contains("Unauthorized") {
309                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
310            } else if e.contains("not found") {
311                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
312            } else {
313                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
314            }
315        }
316    }
317}
318
319/// Borrow a shared object
320///
321/// POST /shared-objects/:id/borrow
322#[post("/shared-objects/{id}/borrow")]
323pub async fn borrow_object(
324    data: web::Data<AppState>,
325    auth: AuthenticatedUser,
326    id: web::Path<Uuid>,
327    request: web::Json<BorrowObjectDto>,
328) -> impl Responder {
329    let org_id = match auth.require_organization() {
330        Ok(id) => id,
331        Err(e) => {
332            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
333        }
334    };
335    match data
336        .shared_object_use_cases
337        .borrow_object(id.into_inner(), auth.user_id, org_id, request.into_inner())
338        .await
339    {
340        Ok(object) => HttpResponse::Ok().json(object),
341        Err(e) => {
342            if e.contains("Owner cannot borrow") {
343                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
344            } else if e.contains("not found") {
345                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
346            } else {
347                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
348            }
349        }
350    }
351}
352
353/// Return a borrowed object
354///
355/// POST /shared-objects/:id/return
356#[post("/shared-objects/{id}/return")]
357pub async fn return_object(
358    data: web::Data<AppState>,
359    auth: AuthenticatedUser,
360    id: web::Path<Uuid>,
361) -> impl Responder {
362    let org_id = match auth.require_organization() {
363        Ok(id) => id,
364        Err(e) => {
365            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
366        }
367    };
368    match data
369        .shared_object_use_cases
370        .return_object(id.into_inner(), auth.user_id, org_id)
371        .await
372    {
373        Ok(object) => HttpResponse::Ok().json(object),
374        Err(e) => {
375            if e.contains("Only borrower can return") {
376                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
377            } else if e.contains("not found") {
378                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
379            } else {
380                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
381            }
382        }
383    }
384}
385
386/// Delete a shared object
387///
388/// DELETE /shared-objects/:id
389#[delete("/shared-objects/{id}")]
390pub async fn delete_shared_object(
391    data: web::Data<AppState>,
392    auth: AuthenticatedUser,
393    id: web::Path<Uuid>,
394) -> impl Responder {
395    let org_id = match auth.require_organization() {
396        Ok(id) => id,
397        Err(e) => {
398            return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
399        }
400    };
401    match data
402        .shared_object_use_cases
403        .delete_shared_object(id.into_inner(), auth.user_id, org_id)
404        .await
405    {
406        Ok(_) => HttpResponse::NoContent().finish(),
407        Err(e) => {
408            if e.contains("Unauthorized") {
409                HttpResponse::Forbidden().json(serde_json::json!({"error": e}))
410            } else if e.contains("not found") {
411                HttpResponse::NotFound().json(serde_json::json!({"error": e}))
412            } else {
413                HttpResponse::BadRequest().json(serde_json::json!({"error": e}))
414            }
415        }
416    }
417}
418
419/// Get shared object statistics for a building
420///
421/// GET /buildings/:building_id/shared-objects/statistics
422#[get("/buildings/{building_id}/shared-objects/statistics")]
423pub async fn get_object_statistics(
424    data: web::Data<AppState>,
425    building_id: web::Path<Uuid>,
426) -> impl Responder {
427    match data
428        .shared_object_use_cases
429        .get_object_statistics(building_id.into_inner())
430        .await
431    {
432        Ok(stats) => HttpResponse::Ok().json(stats),
433        Err(e) => HttpResponse::InternalServerError().json(serde_json::json!({"error": e})),
434    }
435}