koprogo_api/infrastructure/web/handlers/
unit_handlers.rs

1use crate::application::dto::{CreateUnitDto, PageRequest, PageResponse, UpdateUnitDto};
2use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
3use crate::infrastructure::web::{AppState, AuthenticatedUser};
4use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
5use uuid::Uuid;
6use validator::Validate;
7
8#[post("/units")]
9pub async fn create_unit(
10    state: web::Data<AppState>,
11    user: AuthenticatedUser, // JWT-extracted user info (SECURE!)
12    dto: web::Json<CreateUnitDto>,
13) -> impl Responder {
14    // Only SuperAdmin can create units (structural data)
15    if user.role != "superadmin" {
16        return HttpResponse::Forbidden().json(serde_json::json!({
17            "error": "Only SuperAdmin can create units (structural data cannot be modified after creation)"
18        }));
19    }
20
21    // SuperAdmin must specify organization_id and building_id in the request body
22    // Validate that both are provided
23    if dto.organization_id.is_empty() {
24        return HttpResponse::BadRequest().json(serde_json::json!({
25            "error": "SuperAdmin must specify organization_id"
26        }));
27    }
28
29    if dto.building_id.is_empty() {
30        return HttpResponse::BadRequest().json(serde_json::json!({
31            "error": "SuperAdmin must specify building_id"
32        }));
33    }
34
35    // Parse organization_id for audit logging
36    let organization_id = match Uuid::parse_str(&dto.organization_id) {
37        Ok(org_id) => org_id,
38        Err(_) => {
39            return HttpResponse::BadRequest().json(serde_json::json!({
40                "error": "Invalid organization_id format"
41            }))
42        }
43    };
44
45    if let Err(errors) = dto.validate() {
46        return HttpResponse::BadRequest().json(serde_json::json!({
47            "error": "Validation failed",
48            "details": errors.to_string()
49        }));
50    }
51
52    match state.unit_use_cases.create_unit(dto.into_inner()).await {
53        Ok(unit) => {
54            // Audit log: successful unit creation
55            AuditLogEntry::new(
56                AuditEventType::UnitCreated,
57                Some(user.user_id),
58                Some(organization_id),
59            )
60            .with_resource("Unit", Uuid::parse_str(&unit.id).unwrap())
61            .log();
62
63            HttpResponse::Created().json(unit)
64        }
65        Err(err) => {
66            // Audit log: failed unit creation
67            AuditLogEntry::new(
68                AuditEventType::UnitCreated,
69                Some(user.user_id),
70                Some(organization_id),
71            )
72            .with_error(err.clone())
73            .log();
74
75            HttpResponse::BadRequest().json(serde_json::json!({
76                "error": err
77            }))
78        }
79    }
80}
81
82#[get("/units/{id}")]
83pub async fn get_unit(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
84    match state.unit_use_cases.get_unit(*id).await {
85        Ok(Some(unit)) => HttpResponse::Ok().json(unit),
86        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
87            "error": "Unit not found"
88        })),
89        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
90            "error": err
91        })),
92    }
93}
94
95#[get("/units")]
96pub async fn list_units(
97    state: web::Data<AppState>,
98    user: AuthenticatedUser,
99    page_request: web::Query<PageRequest>,
100) -> impl Responder {
101    let organization_id = user.organization_id;
102
103    match state
104        .unit_use_cases
105        .list_units_paginated(&page_request, organization_id)
106        .await
107    {
108        Ok((units, total)) => {
109            let response =
110                PageResponse::new(units, page_request.page, page_request.per_page, total);
111            HttpResponse::Ok().json(response)
112        }
113        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
114            "error": err
115        })),
116    }
117}
118
119#[get("/buildings/{building_id}/units")]
120pub async fn list_units_by_building(
121    state: web::Data<AppState>,
122    building_id: web::Path<Uuid>,
123) -> impl Responder {
124    match state
125        .unit_use_cases
126        .list_units_by_building(*building_id)
127        .await
128    {
129        Ok(units) => HttpResponse::Ok().json(units),
130        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
131            "error": err
132        })),
133    }
134}
135
136#[put("/units/{id}")]
137pub async fn update_unit(
138    state: web::Data<AppState>,
139    user: AuthenticatedUser,
140    id: web::Path<Uuid>,
141    dto: web::Json<UpdateUnitDto>,
142) -> impl Responder {
143    // Only SuperAdmin can update units (structural data including quotités)
144    if user.role != "superadmin" {
145        return HttpResponse::Forbidden().json(serde_json::json!({
146            "error": "Only SuperAdmin can update units (structural data including quotités)"
147        }));
148    }
149
150    if let Err(errors) = dto.validate() {
151        return HttpResponse::BadRequest().json(serde_json::json!({
152            "error": "Validation failed",
153            "details": errors.to_string()
154        }));
155    }
156
157    // Verify the user owns the unit (via building organization check)
158    if user.role != "superadmin" {
159        match state.unit_use_cases.get_unit(*id).await {
160            Ok(Some(unit)) => {
161                // Get the building to check organization
162                let building_id = match Uuid::parse_str(&unit.building_id) {
163                    Ok(id) => id,
164                    Err(_) => {
165                        return HttpResponse::InternalServerError().json(serde_json::json!({
166                            "error": "Invalid building_id"
167                        }));
168                    }
169                };
170
171                match state.building_use_cases.get_building(building_id).await {
172                    Ok(Some(building)) => {
173                        let building_org_id = match Uuid::parse_str(&building.organization_id) {
174                            Ok(id) => id,
175                            Err(_) => {
176                                return HttpResponse::InternalServerError().json(
177                                    serde_json::json!({
178                                        "error": "Invalid building organization_id"
179                                    }),
180                                );
181                            }
182                        };
183
184                        let user_org_id = match user.require_organization() {
185                            Ok(id) => id,
186                            Err(e) => {
187                                return HttpResponse::Unauthorized().json(serde_json::json!({
188                                    "error": e.to_string()
189                                }));
190                            }
191                        };
192
193                        if building_org_id != user_org_id {
194                            return HttpResponse::Forbidden().json(serde_json::json!({
195                                "error": "You can only update units in your own organization"
196                            }));
197                        }
198                    }
199                    Ok(None) => {
200                        return HttpResponse::NotFound().json(serde_json::json!({
201                            "error": "Building not found"
202                        }));
203                    }
204                    Err(err) => {
205                        return HttpResponse::InternalServerError().json(serde_json::json!({
206                            "error": err
207                        }));
208                    }
209                }
210            }
211            Ok(None) => {
212                return HttpResponse::NotFound().json(serde_json::json!({
213                    "error": "Unit not found"
214                }));
215            }
216            Err(err) => {
217                return HttpResponse::InternalServerError().json(serde_json::json!({
218                    "error": err
219                }));
220            }
221        }
222    }
223
224    match state
225        .unit_use_cases
226        .update_unit(*id, dto.into_inner())
227        .await
228    {
229        Ok(unit) => {
230            // Audit log: successful unit update
231            AuditLogEntry::new(
232                AuditEventType::UnitUpdated,
233                Some(user.user_id),
234                user.organization_id,
235            )
236            .with_resource("Unit", *id)
237            .log();
238
239            HttpResponse::Ok().json(unit)
240        }
241        Err(err) => {
242            // Audit log: failed unit update
243            AuditLogEntry::new(
244                AuditEventType::UnitUpdated,
245                Some(user.user_id),
246                user.organization_id,
247            )
248            .with_resource("Unit", *id)
249            .with_error(err.clone())
250            .log();
251
252            HttpResponse::BadRequest().json(serde_json::json!({
253                "error": err
254            }))
255        }
256    }
257}
258
259#[delete("/units/{id}")]
260pub async fn delete_unit(
261    state: web::Data<AppState>,
262    user: AuthenticatedUser,
263    id: web::Path<Uuid>,
264) -> impl Responder {
265    // Only SuperAdmin can delete units (structural data)
266    if user.role != "superadmin" {
267        return HttpResponse::Forbidden().json(serde_json::json!({
268            "error": "Only SuperAdmin can delete units (structural data)"
269        }));
270    }
271
272    match state.unit_use_cases.delete_unit(*id).await {
273        Ok(true) => {
274            // Audit log: successful unit deletion
275            AuditLogEntry::new(
276                AuditEventType::UnitDeleted,
277                Some(user.user_id),
278                user.organization_id,
279            )
280            .with_resource("Unit", *id)
281            .log();
282
283            HttpResponse::Ok().json(serde_json::json!({
284                "message": "Unit deleted successfully"
285            }))
286        }
287        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
288            "error": "Unit not found"
289        })),
290        Err(err) => {
291            // Audit log: failed unit deletion
292            AuditLogEntry::new(
293                AuditEventType::UnitDeleted,
294                Some(user.user_id),
295                user.organization_id,
296            )
297            .with_resource("Unit", *id)
298            .with_error(err.clone())
299            .log();
300
301            HttpResponse::BadRequest().json(serde_json::json!({
302                "error": err
303            }))
304        }
305    }
306}
307
308#[put("/units/{unit_id}/assign-owner/{owner_id}")]
309pub async fn assign_owner(
310    state: web::Data<AppState>,
311    user: AuthenticatedUser,
312    path: web::Path<(Uuid, Uuid)>,
313) -> impl Responder {
314    let (unit_id, owner_id) = path.into_inner();
315
316    match state.unit_use_cases.assign_owner(unit_id, owner_id).await {
317        Ok(unit) => {
318            // Audit log: successful unit assignment
319            AuditLogEntry::new(
320                AuditEventType::UnitAssignedToOwner,
321                Some(user.user_id),
322                user.organization_id,
323            )
324            .with_resource("Unit", unit_id)
325            .log();
326
327            HttpResponse::Ok().json(unit)
328        }
329        Err(err) => {
330            // Audit log: failed unit assignment
331            AuditLogEntry::new(
332                AuditEventType::UnitAssignedToOwner,
333                Some(user.user_id),
334                user.organization_id,
335            )
336            .with_resource("Unit", unit_id)
337            .with_error(err.clone())
338            .log();
339
340            HttpResponse::BadRequest().json(serde_json::json!({
341                "error": err
342            }))
343        }
344    }
345}