koprogo_api/infrastructure/web/handlers/
cdc_handlers.rs1use crate::application::dto::{CreateBoardAlertDto, ElectCdcMembersDto};
4use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
5use crate::infrastructure::web::middleware::scope_guard::verify_acp_org_access;
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{get, post, web, HttpResponse, Responder, ResponseError};
8use uuid::Uuid;
9
10async fn owner_id_of(
17 state: &AppState,
18 user: &AuthenticatedUser,
19) -> Result<Uuid, Box<HttpResponse>> {
20 match state
21 .owner_use_cases
22 .find_owner_by_user_id(user.user_id)
23 .await
24 {
25 Ok(Some(owner)) => Uuid::parse_str(&owner.id).map_err(|_| {
26 Box::new(HttpResponse::InternalServerError().json(serde_json::json!({
27 "error": "Invalid owner id"
28 })))
29 }),
30 Ok(None) => Err(Box::new(HttpResponse::Forbidden().json(
31 serde_json::json!({
32 "error": "Cette action est réservée aux membres du conseil de copropriété, \
33 qui sont copropriétaires (Art. 3.90 §1er CC). Votre compte n'est \
34 rattaché à aucune fiche de copropriétaire."
35 }),
36 ))),
37 Err(err) => Err(Box::new(HttpResponse::InternalServerError().json(
38 serde_json::json!({ "error": format!("Database error: {}", err) }),
39 ))),
40 }
41}
42
43#[utoipa::path(
45 post,
46 path = "/buildings/{building_id}/cdc/alerts",
47 tag = "Cdc",
48 summary = "Émettre une alerte du conseil de copropriété",
49 params(("building_id" = Uuid, Path, description = "UUID de l'immeuble")),
50 request_body = CreateBoardAlertDto,
51 responses(
52 (status = 201, description = "Alerte émise", body = crate::application::dto::BoardAlertResponseDto),
53 (status = 403, description = "Hors mandat ou hors portée"),
54 (status = 422, description = "Sévérité ou AG cible invalide"),
55 ),
56 security(("bearer_auth" = []))
57)]
58#[post("/buildings/{building_id}/cdc/alerts")]
59pub async fn create_cdc_alert(
60 state: web::Data<AppState>,
61 user: AuthenticatedUser,
62 building_id: web::Path<Uuid>,
63 request: web::Json<CreateBoardAlertDto>,
64) -> impl Responder {
65 let owner_id = match owner_id_of(&state, &user).await {
66 Ok(id) => id,
67 Err(resp) => return *resp,
68 };
69 let building_id = building_id.into_inner();
70
71 match state
72 .cdc_use_cases
73 .create_alert(owner_id, building_id, request.into_inner())
74 .await
75 {
76 Ok(alert) => {
77 if let Ok(alert_id) = Uuid::parse_str(&alert.id) {
78 AuditLogEntry::new(
79 AuditEventType::CdcAlertCreated,
80 Some(user.user_id),
81 user.organization_id,
82 )
83 .with_resource("BoardAlert", alert_id)
84 .log();
85 }
86 HttpResponse::Created().json(alert)
87 }
88 Err(err) => {
89 AuditLogEntry::new(
90 AuditEventType::CdcAlertCreated,
91 Some(user.user_id),
92 user.organization_id,
93 )
94 .with_error(err.to_string())
95 .log();
96 err.error_response()
97 }
98 }
99}
100
101#[utoipa::path(
103 get,
104 path = "/meetings/{meeting_id}/cdc/alerts",
105 tag = "Cdc",
106 summary = "Alertes du conseil rattachées à une AG",
107 params(("meeting_id" = Uuid, Path, description = "UUID de l'assemblée")),
108 responses(
109 (status = 200, description = "Alertes de l'AG", body = Vec<crate::application::dto::BoardAlertResponseDto>),
110 (status = 403, description = "Hors portée (cloisonnement #882)"),
111 (status = 404, description = "Assemblée introuvable"),
112 ),
113 security(("bearer_auth" = []))
114)]
115#[get("/meetings/{meeting_id}/cdc/alerts")]
116pub async fn list_cdc_alerts_for_meeting(
117 state: web::Data<AppState>,
118 user: AuthenticatedUser,
119 meeting_id: web::Path<Uuid>,
120) -> impl Responder {
121 let meeting_id = meeting_id.into_inner();
122
123 let meeting = match state.meeting_use_cases.get_meeting(meeting_id).await {
137 Ok(Some(m)) => m,
138 Ok(None) => {
139 return HttpResponse::NotFound().json(serde_json::json!({
140 "error": "Meeting not found"
141 }))
142 }
143 Err(err) => {
144 return HttpResponse::InternalServerError().json(serde_json::json!({
145 "error": err.to_string()
146 }))
147 }
148 };
149 let acp_id = match state
153 .building_use_cases
154 .get_building(meeting.building_id)
155 .await
156 {
157 Ok(Some(b)) => match Uuid::parse_str(&b.acp_id) {
158 Ok(id) => id,
159 Err(_) => {
160 return HttpResponse::Forbidden().json(serde_json::json!({
161 "error": "Impossible de rattacher cette assemblée à une ACP"
162 }))
163 }
164 },
165 _ => {
166 return HttpResponse::Forbidden().json(serde_json::json!({
167 "error": "Impossible de rattacher cette assemblée à une ACP"
168 }))
169 }
170 };
171 if let Err(err) = verify_acp_org_access(&user, acp_id, &state.acp_use_cases).await {
172 return err.error_response();
173 }
174
175 match state
176 .cdc_use_cases
177 .list_alerts_for_meeting(meeting_id)
178 .await
179 {
180 Ok(alerts) => HttpResponse::Ok().json(alerts),
181 Err(err) => err.error_response(),
182 }
183}
184
185#[utoipa::path(
187 post,
188 path = "/buildings/{building_id}/cdc/elections",
189 tag = "Cdc",
190 summary = "Élire les membres du conseil de copropriété",
191 params(("building_id" = Uuid, Path, description = "UUID de l'immeuble")),
192 request_body = ElectCdcMembersDto,
193 responses(
194 (status = 201, description = "Membres élus", body = Vec<crate::application::dto::BoardMemberResponseDto>),
195 (status = 403, description = "Hors portée"),
196 (status = 422, description = "Quorum non atteint ou candidature invalide"),
197 ),
198 security(("bearer_auth" = []))
199)]
200#[post("/buildings/{building_id}/cdc/elections")]
201pub async fn elect_cdc_members(
202 state: web::Data<AppState>,
203 user: AuthenticatedUser,
204 building_id: web::Path<Uuid>,
205 request: web::Json<ElectCdcMembersDto>,
206) -> impl Responder {
207 match state
208 .cdc_use_cases
209 .elect_members(building_id.into_inner(), request.into_inner())
210 .await
211 {
212 Ok(members) => {
213 AuditLogEntry::new(
214 AuditEventType::CdcMembersElected,
215 Some(user.user_id),
216 user.organization_id,
217 )
218 .log();
219 HttpResponse::Created().json(members)
220 }
221 Err(err) => {
222 AuditLogEntry::new(
223 AuditEventType::CdcMembersElected,
224 Some(user.user_id),
225 user.organization_id,
226 )
227 .with_error(err.to_string())
228 .log();
229 err.error_response()
230 }
231 }
232}