1use crate::application::dto::{
2 CastVoteRequest, ChangeVoteRequest, CloseVotingRequest, CreateResolutionRequest,
3 ResolutionResponse, VoteResponse,
4};
5use crate::infrastructure::audit::{AuditEventType, AuditLogEntry};
6use crate::infrastructure::web::{AppState, AuthenticatedUser};
7use actix_web::{delete, get, post, put, web, HttpResponse, Responder};
8use uuid::Uuid;
9
10#[utoipa::path(
13 post,
14 path = "/meetings/{meeting_id}/resolutions",
15 tag = "Resolutions",
16 summary = "Create a resolution for a meeting",
17 params(
18 ("meeting_id" = Uuid, Path, description = "Meeting UUID")
19 ),
20 request_body = CreateResolutionRequest,
21 responses(
22 (status = 201, description = "Resolution created"),
23 (status = 400, description = "Bad Request"),
24 (status = 401, description = "Unauthorized"),
25 ),
26 security(("bearer_auth" = []))
27)]
28#[post("/meetings/{meeting_id}/resolutions")]
29pub async fn create_resolution(
30 state: web::Data<AppState>,
31 user: AuthenticatedUser,
32 meeting_id: web::Path<Uuid>,
33 request: web::Json<CreateResolutionRequest>,
34) -> impl Responder {
35 let organization_id = match user.require_organization() {
36 Ok(org_id) => org_id,
37 Err(e) => {
38 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
39 }
40 };
41
42 let meeting_id = *meeting_id;
43
44 match state
45 .resolution_use_cases
46 .create_resolution(
47 meeting_id,
48 request.title.clone(),
49 request.description.clone(),
50 request.resolution_type.clone(),
51 request.majority_required.clone(),
52 request.agenda_item_index,
53 )
54 .await
55 {
56 Ok(resolution) => {
57 AuditLogEntry::new(
58 AuditEventType::ResolutionCreated,
59 Some(user.user_id),
60 Some(organization_id),
61 )
62 .with_resource("Resolution", resolution.id)
63 .log();
64
65 HttpResponse::Created().json(ResolutionResponse::from(resolution))
66 }
67 Err(err) => {
68 AuditLogEntry::new(
69 AuditEventType::ResolutionCreated,
70 Some(user.user_id),
71 Some(organization_id),
72 )
73 .with_error(err.clone())
74 .log();
75
76 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
77 }
78 }
79}
80
81#[utoipa::path(
82 get,
83 path = "/resolutions/{id}",
84 tag = "Resolutions",
85 summary = "Get resolution by ID",
86 params(
87 ("id" = Uuid, Path, description = "Resolution UUID")
88 ),
89 responses(
90 (status = 200, description = "Resolution found"),
91 (status = 404, description = "Resolution not found"),
92 (status = 500, description = "Internal Server Error"),
93 ),
94 security(("bearer_auth" = []))
95)]
96#[get("/resolutions/{id}")]
97pub async fn get_resolution(
98 state: web::Data<AppState>,
99 user: AuthenticatedUser,
100 id: web::Path<Uuid>,
101) -> impl Responder {
102 match state.resolution_use_cases.get_resolution(*id).await {
103 Ok(Some(resolution)) => {
104 if let Ok(Some(meeting)) = state
107 .meeting_use_cases
108 .get_meeting(resolution.meeting_id)
109 .await
110 {
111 if let Ok(Some(building)) = state
112 .building_use_cases
113 .get_building(meeting.building_id)
114 .await
115 {
116 if let Ok(building_org) = Uuid::parse_str(&building.organization_id) {
117 if let Err(e) = user.verify_org_access(building_org) {
118 return HttpResponse::Forbidden()
119 .json(serde_json::json!({ "error": e }));
120 }
121 }
122 }
123 }
124 HttpResponse::Ok().json(ResolutionResponse::from(resolution))
125 }
126 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
127 "error": "Resolution not found"
128 })),
129 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
130 "error": err
131 })),
132 }
133}
134
135#[utoipa::path(
136 get,
137 path = "/meetings/{meeting_id}/resolutions",
138 tag = "Resolutions",
139 summary = "List all resolutions for a meeting",
140 params(
141 ("meeting_id" = Uuid, Path, description = "Meeting UUID")
142 ),
143 responses(
144 (status = 200, description = "List of resolutions"),
145 (status = 500, description = "Internal Server Error"),
146 ),
147 security(("bearer_auth" = []))
148)]
149#[get("/meetings/{meeting_id}/resolutions")]
150pub async fn list_meeting_resolutions(
151 state: web::Data<AppState>,
152 meeting_id: web::Path<Uuid>,
153) -> impl Responder {
154 match state
155 .resolution_use_cases
156 .get_meeting_resolutions(*meeting_id)
157 .await
158 {
159 Ok(resolutions) => {
160 let responses: Vec<ResolutionResponse> = resolutions
161 .into_iter()
162 .map(ResolutionResponse::from)
163 .collect();
164 HttpResponse::Ok().json(responses)
165 }
166 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
167 "error": err
168 })),
169 }
170}
171
172#[utoipa::path(
173 delete,
174 path = "/resolutions/{id}",
175 tag = "Resolutions",
176 summary = "Delete a resolution",
177 params(
178 ("id" = Uuid, Path, description = "Resolution UUID")
179 ),
180 responses(
181 (status = 204, description = "Resolution deleted"),
182 (status = 400, description = "Bad Request"),
183 (status = 401, description = "Unauthorized"),
184 (status = 404, description = "Resolution not found"),
185 ),
186 security(("bearer_auth" = []))
187)]
188#[delete("/resolutions/{id}")]
189pub async fn delete_resolution(
190 state: web::Data<AppState>,
191 user: AuthenticatedUser,
192 id: web::Path<Uuid>,
193) -> impl Responder {
194 let organization_id = match user.require_organization() {
195 Ok(org_id) => org_id,
196 Err(e) => {
197 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
198 }
199 };
200
201 match state.resolution_use_cases.delete_resolution(*id).await {
202 Ok(true) => {
203 AuditLogEntry::new(
204 AuditEventType::ResolutionDeleted,
205 Some(user.user_id),
206 Some(organization_id),
207 )
208 .with_resource("Resolution", *id)
209 .log();
210
211 HttpResponse::NoContent().finish()
212 }
213 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
214 "error": "Resolution not found"
215 })),
216 Err(err) => {
217 AuditLogEntry::new(
218 AuditEventType::ResolutionDeleted,
219 Some(user.user_id),
220 Some(organization_id),
221 )
222 .with_error(err.clone())
223 .log();
224
225 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
226 }
227 }
228}
229
230#[utoipa::path(
233 post,
234 path = "/resolutions/{resolution_id}/vote",
235 tag = "Resolutions",
236 summary = "Cast a vote on a resolution",
237 params(
238 ("resolution_id" = Uuid, Path, description = "Resolution UUID")
239 ),
240 request_body = CastVoteRequest,
241 responses(
242 (status = 201, description = "Vote cast"),
243 (status = 400, description = "Bad Request"),
244 (status = 401, description = "Unauthorized"),
245 ),
246 security(("bearer_auth" = []))
247)]
248#[post("/resolutions/{resolution_id}/vote")]
249pub async fn cast_vote(
250 state: web::Data<AppState>,
251 user: AuthenticatedUser,
252 resolution_id: web::Path<Uuid>,
253 request: web::Json<CastVoteRequest>,
254) -> impl Responder {
255 let organization_id = match user.require_organization() {
256 Ok(org_id) => org_id,
257 Err(e) => {
258 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
259 }
260 };
261
262 match state
263 .resolution_use_cases
264 .cast_vote(
265 *resolution_id,
266 request.owner_id,
267 request.unit_id,
268 request.vote_choice.clone(),
269 request.voting_power,
270 request.proxy_owner_id,
271 )
272 .await
273 {
274 Ok(vote) => {
275 AuditLogEntry::new(
276 AuditEventType::VoteCast,
277 Some(user.user_id),
278 Some(organization_id),
279 )
280 .with_resource("Vote", vote.id)
281 .with_metadata(serde_json::json!({
282 "resolution_id": *resolution_id,
283 "vote_choice": format!("{:?}", vote.vote_choice)
284 }))
285 .log();
286
287 HttpResponse::Created().json(VoteResponse::from(vote))
288 }
289 Err(err) => {
290 AuditLogEntry::new(
291 AuditEventType::VoteCast,
292 Some(user.user_id),
293 Some(organization_id),
294 )
295 .with_error(err.clone())
296 .log();
297
298 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
299 }
300 }
301}
302
303#[utoipa::path(
304 get,
305 path = "/resolutions/{resolution_id}/votes",
306 tag = "Resolutions",
307 summary = "List all votes for a resolution",
308 params(
309 ("resolution_id" = Uuid, Path, description = "Resolution UUID")
310 ),
311 responses(
312 (status = 200, description = "List of votes"),
313 (status = 500, description = "Internal Server Error"),
314 ),
315 security(("bearer_auth" = []))
316)]
317#[get("/resolutions/{resolution_id}/votes")]
318pub async fn list_resolution_votes(
319 state: web::Data<AppState>,
320 resolution_id: web::Path<Uuid>,
321) -> impl Responder {
322 match state
323 .resolution_use_cases
324 .get_resolution_votes(*resolution_id)
325 .await
326 {
327 Ok(votes) => {
328 let responses: Vec<VoteResponse> = votes.into_iter().map(VoteResponse::from).collect();
329 HttpResponse::Ok().json(responses)
330 }
331 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
332 "error": err
333 })),
334 }
335}
336
337#[utoipa::path(
338 put,
339 path = "/votes/{vote_id}",
340 tag = "Resolutions",
341 summary = "Change an existing vote",
342 params(
343 ("vote_id" = Uuid, Path, description = "Vote UUID")
344 ),
345 request_body = ChangeVoteRequest,
346 responses(
347 (status = 200, description = "Vote changed"),
348 (status = 400, description = "Bad Request"),
349 (status = 401, description = "Unauthorized"),
350 ),
351 security(("bearer_auth" = []))
352)]
353#[put("/votes/{vote_id}")]
354pub async fn change_vote(
355 state: web::Data<AppState>,
356 user: AuthenticatedUser,
357 vote_id: web::Path<Uuid>,
358 request: web::Json<ChangeVoteRequest>,
359) -> impl Responder {
360 let organization_id = match user.require_organization() {
361 Ok(org_id) => org_id,
362 Err(e) => {
363 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
364 }
365 };
366
367 match state
368 .resolution_use_cases
369 .change_vote(*vote_id, request.vote_choice.clone())
370 .await
371 {
372 Ok(vote) => {
373 AuditLogEntry::new(
374 AuditEventType::VoteChanged,
375 Some(user.user_id),
376 Some(organization_id),
377 )
378 .with_resource("Vote", vote.id)
379 .with_metadata(serde_json::json!({
380 "new_choice": format!("{:?}", vote.vote_choice)
381 }))
382 .log();
383
384 HttpResponse::Ok().json(VoteResponse::from(vote))
385 }
386 Err(err) => {
387 AuditLogEntry::new(
388 AuditEventType::VoteChanged,
389 Some(user.user_id),
390 Some(organization_id),
391 )
392 .with_error(err.clone())
393 .log();
394
395 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
396 }
397 }
398}
399
400#[utoipa::path(
401 put,
402 path = "/resolutions/{resolution_id}/close",
403 tag = "Resolutions",
404 summary = "Close voting on a resolution and calculate result",
405 params(
406 ("resolution_id" = Uuid, Path, description = "Resolution UUID")
407 ),
408 request_body = CloseVotingRequest,
409 responses(
410 (status = 200, description = "Voting closed and result calculated"),
411 (status = 400, description = "Bad Request"),
412 (status = 401, description = "Unauthorized"),
413 ),
414 security(("bearer_auth" = []))
415)]
416#[put("/resolutions/{resolution_id}/close")]
417pub async fn close_voting(
418 state: web::Data<AppState>,
419 user: AuthenticatedUser,
420 resolution_id: web::Path<Uuid>,
421 request: web::Json<CloseVotingRequest>,
422) -> impl Responder {
423 let organization_id = match user.require_organization() {
424 Ok(org_id) => org_id,
425 Err(e) => {
426 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
427 }
428 };
429
430 match state
431 .resolution_use_cases
432 .close_voting(*resolution_id, request.total_voting_power)
433 .await
434 {
435 Ok(resolution) => {
436 AuditLogEntry::new(
437 AuditEventType::VotingClosed,
438 Some(user.user_id),
439 Some(organization_id),
440 )
441 .with_resource("Resolution", resolution.id)
442 .with_metadata(serde_json::json!({
443 "final_status": format!("{:?}", resolution.status)
444 }))
445 .log();
446
447 HttpResponse::Ok().json(ResolutionResponse::from(resolution))
448 }
449 Err(err) => {
450 AuditLogEntry::new(
451 AuditEventType::VotingClosed,
452 Some(user.user_id),
453 Some(organization_id),
454 )
455 .with_error(err.clone())
456 .log();
457
458 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
459 }
460 }
461}
462
463#[utoipa::path(
464 get,
465 path = "/meetings/{meeting_id}/vote-summary",
466 tag = "Resolutions",
467 summary = "Get vote summary for a meeting",
468 params(
469 ("meeting_id" = Uuid, Path, description = "Meeting UUID")
470 ),
471 responses(
472 (status = 200, description = "Vote summary for all meeting resolutions"),
473 (status = 500, description = "Internal Server Error"),
474 ),
475 security(("bearer_auth" = []))
476)]
477#[get("/meetings/{meeting_id}/vote-summary")]
478pub async fn get_meeting_vote_summary(
479 state: web::Data<AppState>,
480 meeting_id: web::Path<Uuid>,
481) -> impl Responder {
482 match state
483 .resolution_use_cases
484 .get_meeting_vote_summary(*meeting_id)
485 .await
486 {
487 Ok(resolutions) => {
488 let responses: Vec<ResolutionResponse> = resolutions
489 .into_iter()
490 .map(ResolutionResponse::from)
491 .collect();
492 HttpResponse::Ok().json(responses)
493 }
494 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
495 "error": err
496 })),
497 }
498}