1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
7#[serde(rename_all = "snake_case")]
8pub enum ResolutionType {
9 Ordinary, Extraordinary, }
12
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
15#[serde(rename_all = "snake_case")]
16pub enum MajorityType {
17 Absolute,
20 TwoThirds,
23 FourFifths,
26 Unanimity,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
33#[serde(rename_all = "snake_case")]
34pub enum ResolutionStatus {
35 Pending, Adopted, Rejected, }
39
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
42pub struct Resolution {
43 pub id: Uuid,
44 pub meeting_id: Uuid,
45 pub title: String,
46 pub description: String,
47 pub resolution_type: ResolutionType,
48 pub majority_required: MajorityType,
49 pub vote_count_pour: i32,
50 pub vote_count_contre: i32,
51 pub vote_count_abstention: i32,
52 pub total_voting_power_pour: f64,
53 pub total_voting_power_contre: f64,
54 pub total_voting_power_abstention: f64,
55 pub status: ResolutionStatus,
56 pub agenda_item_index: Option<usize>, pub created_at: DateTime<Utc>,
59 pub voted_at: Option<DateTime<Utc>>,
60}
61
62impl Resolution {
63 pub fn new(
66 meeting_id: Uuid,
67 title: String,
68 description: String,
69 resolution_type: ResolutionType,
70 majority_required: MajorityType,
71 agenda_item_index: Option<usize>,
72 ) -> Result<Self, String> {
73 if title.is_empty() {
74 return Err("Resolution title cannot be empty".to_string());
75 }
76 if description.is_empty() {
77 return Err("Resolution description cannot be empty".to_string());
78 }
79
80 let now = Utc::now();
81 Ok(Self {
82 id: Uuid::new_v4(),
83 meeting_id,
84 title,
85 description,
86 resolution_type,
87 majority_required,
88 vote_count_pour: 0,
89 vote_count_contre: 0,
90 vote_count_abstention: 0,
91 total_voting_power_pour: 0.0,
92 total_voting_power_contre: 0.0,
93 total_voting_power_abstention: 0.0,
94 status: ResolutionStatus::Pending,
95 agenda_item_index,
96 created_at: now,
97 voted_at: None,
98 })
99 }
100
101 pub fn record_vote_pour(&mut self, voting_power: f64) {
103 self.vote_count_pour += 1;
104 self.total_voting_power_pour += voting_power;
105 }
106
107 pub fn record_vote_contre(&mut self, voting_power: f64) {
109 self.vote_count_contre += 1;
110 self.total_voting_power_contre += voting_power;
111 }
112
113 pub fn record_abstention(&mut self, voting_power: f64) {
115 self.vote_count_abstention += 1;
116 self.total_voting_power_abstention += voting_power;
117 }
118
119 pub fn calculate_result(&self, total_voting_power: f64) -> ResolutionStatus {
121 let expressed = self.total_voting_power_pour + self.total_voting_power_contre;
122
123 match &self.majority_required {
124 MajorityType::Absolute => {
125 if expressed > 0.0 && self.total_voting_power_pour > expressed / 2.0 {
127 ResolutionStatus::Adopted
128 } else {
129 ResolutionStatus::Rejected
130 }
131 }
132 MajorityType::TwoThirds => {
133 if expressed > 0.0 && self.total_voting_power_pour / expressed >= 2.0 / 3.0 {
135 ResolutionStatus::Adopted
136 } else {
137 ResolutionStatus::Rejected
138 }
139 }
140 MajorityType::FourFifths => {
141 if expressed > 0.0 && self.total_voting_power_pour / expressed >= 4.0 / 5.0 {
143 ResolutionStatus::Adopted
144 } else {
145 ResolutionStatus::Rejected
146 }
147 }
148 MajorityType::Unanimity => {
149 if total_voting_power > 0.0
152 && (self.total_voting_power_pour - total_voting_power).abs() < 0.01
153 {
154 ResolutionStatus::Adopted
155 } else {
156 ResolutionStatus::Rejected
157 }
158 }
159 }
160 }
161
162 pub fn close_voting(&mut self, total_voting_power: f64) -> Result<(), String> {
164 if self.status != ResolutionStatus::Pending {
165 return Err("Voting already closed for this resolution".to_string());
166 }
167
168 self.status = self.calculate_result(total_voting_power);
169 self.voted_at = Some(Utc::now());
170 Ok(())
171 }
172
173 pub fn total_votes(&self) -> i32 {
175 self.vote_count_pour + self.vote_count_contre + self.vote_count_abstention
176 }
177
178 pub fn pour_percentage(&self) -> f64 {
180 let total = self.total_votes();
181 if total > 0 {
182 (self.vote_count_pour as f64 / total as f64) * 100.0
183 } else {
184 0.0
185 }
186 }
187
188 pub fn contre_percentage(&self) -> f64 {
190 let total = self.total_votes();
191 if total > 0 {
192 (self.vote_count_contre as f64 / total as f64) * 100.0
193 } else {
194 0.0
195 }
196 }
197
198 pub fn abstention_percentage(&self) -> f64 {
200 let total = self.total_votes();
201 if total > 0 {
202 (self.vote_count_abstention as f64 / total as f64) * 100.0
203 } else {
204 0.0
205 }
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn test_create_resolution_success() {
215 let meeting_id = Uuid::new_v4();
216 let resolution = Resolution::new(
217 meeting_id,
218 "Approbation des comptes 2024".to_string(),
219 "Vote pour approuver les comptes annuels de l'exercice 2024".to_string(),
220 ResolutionType::Ordinary,
221 MajorityType::Absolute,
222 Some(0),
223 );
224
225 assert!(resolution.is_ok());
226 let resolution = resolution.unwrap();
227 assert_eq!(resolution.meeting_id, meeting_id);
228 assert_eq!(resolution.status, ResolutionStatus::Pending);
229 assert_eq!(resolution.total_votes(), 0);
230 assert_eq!(resolution.agenda_item_index, Some(0));
231 }
232
233 #[test]
234 fn test_create_resolution_without_agenda_item() {
235 let meeting_id = Uuid::new_v4();
236 let resolution = Resolution::new(
237 meeting_id,
238 "Approbation des comptes 2024".to_string(),
239 "Vote pour approuver les comptes annuels de l'exercice 2024".to_string(),
240 ResolutionType::Ordinary,
241 MajorityType::Absolute,
242 None,
243 );
244
245 assert!(resolution.is_ok());
246 let resolution = resolution.unwrap();
247 assert_eq!(resolution.agenda_item_index, None);
248 }
249
250 #[test]
251 fn test_create_resolution_empty_title_fails() {
252 let meeting_id = Uuid::new_v4();
253 let resolution = Resolution::new(
254 meeting_id,
255 "".to_string(),
256 "Description".to_string(),
257 ResolutionType::Ordinary,
258 MajorityType::Absolute,
259 Some(0),
260 );
261
262 assert!(resolution.is_err());
263 assert_eq!(resolution.unwrap_err(), "Resolution title cannot be empty");
264 }
265
266 #[test]
267 fn test_record_votes() {
268 let meeting_id = Uuid::new_v4();
269 let mut resolution = Resolution::new(
270 meeting_id,
271 "Test Resolution".to_string(),
272 "Description".to_string(),
273 ResolutionType::Ordinary,
274 MajorityType::Absolute,
275 Some(0),
276 )
277 .unwrap();
278
279 resolution.record_vote_pour(100.0);
280 resolution.record_vote_pour(150.0);
281 resolution.record_vote_contre(200.0);
282 resolution.record_abstention(50.0);
283
284 assert_eq!(resolution.vote_count_pour, 2);
285 assert_eq!(resolution.vote_count_contre, 1);
286 assert_eq!(resolution.vote_count_abstention, 1);
287 assert_eq!(resolution.total_voting_power_pour, 250.0);
288 assert_eq!(resolution.total_voting_power_contre, 200.0);
289 assert_eq!(resolution.total_voting_power_abstention, 50.0);
290 assert_eq!(resolution.total_votes(), 4);
291 }
292
293 #[test]
296 fn test_calculate_result_absolute_majority_adopted() {
297 let meeting_id = Uuid::new_v4();
298 let mut resolution = Resolution::new(
299 meeting_id,
300 "Test Resolution".to_string(),
301 "Description".to_string(),
302 ResolutionType::Ordinary,
303 MajorityType::Absolute,
304 Some(0),
305 )
306 .unwrap();
307
308 resolution.record_vote_pour(300.0);
310 resolution.record_vote_contre(150.0);
311 resolution.record_abstention(50.0);
312
313 let result = resolution.calculate_result(1000.0);
314 assert_eq!(result, ResolutionStatus::Adopted);
315 }
316
317 #[test]
318 fn test_calculate_result_absolute_majority_rejected() {
319 let meeting_id = Uuid::new_v4();
320 let mut resolution = Resolution::new(
321 meeting_id,
322 "Test Resolution".to_string(),
323 "Description".to_string(),
324 ResolutionType::Ordinary,
325 MajorityType::Absolute,
326 Some(0),
327 )
328 .unwrap();
329
330 resolution.record_vote_pour(150.0);
332 resolution.record_vote_contre(300.0);
333 resolution.record_abstention(50.0);
334
335 let result = resolution.calculate_result(1000.0);
336 assert_eq!(result, ResolutionStatus::Rejected);
337 }
338
339 #[test]
340 fn test_absolute_majority_abstentions_excluded() {
341 let meeting_id = Uuid::new_v4();
342 let mut resolution = Resolution::new(
343 meeting_id,
344 "Test Resolution".to_string(),
345 "Description".to_string(),
346 ResolutionType::Ordinary,
347 MajorityType::Absolute,
348 Some(0),
349 )
350 .unwrap();
351
352 resolution.record_vote_pour(300.0);
355 resolution.record_vote_contre(200.0);
356 resolution.record_abstention(500.0);
357
358 let result = resolution.calculate_result(1000.0);
359 assert_eq!(result, ResolutionStatus::Adopted);
360 }
361
362 #[test]
365 fn test_calculate_result_two_thirds_majority_adopted() {
366 let meeting_id = Uuid::new_v4();
367 let mut resolution = Resolution::new(
368 meeting_id,
369 "Test Resolution".to_string(),
370 "Description".to_string(),
371 ResolutionType::Extraordinary,
372 MajorityType::TwoThirds,
373 Some(0),
374 )
375 .unwrap();
376
377 resolution.record_vote_pour(700.0);
379 resolution.record_vote_contre(200.0);
380 resolution.record_abstention(100.0);
381
382 let result = resolution.calculate_result(1000.0);
383 assert_eq!(result, ResolutionStatus::Adopted);
384 }
385
386 #[test]
387 fn test_calculate_result_two_thirds_majority_rejected() {
388 let meeting_id = Uuid::new_v4();
389 let mut resolution = Resolution::new(
390 meeting_id,
391 "Test Resolution".to_string(),
392 "Description".to_string(),
393 ResolutionType::Extraordinary,
394 MajorityType::TwoThirds,
395 Some(0),
396 )
397 .unwrap();
398
399 resolution.record_vote_pour(600.0);
402 resolution.record_vote_contre(300.0);
403 resolution.record_abstention(100.0);
404
405 let result = resolution.calculate_result(1000.0);
406 assert_eq!(result, ResolutionStatus::Adopted);
407 }
408
409 #[test]
410 fn test_two_thirds_majority_barely_rejected() {
411 let meeting_id = Uuid::new_v4();
412 let mut resolution = Resolution::new(
413 meeting_id,
414 "Test Resolution".to_string(),
415 "Description".to_string(),
416 ResolutionType::Extraordinary,
417 MajorityType::TwoThirds,
418 Some(0),
419 )
420 .unwrap();
421
422 resolution.record_vote_pour(500.0);
424 resolution.record_vote_contre(300.0);
425 resolution.record_abstention(200.0);
426
427 let result = resolution.calculate_result(1000.0);
428 assert_eq!(result, ResolutionStatus::Rejected);
429 }
430
431 #[test]
432 fn test_two_thirds_abstentions_excluded() {
433 let meeting_id = Uuid::new_v4();
434 let mut resolution = Resolution::new(
435 meeting_id,
436 "Test Resolution".to_string(),
437 "Description".to_string(),
438 ResolutionType::Extraordinary,
439 MajorityType::TwoThirds,
440 Some(0),
441 )
442 .unwrap();
443
444 resolution.record_vote_pour(400.0);
446 resolution.record_vote_contre(100.0);
447 resolution.record_abstention(500.0);
448
449 let result = resolution.calculate_result(1000.0);
450 assert_eq!(result, ResolutionStatus::Adopted);
451 }
452
453 #[test]
456 fn test_calculate_result_four_fifths_majority_adopted() {
457 let meeting_id = Uuid::new_v4();
458 let mut resolution = Resolution::new(
459 meeting_id,
460 "Test Resolution".to_string(),
461 "Description".to_string(),
462 ResolutionType::Extraordinary,
463 MajorityType::FourFifths,
464 Some(0),
465 )
466 .unwrap();
467
468 resolution.record_vote_pour(800.0);
470 resolution.record_vote_contre(100.0);
471 resolution.record_abstention(100.0);
472
473 let result = resolution.calculate_result(1000.0);
474 assert_eq!(result, ResolutionStatus::Adopted);
475 }
476
477 #[test]
478 fn test_calculate_result_four_fifths_majority_rejected() {
479 let meeting_id = Uuid::new_v4();
480 let mut resolution = Resolution::new(
481 meeting_id,
482 "Test Resolution".to_string(),
483 "Description".to_string(),
484 ResolutionType::Extraordinary,
485 MajorityType::FourFifths,
486 Some(0),
487 )
488 .unwrap();
489
490 resolution.record_vote_pour(700.0);
492 resolution.record_vote_contre(200.0);
493 resolution.record_abstention(100.0);
494
495 let result = resolution.calculate_result(1000.0);
496 assert_eq!(result, ResolutionStatus::Rejected);
497 }
498
499 #[test]
500 fn test_four_fifths_abstentions_excluded() {
501 let meeting_id = Uuid::new_v4();
502 let mut resolution = Resolution::new(
503 meeting_id,
504 "Test Resolution".to_string(),
505 "Description".to_string(),
506 ResolutionType::Extraordinary,
507 MajorityType::FourFifths,
508 Some(0),
509 )
510 .unwrap();
511
512 resolution.record_vote_pour(400.0);
514 resolution.record_vote_contre(50.0);
515 resolution.record_abstention(550.0);
516
517 let result = resolution.calculate_result(1000.0);
518 assert_eq!(result, ResolutionStatus::Adopted);
519 }
520
521 #[test]
524 fn test_calculate_result_unanimity_adopted() {
525 let meeting_id = Uuid::new_v4();
526 let mut resolution = Resolution::new(
527 meeting_id,
528 "Test Resolution".to_string(),
529 "Description".to_string(),
530 ResolutionType::Extraordinary,
531 MajorityType::Unanimity,
532 Some(0),
533 )
534 .unwrap();
535
536 resolution.record_vote_pour(10000.0);
538
539 let result = resolution.calculate_result(10000.0);
540 assert_eq!(result, ResolutionStatus::Adopted);
541 }
542
543 #[test]
544 fn test_calculate_result_unanimity_rejected_missing_votes() {
545 let meeting_id = Uuid::new_v4();
546 let mut resolution = Resolution::new(
547 meeting_id,
548 "Test Resolution".to_string(),
549 "Description".to_string(),
550 ResolutionType::Extraordinary,
551 MajorityType::Unanimity,
552 Some(0),
553 )
554 .unwrap();
555
556 resolution.record_vote_pour(9000.0);
558
559 let result = resolution.calculate_result(10000.0);
560 assert_eq!(result, ResolutionStatus::Rejected);
561 }
562
563 #[test]
564 fn test_unanimity_requires_all_tantiemes_not_just_present() {
565 let meeting_id = Uuid::new_v4();
566 let mut resolution = Resolution::new(
567 meeting_id,
568 "Test Resolution".to_string(),
569 "Description".to_string(),
570 ResolutionType::Extraordinary,
571 MajorityType::Unanimity,
572 Some(0),
573 )
574 .unwrap();
575
576 resolution.record_vote_pour(8000.0);
579
580 let result = resolution.calculate_result(10000.0);
581 assert_eq!(result, ResolutionStatus::Rejected);
582 }
583
584 #[test]
585 fn test_unanimity_rejected_with_abstention() {
586 let meeting_id = Uuid::new_v4();
587 let mut resolution = Resolution::new(
588 meeting_id,
589 "Test Resolution".to_string(),
590 "Description".to_string(),
591 ResolutionType::Extraordinary,
592 MajorityType::Unanimity,
593 Some(0),
594 )
595 .unwrap();
596
597 resolution.record_vote_pour(9500.0);
599 resolution.record_abstention(500.0);
600
601 let result = resolution.calculate_result(10000.0);
602 assert_eq!(result, ResolutionStatus::Rejected);
603 }
604
605 #[test]
608 fn test_close_voting_success() {
609 let meeting_id = Uuid::new_v4();
610 let mut resolution = Resolution::new(
611 meeting_id,
612 "Test Resolution".to_string(),
613 "Description".to_string(),
614 ResolutionType::Ordinary,
615 MajorityType::Absolute,
616 Some(0),
617 )
618 .unwrap();
619
620 resolution.record_vote_pour(300.0);
621 resolution.record_vote_contre(150.0);
622
623 let result = resolution.close_voting(1000.0);
624 assert!(result.is_ok());
625 assert_eq!(resolution.status, ResolutionStatus::Adopted);
626 assert!(resolution.voted_at.is_some());
627 }
628
629 #[test]
630 fn test_close_voting_already_closed_fails() {
631 let meeting_id = Uuid::new_v4();
632 let mut resolution = Resolution::new(
633 meeting_id,
634 "Test Resolution".to_string(),
635 "Description".to_string(),
636 ResolutionType::Ordinary,
637 MajorityType::Absolute,
638 Some(0),
639 )
640 .unwrap();
641
642 resolution.record_vote_pour(300.0);
643 resolution.close_voting(1000.0).unwrap();
644
645 let result = resolution.close_voting(1000.0);
646 assert!(result.is_err());
647 assert_eq!(
648 result.unwrap_err(),
649 "Voting already closed for this resolution"
650 );
651 }
652
653 #[test]
654 fn test_percentages() {
655 let meeting_id = Uuid::new_v4();
656 let mut resolution = Resolution::new(
657 meeting_id,
658 "Test Resolution".to_string(),
659 "Description".to_string(),
660 ResolutionType::Ordinary,
661 MajorityType::Absolute,
662 Some(0),
663 )
664 .unwrap();
665
666 resolution.record_vote_pour(100.0);
667 resolution.record_vote_pour(100.0); resolution.record_vote_contre(100.0); resolution.record_abstention(100.0); assert_eq!(resolution.pour_percentage(), 50.0); assert_eq!(resolution.contre_percentage(), 25.0); assert_eq!(resolution.abstention_percentage(), 25.0); }
675}