koprogo_api/infrastructure/web/handlers/
convocation_handlers.rs1use crate::application::dto::{
2 CreateConvocationRequest, ScheduleConvocationRequest, ScheduleSecondConvocationRequest,
3 SendConvocationRequest, SetProxyRequest, UpdateAttendanceRequest,
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("/convocations")]
13pub async fn create_convocation(
14 state: web::Data<AppState>,
15 user: AuthenticatedUser,
16 request: web::Json<CreateConvocationRequest>,
17) -> impl Responder {
18 let organization_id = match user.require_organization() {
19 Ok(org_id) => org_id,
20 Err(e) => {
21 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
22 }
23 };
24
25 let created_by = user.user_id;
26
27 match state
28 .convocation_use_cases
29 .create_convocation(organization_id, request.into_inner(), created_by)
30 .await
31 {
32 Ok(convocation) => {
33 AuditLogEntry::new(
34 AuditEventType::ConvocationCreated,
35 Some(user.user_id),
36 Some(organization_id),
37 )
38 .with_resource("Convocation", convocation.id)
39 .log();
40
41 HttpResponse::Created().json(convocation)
42 }
43 Err(err) => {
44 AuditLogEntry::new(
45 AuditEventType::ConvocationCreated,
46 Some(user.user_id),
47 Some(organization_id),
48 )
49 .with_error(err.clone())
50 .log();
51
52 HttpResponse::BadRequest().json(serde_json::json!({"error": err}))
53 }
54 }
55}
56
57#[get("/convocations/{id}")]
58pub async fn get_convocation(
59 state: web::Data<AppState>,
60 user: AuthenticatedUser,
61 id: web::Path<Uuid>,
62) -> impl Responder {
63 match state.convocation_use_cases.get_convocation(*id).await {
64 Ok(convocation) => {
65 if let Err(err) = user.verify_org_access(convocation.organization_id) {
67 return HttpResponse::Forbidden().json(serde_json::json!({"error": err}));
68 }
69 HttpResponse::Ok().json(convocation)
70 }
71 Err(err) => HttpResponse::NotFound().json(serde_json::json!({"error": err})),
72 }
73}
74
75#[get("/meetings/{meeting_id}/convocation")]
76pub async fn get_convocation_by_meeting(
77 state: web::Data<AppState>,
78 meeting_id: web::Path<Uuid>,
79) -> impl Responder {
80 match state
81 .convocation_use_cases
82 .get_convocation_by_meeting(*meeting_id)
83 .await
84 {
85 Ok(Some(convocation)) => HttpResponse::Ok().json(convocation),
86 Ok(None) => HttpResponse::NotFound().json(serde_json::json!({
87 "error": "Convocation not found for this meeting"
88 })),
89 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
90 }
91}
92
93#[get("/buildings/{building_id}/convocations")]
94pub async fn list_building_convocations(
95 state: web::Data<AppState>,
96 building_id: web::Path<Uuid>,
97) -> impl Responder {
98 match state
99 .convocation_use_cases
100 .list_building_convocations(*building_id)
101 .await
102 {
103 Ok(convocations) => HttpResponse::Ok().json(convocations),
104 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
105 }
106}
107
108#[get("/organizations/{organization_id}/convocations")]
109pub async fn list_organization_convocations(
110 state: web::Data<AppState>,
111 user: AuthenticatedUser,
112 organization_id: web::Path<Uuid>,
113) -> impl Responder {
114 if let Err(e) = user.verify_org_access(*organization_id) {
115 return HttpResponse::Forbidden().json(serde_json::json!({"error": e}));
116 }
117 match state
118 .convocation_use_cases
119 .list_organization_convocations(*organization_id)
120 .await
121 {
122 Ok(convocations) => HttpResponse::Ok().json(convocations),
123 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
124 }
125}
126
127#[delete("/convocations/{id}")]
128pub async fn delete_convocation(
129 state: web::Data<AppState>,
130 user: AuthenticatedUser,
131 id: web::Path<Uuid>,
132) -> impl Responder {
133 let organization_id = match user.require_organization() {
134 Ok(org_id) => org_id,
135 Err(e) => {
136 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
137 }
138 };
139
140 match state.convocation_use_cases.delete_convocation(*id).await {
141 Ok(true) => {
142 AuditLogEntry::new(
143 AuditEventType::ConvocationDeleted,
144 Some(user.user_id),
145 Some(organization_id),
146 )
147 .with_resource("Convocation", *id)
148 .log();
149
150 HttpResponse::NoContent().finish()
151 }
152 Ok(false) => HttpResponse::NotFound().json(serde_json::json!({
153 "error": "Convocation not found"
154 })),
155 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
156 }
157}
158
159#[put("/convocations/{id}/schedule")]
162pub async fn schedule_convocation(
163 state: web::Data<AppState>,
164 user: AuthenticatedUser,
165 id: web::Path<Uuid>,
166 request: web::Json<ScheduleConvocationRequest>,
167) -> impl Responder {
168 let organization_id = match user.require_organization() {
169 Ok(org_id) => org_id,
170 Err(e) => {
171 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
172 }
173 };
174
175 match state
176 .convocation_use_cases
177 .schedule_convocation(*id, request.into_inner())
178 .await
179 {
180 Ok(convocation) => {
181 AuditLogEntry::new(
182 AuditEventType::ConvocationScheduled,
183 Some(user.user_id),
184 Some(organization_id),
185 )
186 .with_resource("Convocation", convocation.id)
187 .log();
188
189 HttpResponse::Ok().json(convocation)
190 }
191 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
192 }
193}
194
195#[post("/convocations/{id}/send")]
196pub async fn send_convocation(
197 state: web::Data<AppState>,
198 user: AuthenticatedUser,
199 id: web::Path<Uuid>,
200 request: web::Json<SendConvocationRequest>,
201) -> impl Responder {
202 let organization_id = match user.require_organization() {
203 Ok(org_id) => org_id,
204 Err(e) => {
205 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
206 }
207 };
208
209 match state
211 .convocation_use_cases
212 .send_convocation(*id, request.into_inner())
213 .await
214 {
215 Ok(convocation) => {
216 AuditLogEntry::new(
217 AuditEventType::ConvocationSent,
218 Some(user.user_id),
219 Some(organization_id),
220 )
221 .with_resource("Convocation", convocation.id)
222 .with_details(format!("recipients: {}", convocation.total_recipients))
223 .log();
224
225 HttpResponse::Ok().json(convocation)
226 }
227 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
228 }
229}
230
231#[put("/convocations/{id}/cancel")]
232pub async fn cancel_convocation(
233 state: web::Data<AppState>,
234 user: AuthenticatedUser,
235 id: web::Path<Uuid>,
236) -> impl Responder {
237 let organization_id = match user.require_organization() {
238 Ok(org_id) => org_id,
239 Err(e) => {
240 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
241 }
242 };
243
244 match state.convocation_use_cases.cancel_convocation(*id).await {
245 Ok(convocation) => {
246 AuditLogEntry::new(
247 AuditEventType::ConvocationCancelled,
248 Some(user.user_id),
249 Some(organization_id),
250 )
251 .with_resource("Convocation", convocation.id)
252 .log();
253
254 HttpResponse::Ok().json(convocation)
255 }
256 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
257 }
258}
259
260#[get("/convocations/{id}/recipients")]
263pub async fn list_convocation_recipients(
264 state: web::Data<AppState>,
265 id: web::Path<Uuid>,
266) -> impl Responder {
267 match state
268 .convocation_use_cases
269 .list_convocation_recipients(*id)
270 .await
271 {
272 Ok(recipients) => HttpResponse::Ok().json(recipients),
273 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
274 }
275}
276
277#[get("/convocations/{id}/tracking-summary")]
278pub async fn get_convocation_tracking_summary(
279 state: web::Data<AppState>,
280 id: web::Path<Uuid>,
281) -> impl Responder {
282 match state.convocation_use_cases.get_tracking_summary(*id).await {
283 Ok(summary) => HttpResponse::Ok().json(summary),
284 Err(err) => HttpResponse::InternalServerError().json(serde_json::json!({"error": err})),
285 }
286}
287
288#[put("/convocation-recipients/{id}/email-opened")]
289pub async fn mark_recipient_email_opened(
290 state: web::Data<AppState>,
291 id: web::Path<Uuid>,
292) -> impl Responder {
293 match state
294 .convocation_use_cases
295 .mark_recipient_email_opened(*id)
296 .await
297 {
298 Ok(recipient) => HttpResponse::Ok().json(recipient),
299 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
300 }
301}
302
303#[put("/convocation-recipients/{id}/attendance")]
304pub async fn update_recipient_attendance(
305 state: web::Data<AppState>,
306 user: AuthenticatedUser,
307 id: web::Path<Uuid>,
308 request: web::Json<UpdateAttendanceRequest>,
309) -> impl Responder {
310 let organization_id = match user.require_organization() {
311 Ok(org_id) => org_id,
312 Err(e) => {
313 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
314 }
315 };
316
317 match state
318 .convocation_use_cases
319 .update_recipient_attendance(*id, request.attendance_status.clone())
320 .await
321 {
322 Ok(recipient) => {
323 AuditLogEntry::new(
324 AuditEventType::ConvocationAttendanceUpdated,
325 Some(user.user_id),
326 Some(organization_id),
327 )
328 .with_resource("ConvocationRecipient", recipient.id)
329 .with_details(format!("status: {:?}", recipient.attendance_status))
330 .log();
331
332 HttpResponse::Ok().json(recipient)
333 }
334 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
335 }
336}
337
338#[put("/convocation-recipients/{id}/proxy")]
339pub async fn set_recipient_proxy(
340 state: web::Data<AppState>,
341 user: AuthenticatedUser,
342 id: web::Path<Uuid>,
343 request: web::Json<SetProxyRequest>,
344) -> impl Responder {
345 let organization_id = match user.require_organization() {
346 Ok(org_id) => org_id,
347 Err(e) => {
348 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
349 }
350 };
351
352 match state
353 .convocation_use_cases
354 .set_recipient_proxy(*id, request.proxy_owner_id)
355 .await
356 {
357 Ok(recipient) => {
358 AuditLogEntry::new(
359 AuditEventType::ConvocationProxySet,
360 Some(user.user_id),
361 Some(organization_id),
362 )
363 .with_resource("ConvocationRecipient", recipient.id)
364 .with_details(format!("proxy_owner_id: {:?}", recipient.proxy_owner_id))
365 .log();
366
367 HttpResponse::Ok().json(recipient)
368 }
369 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
370 }
371}
372
373#[post("/convocations/{id}/reminders")]
374pub async fn send_convocation_reminders(
375 state: web::Data<AppState>,
376 user: AuthenticatedUser,
377 id: web::Path<Uuid>,
378) -> impl Responder {
379 let organization_id = match user.require_organization() {
380 Ok(org_id) => org_id,
381 Err(e) => {
382 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
383 }
384 };
385
386 match state.convocation_use_cases.send_reminders(*id).await {
387 Ok(recipients) => {
388 AuditLogEntry::new(
389 AuditEventType::ConvocationReminderSent,
390 Some(user.user_id),
391 Some(organization_id),
392 )
393 .with_resource("Convocation", *id)
394 .with_details(format!("recipients: {}", recipients.len()))
395 .log();
396
397 HttpResponse::Ok().json(recipients)
398 }
399 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
400 }
401}
402
403#[post("/convocations/second")]
406pub async fn schedule_second_convocation(
407 state: web::Data<AppState>,
408 user: AuthenticatedUser,
409 request: web::Json<ScheduleSecondConvocationRequest>,
410) -> impl Responder {
411 let organization_id = match user.require_organization() {
412 Ok(org_id) => org_id,
413 Err(e) => {
414 return HttpResponse::Unauthorized().json(serde_json::json!({"error": e.to_string()}))
415 }
416 };
417
418 let req = request.into_inner();
419
420 let new_meeting_req = crate::application::dto::CreateMeetingRequest {
422 organization_id,
423 building_id: req.building_id,
424 meeting_type: crate::domain::entities::MeetingType::Ordinary,
425 title: format!("Second Convocation (Art. 3.87 §5 CC)"),
426 description: Some("Second convocation after quorum not reached".to_string()),
427 scheduled_date: req.new_meeting_date,
428 location: "Same as first meeting".to_string(),
429 is_second_convocation: true,
430 };
431
432 let new_meeting = match state
434 .meeting_use_cases
435 .create_meeting(new_meeting_req)
436 .await
437 {
438 Ok(m) => m,
439 Err(err) => {
440 return HttpResponse::BadRequest()
441 .json(serde_json::json!({"error": format!("Failed to create meeting: {}", err)}))
442 }
443 };
444
445 match state
446 .convocation_use_cases
447 .schedule_second_convocation(
448 organization_id,
449 req.building_id,
450 req.first_meeting_id,
451 new_meeting.id,
452 req.new_meeting_date,
453 req.language,
454 user.user_id,
455 )
456 .await
457 {
458 Ok(convocation) => {
459 AuditLogEntry::new(
460 AuditEventType::SecondConvocationScheduled,
461 Some(user.user_id),
462 Some(organization_id),
463 )
464 .with_resource("Convocation", convocation.id)
465 .with_details(format!(
466 "first_meeting_id: {}, new_meeting_id: {}",
467 req.first_meeting_id, new_meeting.id
468 ))
469 .log();
470
471 HttpResponse::Created().json(convocation)
472 }
473 Err(err) => HttpResponse::BadRequest().json(serde_json::json!({"error": err})),
474 }
475}