1use crate::application::dto::{CreateAcpDto, UpdateAcpDto};
17use crate::application::use_cases::acp_use_cases::AcpCaller;
18use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
19use crate::infrastructure::web::{AppState, AuthenticatedUser};
20use actix_web::{delete, get, post, put, web, HttpResponse, Responder, ResponseError};
21use uuid::Uuid;
22use validator::Validate;
23
24pub(crate) fn caller_from_user(user: &AuthenticatedUser) -> AcpCaller {
27 match user.role.to_lowercase().as_str() {
28 "superadmin" => AcpCaller::SuperAdmin,
29 "admin" => match user.organization_id {
30 Some(org) => AcpCaller::Admin {
31 organization_id: org,
32 },
33 None => AcpCaller::SuperAdmin, },
35 "syndic" | "accountant" => match user.organization_id {
36 Some(org) => AcpCaller::Syndic {
37 organization_id: org,
38 },
39 None => AcpCaller::Owner {
40 user_id: user.user_id,
41 },
42 },
43 _ => AcpCaller::Owner {
44 user_id: user.user_id,
45 },
46 }
47}
48
49#[utoipa::path(
50 post,
51 path = "/acps",
52 tag = "Acps",
53 summary = "Create an ACP (Association des Copropriétaires)",
54 request_body = CreateAcpDto,
55 responses(
56 (status = 201, description = "ACP created", body = crate::application::dto::AcpResponseDto),
57 (status = 400, description = "Validation error"),
58 (status = 403, description = "Forbidden (non-admin)"),
59 (status = 422, description = "Domain validation error"),
60 ),
61 security(("bearer_auth" = []))
62)]
63#[post("/acps")]
64pub async fn create_acp(
65 state: web::Data<AppState>,
66 user: AuthenticatedUser,
67 dto: web::Json<CreateAcpDto>,
68) -> impl Responder {
69 if let Err(errors) = dto.validate() {
70 return HttpResponse::BadRequest().json(serde_json::json!({
71 "error": "Validation failed",
72 "details": errors.to_string(),
73 "kind": "validation",
74 }));
75 }
76
77 let caller = caller_from_user(&user);
78 match state
79 .acp_use_cases
80 .create_acp(&caller, dto.into_inner())
81 .await
82 {
83 Ok(resp) => {
84 if let Ok(acp_uuid) = Uuid::parse_str(&resp.id) {
86 AuditLogEntry::new(
87 AuditEventType::AcpCreated,
88 Some(user.user_id),
89 user.organization_id,
90 )
91 .with_resource("Acp", acp_uuid)
92 .log();
93 }
94 HttpResponse::Created().json(resp)
95 }
96 Err(err) => {
97 AuditLogEntry::new(
98 AuditEventType::AcpCreated,
99 Some(user.user_id),
100 user.organization_id,
101 )
102 .with_error(err.to_string())
103 .log();
104 err.error_response()
105 }
106 }
107}
108
109#[utoipa::path(
110 get,
111 path = "/acps",
112 tag = "Acps",
113 summary = "List ACPs visible to the authenticated user",
114 responses(
115 (status = 200, description = "List of ACPs", body = Vec<crate::application::dto::AcpResponseDto>),
116 (status = 401, description = "Unauthorized"),
117 ),
118 security(("bearer_auth" = []))
119)]
120#[get("/acps")]
121pub async fn list_acps(state: web::Data<AppState>, user: AuthenticatedUser) -> impl Responder {
122 let caller = caller_from_user(&user);
123 match state.acp_use_cases.list_acps(&caller).await {
124 Ok(list) => HttpResponse::Ok().json(list),
125 Err(err) => err.error_response(),
126 }
127}
128
129#[utoipa::path(
130 get,
131 path = "/acps/with-metrics",
132 tag = "Acps",
133 summary = "Les ACP du périmètre, avec leurs métriques agrégées",
134 description = "Sert la table « Mes ACP » du tableau de bord syndic : nombre \
135 de blocs, lots encodés et déclarés, somme des quotités. \
136 Séparée de `GET /acps` parce que les métriques coûtent \
137 quatre sous-requêtes par ligne : un sélecteur qui n'a besoin \
138 que des noms ne doit pas les payer.",
139 responses(
140 (status = 200, description = "Liste des ACP avec métriques"),
141 (status = 401, description = "Non authentifié"),
142 ),
143 security(("bearer_auth" = []))
144)]
145#[get("/acps/with-metrics")]
146pub async fn list_acps_with_metrics(
147 state: web::Data<AppState>,
148 user: AuthenticatedUser,
149) -> impl Responder {
150 let caller = caller_from_user(&user);
151 match state.acp_use_cases.list_acps_with_metrics(&caller).await {
152 Ok(list) => HttpResponse::Ok().json(list),
153 Err(err) => err.error_response(),
154 }
155}
156
157#[utoipa::path(
158 get,
159 path = "/acps/{id}",
160 tag = "Acps",
161 summary = "Get an ACP by id (scope-guarded)",
162 params(("id" = Uuid, Path, description = "ACP UUID")),
163 responses(
164 (status = 200, description = "ACP found", body = crate::application::dto::AcpResponseDto),
165 (status = 403, description = "Out of scope (AcpNotInScope)"),
166 (status = 404, description = "Not found"),
167 ),
168 security(("bearer_auth" = []))
169)]
170#[get("/acps/{id}")]
171pub async fn get_acp(
172 state: web::Data<AppState>,
173 user: AuthenticatedUser,
174 id: web::Path<Uuid>,
175) -> impl Responder {
176 let caller = caller_from_user(&user);
177 match state.acp_use_cases.get_acp(&caller, *id).await {
178 Ok(resp) => HttpResponse::Ok().json(resp),
179 Err(err) => err.error_response(),
180 }
181}
182
183#[utoipa::path(
184 put,
185 path = "/acps/{id}",
186 tag = "Acps",
187 summary = "Update an ACP (admin + scope)",
188 params(("id" = Uuid, Path, description = "ACP UUID")),
189 request_body = UpdateAcpDto,
190 responses(
191 (status = 200, description = "ACP updated", body = crate::application::dto::AcpResponseDto),
192 (status = 400, description = "Validation error"),
193 (status = 403, description = "Forbidden / out of scope"),
194 (status = 404, description = "Not found"),
195 ),
196 security(("bearer_auth" = []))
197)]
198#[put("/acps/{id}")]
199pub async fn update_acp(
200 state: web::Data<AppState>,
201 user: AuthenticatedUser,
202 id: web::Path<Uuid>,
203 dto: web::Json<UpdateAcpDto>,
204) -> impl Responder {
205 if let Err(errors) = dto.validate() {
206 return HttpResponse::BadRequest().json(serde_json::json!({
207 "error": "Validation failed",
208 "details": errors.to_string(),
209 "kind": "validation",
210 }));
211 }
212
213 let acp_id = *id;
214 let caller = caller_from_user(&user);
215 match state
216 .acp_use_cases
217 .update_acp(&caller, acp_id, dto.into_inner())
218 .await
219 {
220 Ok(resp) => {
221 AuditLogEntry::new(
222 AuditEventType::AcpUpdated,
223 Some(user.user_id),
224 user.organization_id,
225 )
226 .with_resource("Acp", acp_id)
227 .log();
228 HttpResponse::Ok().json(resp)
229 }
230 Err(err) => {
231 AuditLogEntry::new(
232 AuditEventType::AcpUpdated,
233 Some(user.user_id),
234 user.organization_id,
235 )
236 .with_resource("Acp", acp_id)
237 .with_error(err.to_string())
238 .log();
239 err.error_response()
240 }
241 }
242}
243
244#[utoipa::path(
245 delete,
246 path = "/acps/{id}",
247 tag = "Acps",
248 summary = "Archive (delete) an ACP (admin + scope)",
249 params(("id" = Uuid, Path, description = "ACP UUID")),
250 responses(
251 (status = 204, description = "ACP archived"),
252 (status = 403, description = "Forbidden / out of scope"),
253 (status = 404, description = "Not found"),
254 (
255 status = 409,
256 description = "ACP still carries buildings — detach or delete them first"
257 ),
258 ),
259 security(("bearer_auth" = []))
260)]
261#[delete("/acps/{id}")]
262pub async fn archive_acp(
263 state: web::Data<AppState>,
264 user: AuthenticatedUser,
265 id: web::Path<Uuid>,
266) -> impl Responder {
267 let acp_id = *id;
268 let caller = caller_from_user(&user);
269 match state.acp_use_cases.archive_acp(&caller, acp_id).await {
270 Ok(()) => {
271 AuditLogEntry::new(
272 AuditEventType::AcpArchived,
273 Some(user.user_id),
274 user.organization_id,
275 )
276 .with_resource("Acp", acp_id)
277 .log();
278 HttpResponse::NoContent().finish()
279 }
280 Err(err) => {
281 AuditLogEntry::new(
282 AuditEventType::AcpArchived,
283 Some(user.user_id),
284 user.organization_id,
285 )
286 .with_resource("Acp", acp_id)
287 .with_error(err.to_string())
288 .log();
289 err.error_response()
290 }
291 }
292}