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(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 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 if user.role != "superadmin" {
159 match state.unit_use_cases.get_unit(*id).await {
160 Ok(Some(unit)) => {
161 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 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 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 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 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 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 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 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}