Skip to main content

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.is_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.is_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.is_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.is_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": "Le tableau de bord du conseil est réservé à ses membres, \
327                          qui sont copropriétaires (Art. 3.90 § 1er). Votre compte \
328                          n'est rattaché à aucune fiche de copropriétaire."
329            }));
330        }
331        Err(err) => {
332            return HttpResponse::InternalServerError().json(serde_json::json!({
333                "error": format!("Database error: {}", err)
334            }));
335        }
336    };
337
338    // Authorization: Verify user is an active board member for this building (unless superadmin)
339    let is_superadmin = user.is_superadmin();
340    if !is_superadmin {
341        match state
342            .board_member_use_cases
343            .has_active_board_mandate(owner_id, building_id)
344            .await
345        {
346            Ok(true) => {
347                // User is an active board member, proceed
348            }
349            Ok(false) => {
350                return HttpResponse::Forbidden().json(serde_json::json!({
351                    "error": "Access denied. You are not an active board member for this building."
352                }));
353            }
354            Err(err) => {
355                return HttpResponse::InternalServerError().json(serde_json::json!({
356                    "error": format!("Authorization check failed: {}", err)
357                }));
358            }
359        }
360    }
361
362    match state
363        .board_dashboard_use_cases
364        .get_dashboard(building_id, owner_id)
365        .await
366    {
367        Ok(dashboard) => HttpResponse::Ok().json(dashboard),
368        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
369            "error": err
370        })),
371    }
372}
373
374/// GET /board-members/my-mandates - Get all active board mandates for the authenticated user
375#[get("/board-members/my-mandates")]
376pub async fn get_my_mandates(
377    state: web::Data<AppState>,
378    user: AuthenticatedUser,
379) -> impl Responder {
380    let organization_id = match user.require_organization() {
381        Ok(org_id) => org_id,
382        Err(e) => {
383            return HttpResponse::Unauthorized().json(serde_json::json!({
384                "error": e.to_string()
385            }))
386        }
387    };
388
389    // Get the owner ID for this user
390    let owner_id = match state
391        .owner_use_cases
392        .find_owner_by_user_id_and_organization(user.user_id, organization_id)
393        .await
394    {
395        Ok(Some(owner_dto)) => match uuid::Uuid::parse_str(&owner_dto.id) {
396            Ok(id) => id,
397            Err(_) => {
398                return HttpResponse::InternalServerError().json(serde_json::json!({
399                    "error": "Invalid owner id"
400                }))
401            }
402        },
403        Ok(None) => {
404            return HttpResponse::Ok().json(serde_json::json!({ "mandates": [] }));
405        }
406        Err(err) => {
407            return HttpResponse::InternalServerError().json(serde_json::json!({
408                "error": format!("Failed to fetch owner: {}", err)
409            }));
410        }
411    };
412
413    // Get all active board member mandates for this owner (with building info)
414    match state
415        .board_member_use_cases
416        .get_active_mandates_for_owner(owner_id, organization_id)
417        .await
418    {
419        Ok(mandates) => HttpResponse::Ok().json(serde_json::json!({ "mandates": mandates })),
420        Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
421            "error": format!("Failed to fetch mandates: {}", err)
422        })),
423    }
424}