koprogo_api/infrastructure/web/handlers/
unit_handlers.rs1use 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, dto: web::Json<CreateUnitDto>,
13) -> impl Responder {
14 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 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 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 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 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(
84 state: web::Data<AppState>,
85 user: AuthenticatedUser,
86 id: web::Path<Uuid>,
87) -> impl Responder {
88 match state.unit_use_cases.get_unit(*id).await {
89 Ok(Some(unit)) => {
90 if let Ok(building_id) = Uuid::parse_str(&unit.building_id) {
92 if let Ok(Some(building)) = state.building_use_cases.get_building(building_id).await
93 {
94 if let Ok(building_org) = Uuid::parse_str(&building.organization_id) {
95 if let Err(e) = user.verify_org_access(building_org) {
96 return HttpResponse::Forbidden()
97 .json(serde_json::json!({ "error": e }));
98 }
99 }
100 }
101 }
102 HttpResponse::Ok().json(unit)
103 }
104 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
105 "error": "Unit not found"
106 })),
107 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
108 "error": err
109 })),
110 }
111}
112
113#[get("/units")]
114pub async fn list_units(
115 state: web::Data<AppState>,
116 user: AuthenticatedUser,
117 page_request: web::Query<PageRequest>,
118) -> impl Responder {
119 let organization_id = user.organization_id;
120
121 match state
122 .unit_use_cases
123 .list_units_paginated(&page_request, organization_id)
124 .await
125 {
126 Ok((units, total)) => {
127 let response =
128 PageResponse::new(units, page_request.page, page_request.per_page, total);
129 HttpResponse::Ok().json(response)
130 }
131 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
132 "error": err
133 })),
134 }
135}
136
137#[get("/buildings/{building_id}/units")]
138pub async fn list_units_by_building(
139 state: web::Data<AppState>,
140 user: AuthenticatedUser,
141 building_id: web::Path<Uuid>,
142) -> impl Responder {
143 match state.building_use_cases.get_building(*building_id).await {
145 Ok(Some(building)) => {
146 if let Ok(building_org) = Uuid::parse_str(&building.organization_id) {
147 if let Err(e) = user.verify_org_access(building_org) {
148 return HttpResponse::Forbidden().json(serde_json::json!({ "error": e }));
149 }
150 }
151 }
152 Ok(None) => {
153 return HttpResponse::NotFound().json(serde_json::json!({
154 "error": "Building not found"
155 }));
156 }
157 Err(err) => {
158 return HttpResponse::InternalServerError().json(serde_json::json!({
159 "error": err
160 }));
161 }
162 }
163
164 match state
165 .unit_use_cases
166 .list_units_by_building(*building_id)
167 .await
168 {
169 Ok(units) => HttpResponse::Ok().json(units),
170 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
171 "error": err
172 })),
173 }
174}
175
176#[put("/units/{id}")]
177pub async fn update_unit(
178 state: web::Data<AppState>,
179 user: AuthenticatedUser,
180 id: web::Path<Uuid>,
181 dto: web::Json<UpdateUnitDto>,
182) -> impl Responder {
183 if user.role != "superadmin" {
185 return HttpResponse::Forbidden().json(serde_json::json!({
186 "error": "Only SuperAdmin can update units (structural data including quotités)"
187 }));
188 }
189
190 if let Err(errors) = dto.validate() {
191 return HttpResponse::BadRequest().json(serde_json::json!({
192 "error": "Validation failed",
193 "details": errors.to_string()
194 }));
195 }
196
197 if user.role != "superadmin" {
199 match state.unit_use_cases.get_unit(*id).await {
200 Ok(Some(unit)) => {
201 let building_id = match Uuid::parse_str(&unit.building_id) {
203 Ok(id) => id,
204 Err(_) => {
205 return HttpResponse::InternalServerError().json(serde_json::json!({
206 "error": "Invalid building_id"
207 }));
208 }
209 };
210
211 match state.building_use_cases.get_building(building_id).await {
212 Ok(Some(building)) => {
213 let building_org_id = match Uuid::parse_str(&building.organization_id) {
214 Ok(id) => id,
215 Err(_) => {
216 return HttpResponse::InternalServerError().json(
217 serde_json::json!({
218 "error": "Invalid building organization_id"
219 }),
220 );
221 }
222 };
223
224 let user_org_id = match user.require_organization() {
225 Ok(id) => id,
226 Err(e) => {
227 return HttpResponse::Unauthorized().json(serde_json::json!({
228 "error": e.to_string()
229 }));
230 }
231 };
232
233 if building_org_id != user_org_id {
234 return HttpResponse::Forbidden().json(serde_json::json!({
235 "error": "You can only update units in your own organization"
236 }));
237 }
238 }
239 Ok(None) => {
240 return HttpResponse::NotFound().json(serde_json::json!({
241 "error": "Building not found"
242 }));
243 }
244 Err(err) => {
245 return HttpResponse::InternalServerError().json(serde_json::json!({
246 "error": err
247 }));
248 }
249 }
250 }
251 Ok(None) => {
252 return HttpResponse::NotFound().json(serde_json::json!({
253 "error": "Unit not found"
254 }));
255 }
256 Err(err) => {
257 return HttpResponse::InternalServerError().json(serde_json::json!({
258 "error": err
259 }));
260 }
261 }
262 }
263
264 match state
265 .unit_use_cases
266 .update_unit(*id, dto.into_inner())
267 .await
268 {
269 Ok(unit) => {
270 AuditLogEntry::new(
272 AuditEventType::UnitUpdated,
273 Some(user.user_id),
274 user.organization_id,
275 )
276 .with_resource("Unit", *id)
277 .log();
278
279 HttpResponse::Ok().json(unit)
280 }
281 Err(err) => {
282 AuditLogEntry::new(
284 AuditEventType::UnitUpdated,
285 Some(user.user_id),
286 user.organization_id,
287 )
288 .with_resource("Unit", *id)
289 .with_error(err.clone())
290 .log();
291
292 HttpResponse::BadRequest().json(serde_json::json!({
293 "error": err
294 }))
295 }
296 }
297}
298
299#[delete("/units/{id}")]
300pub async fn delete_unit(
301 state: web::Data<AppState>,
302 user: AuthenticatedUser,
303 id: web::Path<Uuid>,
304) -> impl Responder {
305 if user.role != "superadmin" {
307 return HttpResponse::Forbidden().json(serde_json::json!({
308 "error": "Only SuperAdmin can delete units (structural data)"
309 }));
310 }
311
312 match state.unit_use_cases.delete_unit(*id).await {
313 Ok(true) => {
314 AuditLogEntry::new(
316 AuditEventType::UnitDeleted,
317 Some(user.user_id),
318 user.organization_id,
319 )
320 .with_resource("Unit", *id)
321 .log();
322
323 HttpResponse::Ok().json(serde_json::json!({
324 "message": "Unit deleted successfully"
325 }))
326 }
327 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
328 "error": "Unit not found"
329 })),
330 Err(err) => {
331 AuditLogEntry::new(
333 AuditEventType::UnitDeleted,
334 Some(user.user_id),
335 user.organization_id,
336 )
337 .with_resource("Unit", *id)
338 .with_error(err.clone())
339 .log();
340
341 HttpResponse::BadRequest().json(serde_json::json!({
342 "error": err
343 }))
344 }
345 }
346}
347
348#[put("/units/{unit_id}/assign-owner/{owner_id}")]
349pub async fn assign_owner(
350 state: web::Data<AppState>,
351 user: AuthenticatedUser,
352 path: web::Path<(Uuid, Uuid)>,
353) -> impl Responder {
354 let (unit_id, owner_id) = path.into_inner();
355
356 match state.unit_use_cases.assign_owner(unit_id, owner_id).await {
357 Ok(unit) => {
358 AuditLogEntry::new(
360 AuditEventType::UnitAssignedToOwner,
361 Some(user.user_id),
362 user.organization_id,
363 )
364 .with_resource("Unit", unit_id)
365 .log();
366
367 HttpResponse::Ok().json(unit)
368 }
369 Err(err) => {
370 AuditLogEntry::new(
372 AuditEventType::UnitAssignedToOwner,
373 Some(user.user_id),
374 user.organization_id,
375 )
376 .with_resource("Unit", unit_id)
377 .with_error(err.clone())
378 .log();
379
380 HttpResponse::BadRequest().json(serde_json::json!({
381 "error": err
382 }))
383 }
384 }
385}