koprogo_api/infrastructure/web/handlers/
board_member_handlers.rs

1use crate::application::dto::{CreateBoardMemberDto, RenewMandateDto};
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;
6
7/// Élire un nouveau membre du conseil de copropriété
8#[post("/board-members")]
9pub async fn elect_board_member(
10    state: web::Data<AppState>,
11    user: AuthenticatedUser,
12    request: web::Json<CreateBoardMemberDto>,
13) -> impl Responder {
14    // SuperAdmin can elect members for any organization, others need to belong to an organization
15    let organization_id = if user.role == "superadmin" {
16        // For superadmin, get organization_id from the building
17        None // Will be determined by the use case
18    } else {
19        match user.require_organization() {
20            Ok(org_id) => Some(org_id),
21            Err(e) => {
22                return HttpResponse::Unauthorized().json(serde_json::json!({
23                    "error": e.to_string()
24                }))
25            }
26        }
27    };
28
29    match state
30        .board_member_use_cases
31        .elect_board_member(request.into_inner())
32        .await
33    {
34        Ok(member) => {
35            // Audit log: successful board member election
36            if let Ok(member_uuid) = Uuid::parse_str(&member.id) {
37                AuditLogEntry::new(
38                    AuditEventType::BoardMemberElected,
39                    Some(user.user_id),
40                    organization_id,
41                )
42                .with_resource("BoardMember", member_uuid)
43                .log();
44            }
45
46            HttpResponse::Created().json(member)
47        }
48        Err(err) => {
49            // Audit log: failed board member election
50            AuditLogEntry::new(
51                AuditEventType::BoardMemberElected,
52                Some(user.user_id),
53                organization_id,
54            )
55            .with_error(err.clone())
56            .log();
57
58            HttpResponse::BadRequest().json(serde_json::json!({
59                "error": err
60            }))
61        }
62    }
63}
64
65/// Récupérer un membre du conseil par ID
66#[get("/board-members/{id}")]
67pub async fn get_board_member(
68    state: web::Data<AppState>,
69    user: AuthenticatedUser,
70    id: web::Path<Uuid>,
71) -> impl Responder {
72    let _organization_id = match user.require_organization() {
73        Ok(org_id) => org_id,
74        Err(e) => {
75            return HttpResponse::Unauthorized().json(serde_json::json!({
76                "error": e.to_string()
77            }))
78        }
79    };
80
81    match state.board_member_use_cases.get_board_member(*id).await {
82        Ok(Some(member)) => HttpResponse::Ok().json(member),
83        Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
84            "error": "Board member not found"
85        })),
86        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
87            "error": err
88        })),
89    }
90}
91
92/// Lister tous les membres actifs du conseil pour un immeuble
93#[get("/buildings/{building_id}/board-members/active")]
94pub async fn list_active_board_members(
95    state: web::Data<AppState>,
96    user: AuthenticatedUser,
97    building_id: web::Path<Uuid>,
98) -> impl Responder {
99    // SuperAdmin can access all buildings, others need to belong to an organization
100    if user.role != "superadmin" {
101        if let Err(e) = user.require_organization() {
102            return HttpResponse::Unauthorized().json(serde_json::json!({
103                "error": e.to_string()
104            }));
105        }
106    }
107
108    match state
109        .board_member_use_cases
110        .list_active_board_members(*building_id)
111        .await
112    {
113        Ok(members) => HttpResponse::Ok().json(members),
114        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
115            "error": err
116        })),
117    }
118}
119
120/// Lister tous les membres du conseil (actifs et historique) pour un immeuble
121#[get("/buildings/{building_id}/board-members")]
122pub async fn list_all_board_members(
123    state: web::Data<AppState>,
124    user: AuthenticatedUser,
125    building_id: web::Path<Uuid>,
126) -> impl Responder {
127    // SuperAdmin can access all buildings, others need to belong to an organization
128    if user.role != "superadmin" {
129        if let Err(e) = user.require_organization() {
130            return HttpResponse::Unauthorized().json(serde_json::json!({
131                "error": e.to_string()
132            }));
133        }
134    }
135
136    match state
137        .board_member_use_cases
138        .list_all_board_members(*building_id)
139        .await
140    {
141        Ok(members) => HttpResponse::Ok().json(members),
142        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
143            "error": err
144        })),
145    }
146}
147
148/// Renouveler le mandat d'un membre du conseil
149#[put("/board-members/{id}/renew")]
150pub async fn renew_mandate(
151    state: web::Data<AppState>,
152    user: AuthenticatedUser,
153    id: web::Path<Uuid>,
154    request: web::Json<RenewMandateDto>,
155) -> impl Responder {
156    let organization_id = match user.require_organization() {
157        Ok(org_id) => org_id,
158        Err(e) => {
159            return HttpResponse::Unauthorized().json(serde_json::json!({
160                "error": e.to_string()
161            }))
162        }
163    };
164
165    match state
166        .board_member_use_cases
167        .renew_mandate(*id, request.into_inner())
168        .await
169    {
170        Ok(member) => {
171            // Audit log: successful mandate renewal
172            if let Ok(member_uuid) = Uuid::parse_str(&member.id) {
173                AuditLogEntry::new(
174                    AuditEventType::BoardMemberMandateRenewed,
175                    Some(user.user_id),
176                    Some(organization_id),
177                )
178                .with_resource("BoardMember", member_uuid)
179                .log();
180            }
181
182            HttpResponse::Ok().json(member)
183        }
184        Err(err) => {
185            // Audit log: failed mandate renewal
186            AuditLogEntry::new(
187                AuditEventType::BoardMemberMandateRenewed,
188                Some(user.user_id),
189                Some(organization_id),
190            )
191            .with_error(err.clone())
192            .log();
193
194            HttpResponse::BadRequest().json(serde_json::json!({
195                "error": err
196            }))
197        }
198    }
199}
200
201/// Retirer un membre du conseil (fin de mandat anticipée)
202#[delete("/board-members/{id}")]
203pub async fn remove_board_member(
204    state: web::Data<AppState>,
205    user: AuthenticatedUser,
206    id: web::Path<Uuid>,
207) -> impl Responder {
208    let organization_id = match user.require_organization() {
209        Ok(org_id) => org_id,
210        Err(e) => {
211            return HttpResponse::Unauthorized().json(serde_json::json!({
212                "error": e.to_string()
213            }))
214        }
215    };
216
217    match state.board_member_use_cases.remove_board_member(*id).await {
218        Ok(true) => {
219            // Audit log: successful board member removal
220            AuditLogEntry::new(
221                AuditEventType::BoardMemberRemoved,
222                Some(user.user_id),
223                Some(organization_id),
224            )
225            .with_resource("BoardMember", *id)
226            .log();
227
228            HttpResponse::NoContent().finish()
229        }
230        Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
231            "error": "Board member not found"
232        })),
233        Err(err) => {
234            // Audit log: failed board member removal
235            AuditLogEntry::new(
236                AuditEventType::BoardMemberRemoved,
237                Some(user.user_id),
238                Some(organization_id),
239            )
240            .with_error(err.clone())
241            .log();
242
243            HttpResponse::InternalServerError().json(serde_json::json!({
244                "error": err
245            }))
246        }
247    }
248}
249
250/// Obtenir des statistiques sur le conseil d'un immeuble
251#[get("/buildings/{building_id}/board-members/stats")]
252pub async fn get_board_stats(
253    state: web::Data<AppState>,
254    user: AuthenticatedUser,
255    building_id: web::Path<Uuid>,
256) -> impl Responder {
257    let _organization_id = match user.require_organization() {
258        Ok(org_id) => org_id,
259        Err(e) => {
260            return HttpResponse::Unauthorized().json(serde_json::json!({
261                "error": e.to_string()
262            }))
263        }
264    };
265
266    match state
267        .board_member_use_cases
268        .get_board_stats(*building_id)
269        .await
270    {
271        Ok(stats) => HttpResponse::Ok().json(stats),
272        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
273            "error": err
274        })),
275    }
276}
277
278/// Obtenir le tableau de bord d'un membre du conseil
279/// Accessible uniquement aux membres du conseil et superadmins
280#[get("/board-members/dashboard")]
281pub async fn get_board_dashboard(
282    state: web::Data<AppState>,
283    user: AuthenticatedUser,
284    query: web::Query<std::collections::HashMap<String, String>>,
285) -> impl Responder {
286    // SuperAdmin can access dashboard for any building, others need to belong to an organization
287    let _organization_id = if user.role != "superadmin" {
288        match user.require_organization() {
289            Ok(org_id) => Some(org_id),
290            Err(e) => {
291                return HttpResponse::Unauthorized().json(serde_json::json!({
292                    "error": e.to_string()
293                }))
294            }
295        }
296    } else {
297        None
298    };
299
300    // Get building_id from query parameters
301    let building_id = match query.get("building_id") {
302        Some(id_str) => match Uuid::parse_str(id_str) {
303            Ok(id) => id,
304            Err(_) => {
305                return HttpResponse::BadRequest().json(serde_json::json!({
306                    "error": "Invalid building_id format"
307                }))
308            }
309        },
310        None => {
311            return HttpResponse::BadRequest().json(serde_json::json!({
312                "error": "building_id query parameter is required"
313            }))
314        }
315    };
316
317    // Get owner_id from user->owner link
318    let owner_id = match state
319        .owner_use_cases
320        .find_owner_by_user_id(user.user_id)
321        .await
322    {
323        Ok(Some(owner_dto)) => uuid::Uuid::parse_str(&owner_dto.id).unwrap_or(user.user_id),
324        Ok(None) => {
325            return HttpResponse::Forbidden().json(serde_json::json!({
326                "error": "User is not linked to an owner. Board dashboard is only accessible to board members."
327            }));
328        }
329        Err(err) => {
330            return HttpResponse::InternalServerError().json(serde_json::json!({
331                "error": format!("Database error: {}", err)
332            }));
333        }
334    };
335
336    // Authorization: Verify user is an active board member for this building (unless superadmin)
337    let is_superadmin = user.role == "superadmin";
338    if !is_superadmin {
339        match state
340            .board_member_use_cases
341            .has_active_board_mandate(owner_id, building_id)
342            .await
343        {
344            Ok(true) => {
345                // User is an active board member, proceed
346            }
347            Ok(false) => {
348                return HttpResponse::Forbidden().json(serde_json::json!({
349                    "error": "Access denied. You are not an active board member for this building."
350                }));
351            }
352            Err(err) => {
353                return HttpResponse::InternalServerError().json(serde_json::json!({
354                    "error": format!("Authorization check failed: {}", err)
355                }));
356            }
357        }
358    }
359
360    match state
361        .board_dashboard_use_cases
362        .get_dashboard(building_id, owner_id)
363        .await
364    {
365        Ok(dashboard) => HttpResponse::Ok().json(dashboard),
366        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
367            "error": err
368        })),
369    }
370}
371
372/// GET /board-members/my-mandates - Get all active board mandates for the authenticated user
373#[get("/board-members/my-mandates")]
374pub async fn get_my_mandates(
375    state: web::Data<AppState>,
376    user: AuthenticatedUser,
377) -> impl Responder {
378    let organization_id = match user.require_organization() {
379        Ok(org_id) => org_id,
380        Err(e) => {
381            return HttpResponse::Unauthorized().json(serde_json::json!({
382                "error": e.to_string()
383            }))
384        }
385    };
386
387    // Get the owner ID for this user
388    let owner_id = match state
389        .owner_use_cases
390        .find_owner_by_user_id_and_organization(user.user_id, organization_id)
391        .await
392    {
393        Ok(Some(owner_dto)) => match uuid::Uuid::parse_str(&owner_dto.id) {
394            Ok(id) => id,
395            Err(_) => {
396                return HttpResponse::InternalServerError().json(serde_json::json!({
397                    "error": "Invalid owner id"
398                }))
399            }
400        },
401        Ok(None) => {
402            return HttpResponse::Ok().json(serde_json::json!({ "mandates": [] }));
403        }
404        Err(err) => {
405            return HttpResponse::InternalServerError().json(serde_json::json!({
406                "error": format!("Failed to fetch owner: {}", err)
407            }));
408        }
409    };
410
411    // Get all active board member mandates for this owner (with building info)
412    match state
413        .board_member_use_cases
414        .get_active_mandates_for_owner(owner_id, organization_id)
415        .await
416    {
417        Ok(mandates) => HttpResponse::Ok().json(serde_json::json!({ "mandates": mandates })),
418        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
419            "error": format!("Failed to fetch mandates: {}", err)
420        })),
421    }
422}