koprogo_api/infrastructure/web/handlers/
resolution_handlers.rs

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// ==================== Resolution Endpoints ====================
11
12#[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// ==================== Vote Endpoints ====================
143
144#[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}