koprogo_api/infrastructure/web/handlers/
resolution_handlers.rs1use 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#[post("/meetings/{meeting_id}/resolutions")]
13pub async fn create_resolution(
14 state: web::Data<AppState>,
15 user: AuthenticatedUser,
16 meeting_id: web::Path<Uuid>,
17 request: web::Json<CreateResolutionRequest>,
18) -> impl Responder {
19 let organization_id = match user.require_organization() {
20 Ok(org_id) => org_id,
21 Err(e) => {
22 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
23 }
24 };
25
26 let meeting_id = *meeting_id;
27
28 match state
29 .resolution_use_cases
30 .create_resolution(
31 meeting_id,
32 request.title.clone(),
33 request.description.clone(),
34 request.resolution_type.clone(),
35 request.majority_required.clone(),
36 )
37 .await
38 {
39 Ok(resolution) => {
40 AuditLogEntry::new(
41 AuditEventType::ResolutionCreated,
42 Some(user.user_id),
43 Some(organization_id),
44 )
45 .with_resource("Resolution", resolution.id)
46 .log();
47
48 HttpResponse::Created().json(ResolutionResponse::from(resolution))
49 }
50 Err(err) => {
51 AuditLogEntry::new(
52 AuditEventType::ResolutionCreated,
53 Some(user.user_id),
54 Some(organization_id),
55 )
56 .with_error(err.clone())
57 .log();
58
59 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
60 }
61 }
62}
63
64#[get("/resolutions/{id}")]
65pub async fn get_resolution(state: web::Data<AppState>, id: web::Path<Uuid>) -> impl Responder {
66 match state.resolution_use_cases.get_resolution(*id).await {
67 Ok(Some(resolution)) => HttpResponse::Ok().json(ResolutionResponse::from(resolution)),
68 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
69 "error": "Resolution not found"
70 })),
71 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
72 "error": err
73 })),
74 }
75}
76
77#[get("/meetings/{meeting_id}/resolutions")]
78pub async fn list_meeting_resolutions(
79 state: web::Data<AppState>,
80 meeting_id: web::Path<Uuid>,
81) -> impl Responder {
82 match state
83 .resolution_use_cases
84 .get_meeting_resolutions(*meeting_id)
85 .await
86 {
87 Ok(resolutions) => {
88 let responses: Vec<ResolutionResponse> = resolutions
89 .into_iter()
90 .map(ResolutionResponse::from)
91 .collect();
92 HttpResponse::Ok().json(responses)
93 }
94 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
95 "error": err
96 })),
97 }
98}
99
100#[delete("/resolutions/{id}")]
101pub async fn delete_resolution(
102 state: web::Data<AppState>,
103 user: AuthenticatedUser,
104 id: web::Path<Uuid>,
105) -> impl Responder {
106 let organization_id = match user.require_organization() {
107 Ok(org_id) => org_id,
108 Err(e) => {
109 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
110 }
111 };
112
113 match state.resolution_use_cases.delete_resolution(*id).await {
114 Ok(true) => {
115 AuditLogEntry::new(
116 AuditEventType::ResolutionDeleted,
117 Some(user.user_id),
118 Some(organization_id),
119 )
120 .with_resource("Resolution", *id)
121 .log();
122
123 HttpResponse::NoContent().finish()
124 }
125 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
126 "error": "Resolution not found"
127 })),
128 Err(err) => {
129 AuditLogEntry::new(
130 AuditEventType::ResolutionDeleted,
131 Some(user.user_id),
132 Some(organization_id),
133 )
134 .with_error(err.clone())
135 .log();
136
137 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
138 }
139 }
140}
141
142#[post("/resolutions/{resolution_id}/vote")]
145pub async fn cast_vote(
146 state: web::Data<AppState>,
147 user: AuthenticatedUser,
148 resolution_id: web::Path<Uuid>,
149 request: web::Json<CastVoteRequest>,
150) -> impl Responder {
151 let organization_id = match user.require_organization() {
152 Ok(org_id) => org_id,
153 Err(e) => {
154 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
155 }
156 };
157
158 match state
159 .resolution_use_cases
160 .cast_vote(
161 *resolution_id,
162 request.owner_id,
163 request.unit_id,
164 request.vote_choice.clone(),
165 request.voting_power,
166 request.proxy_owner_id,
167 )
168 .await
169 {
170 Ok(vote) => {
171 AuditLogEntry::new(
172 AuditEventType::VoteCast,
173 Some(user.user_id),
174 Some(organization_id),
175 )
176 .with_resource("Vote", vote.id)
177 .with_metadata(serde_json::json!({
178 "resolution_id": *resolution_id,
179 "vote_choice": format!("{:?}", vote.vote_choice)
180 }))
181 .log();
182
183 HttpResponse::Created().json(VoteResponse::from(vote))
184 }
185 Err(err) => {
186 AuditLogEntry::new(
187 AuditEventType::VoteCast,
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!({"error": err}))
195 }
196 }
197}
198
199#[get("/resolutions/{resolution_id}/votes")]
200pub async fn list_resolution_votes(
201 state: web::Data<AppState>,
202 resolution_id: web::Path<Uuid>,
203) -> impl Responder {
204 match state
205 .resolution_use_cases
206 .get_resolution_votes(*resolution_id)
207 .await
208 {
209 Ok(votes) => {
210 let responses: Vec<VoteResponse> = votes.into_iter().map(VoteResponse::from).collect();
211 HttpResponse::Ok().json(responses)
212 }
213 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
214 "error": err
215 })),
216 }
217}
218
219#[put("/votes/{vote_id}")]
220pub async fn change_vote(
221 state: web::Data<AppState>,
222 user: AuthenticatedUser,
223 vote_id: web::Path<Uuid>,
224 request: web::Json<ChangeVoteRequest>,
225) -> impl Responder {
226 let organization_id = match user.require_organization() {
227 Ok(org_id) => org_id,
228 Err(e) => {
229 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
230 }
231 };
232
233 match state
234 .resolution_use_cases
235 .change_vote(*vote_id, request.vote_choice.clone())
236 .await
237 {
238 Ok(vote) => {
239 AuditLogEntry::new(
240 AuditEventType::VoteChanged,
241 Some(user.user_id),
242 Some(organization_id),
243 )
244 .with_resource("Vote", vote.id)
245 .with_metadata(serde_json::json!({
246 "new_choice": format!("{:?}", vote.vote_choice)
247 }))
248 .log();
249
250 HttpResponse::Ok().json(VoteResponse::from(vote))
251 }
252 Err(err) => {
253 AuditLogEntry::new(
254 AuditEventType::VoteChanged,
255 Some(user.user_id),
256 Some(organization_id),
257 )
258 .with_error(err.clone())
259 .log();
260
261 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
262 }
263 }
264}
265
266#[put("/resolutions/{resolution_id}/close")]
267pub async fn close_voting(
268 state: web::Data<AppState>,
269 user: AuthenticatedUser,
270 resolution_id: web::Path<Uuid>,
271 request: web::Json<CloseVotingRequest>,
272) -> impl Responder {
273 let organization_id = match user.require_organization() {
274 Ok(org_id) => org_id,
275 Err(e) => {
276 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
277 }
278 };
279
280 match state
281 .resolution_use_cases
282 .close_voting(*resolution_id, request.total_voting_power)
283 .await
284 {
285 Ok(resolution) => {
286 AuditLogEntry::new(
287 AuditEventType::VotingClosed,
288 Some(user.user_id),
289 Some(organization_id),
290 )
291 .with_resource("Resolution", resolution.id)
292 .with_metadata(serde_json::json!({
293 "final_status": format!("{:?}", resolution.status)
294 }))
295 .log();
296
297 HttpResponse::Ok().json(ResolutionResponse::from(resolution))
298 }
299 Err(err) => {
300 AuditLogEntry::new(
301 AuditEventType::VotingClosed,
302 Some(user.user_id),
303 Some(organization_id),
304 )
305 .with_error(err.clone())
306 .log();
307
308 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
309 }
310 }
311}
312
313#[get("/meetings/{meeting_id}/vote-summary")]
314pub async fn get_meeting_vote_summary(
315 state: web::Data<AppState>,
316 meeting_id: web::Path<Uuid>,
317) -> impl Responder {
318 match state
319 .resolution_use_cases
320 .get_meeting_vote_summary(*meeting_id)
321 .await
322 {
323 Ok(resolutions) => {
324 let responses: Vec<ResolutionResponse> = resolutions
325 .into_iter()
326 .map(ResolutionResponse::from)
327 .collect();
328 HttpResponse::Ok().json(responses)
329 }
330 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({
331 "error": err
332 })),
333 }
334}