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 Simple, Absolute, Qualified(f64), }
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
24#[serde(rename_all = "snake_case")]
25pub enum ResolutionStatus {
26 Pending, Adopted, Rejected, }
30
31#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
33pub struct Resolution {
34 pub id: Uuid,
35 pub meeting_id: Uuid,
36 pub title: String,
37 pub description: String,
38 pub resolution_type: ResolutionType,
39 pub majority_required: MajorityType,
40 pub vote_count_pour: i32,
41 pub vote_count_contre: i32,
42 pub vote_count_abstention: i32,
43 pub total_voting_power_pour: f64,
44 pub total_voting_power_contre: f64,
45 pub total_voting_power_abstention: f64,
46 pub status: ResolutionStatus,
47 pub agenda_item_index: Option<usize>, pub created_at: DateTime<Utc>,
50 pub voted_at: Option<DateTime<Utc>>,
51}
52
53impl Resolution {
54 pub fn new(
57 meeting_id: Uuid,
58 title: String,
59 description: String,
60 resolution_type: ResolutionType,
61 majority_required: MajorityType,
62 agenda_item_index: Option<usize>,
63 ) -> Result<Self, String> {
64 if title.is_empty() {
65 return Err("Resolution title cannot be empty".to_string());
66 }
67 if description.is_empty() {
68 return Err("Resolution description cannot be empty".to_string());
69 }
70
71 if let MajorityType::Qualified(threshold) = &majority_required {
73 if *threshold <= 0.0 || *threshold > 1.0 {
74 return Err("Qualified majority threshold must be between 0 and 1".to_string());
75 }
76 }
77
78 let now = Utc::now();
79 Ok(Self {
80 id: Uuid::new_v4(),
81 meeting_id,
82 title,
83 description,
84 resolution_type,
85 majority_required,
86 vote_count_pour: 0,
87 vote_count_contre: 0,
88 vote_count_abstention: 0,
89 total_voting_power_pour: 0.0,
90 total_voting_power_contre: 0.0,
91 total_voting_power_abstention: 0.0,
92 status: ResolutionStatus::Pending,
93 agenda_item_index,
94 created_at: now,
95 voted_at: None,
96 })
97 }
98
99 pub fn record_vote_pour(&mut self, voting_power: f64) {
101 self.vote_count_pour += 1;
102 self.total_voting_power_pour += voting_power;
103 }
104
105 pub fn record_vote_contre(&mut self, voting_power: f64) {
107 self.vote_count_contre += 1;
108 self.total_voting_power_contre += voting_power;
109 }
110
111 pub fn record_abstention(&mut self, voting_power: f64) {
113 self.vote_count_abstention += 1;
114 self.total_voting_power_abstention += voting_power;
115 }
116
117 pub fn calculate_result(&self, total_voting_power: f64) -> ResolutionStatus {
119 match &self.majority_required {
120 MajorityType::Simple => {
121 if self.total_voting_power_pour
123 > self.total_voting_power_contre + self.total_voting_power_abstention
124 {
125 ResolutionStatus::Adopted
126 } else {
127 ResolutionStatus::Rejected
128 }
129 }
130 MajorityType::Absolute => {
131 if self.total_voting_power_pour > total_voting_power / 2.0 {
133 ResolutionStatus::Adopted
134 } else {
135 ResolutionStatus::Rejected
136 }
137 }
138 MajorityType::Qualified(threshold) => {
139 let pour_ratio = if total_voting_power > 0.0 {
141 self.total_voting_power_pour / total_voting_power
142 } else {
143 0.0
144 };
145 if pour_ratio >= *threshold {
146 ResolutionStatus::Adopted
147 } else {
148 ResolutionStatus::Rejected
149 }
150 }
151 }
152 }
153
154 pub fn close_voting(&mut self, total_voting_power: f64) -> Result<(), String> {
156 if self.status != ResolutionStatus::Pending {
157 return Err("Voting already closed for this resolution".to_string());
158 }
159
160 self.status = self.calculate_result(total_voting_power);
161 self.voted_at = Some(Utc::now());
162 Ok(())
163 }
164
165 pub fn total_votes(&self) -> i32 {
167 self.vote_count_pour + self.vote_count_contre + self.vote_count_abstention
168 }
169
170 pub fn pour_percentage(&self) -> f64 {
172 let total = self.total_votes();
173 if total > 0 {
174 (self.vote_count_pour as f64 / total as f64) * 100.0
175 } else {
176 0.0
177 }
178 }
179
180 pub fn contre_percentage(&self) -> f64 {
182 let total = self.total_votes();
183 if total > 0 {
184 (self.vote_count_contre as f64 / total as f64) * 100.0
185 } else {
186 0.0
187 }
188 }
189
190 pub fn abstention_percentage(&self) -> f64 {
192 let total = self.total_votes();
193 if total > 0 {
194 (self.vote_count_abstention as f64 / total as f64) * 100.0
195 } else {
196 0.0
197 }
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204
205 #[test]
206 fn test_create_resolution_success() {
207 let meeting_id = Uuid::new_v4();
208 let resolution = Resolution::new(
209 meeting_id,
210 "Approbation des comptes 2024".to_string(),
211 "Vote pour approuver les comptes annuels de l'exercice 2024".to_string(),
212 ResolutionType::Ordinary,
213 MajorityType::Simple,
214 Some(0),
215 );
216
217 assert!(resolution.is_ok());
218 let resolution = resolution.unwrap();
219 assert_eq!(resolution.meeting_id, meeting_id);
220 assert_eq!(resolution.status, ResolutionStatus::Pending);
221 assert_eq!(resolution.total_votes(), 0);
222 assert_eq!(resolution.agenda_item_index, Some(0));
223 }
224
225 #[test]
226 fn test_create_resolution_without_agenda_item() {
227 let meeting_id = Uuid::new_v4();
228 let resolution = Resolution::new(
229 meeting_id,
230 "Approbation des comptes 2024".to_string(),
231 "Vote pour approuver les comptes annuels de l'exercice 2024".to_string(),
232 ResolutionType::Ordinary,
233 MajorityType::Simple,
234 None,
235 );
236
237 assert!(resolution.is_ok());
238 let resolution = resolution.unwrap();
239 assert_eq!(resolution.agenda_item_index, None);
240 }
241
242 #[test]
243 fn test_create_resolution_empty_title_fails() {
244 let meeting_id = Uuid::new_v4();
245 let resolution = Resolution::new(
246 meeting_id,
247 "".to_string(),
248 "Description".to_string(),
249 ResolutionType::Ordinary,
250 MajorityType::Simple,
251 Some(0),
252 );
253
254 assert!(resolution.is_err());
255 assert_eq!(resolution.unwrap_err(), "Resolution title cannot be empty");
256 }
257
258 #[test]
259 fn test_create_resolution_invalid_qualified_threshold_fails() {
260 let meeting_id = Uuid::new_v4();
261 let resolution = Resolution::new(
262 meeting_id,
263 "Test".to_string(),
264 "Description".to_string(),
265 ResolutionType::Extraordinary,
266 MajorityType::Qualified(1.5), Some(0),
268 );
269
270 assert!(resolution.is_err());
271 assert!(resolution
272 .unwrap_err()
273 .contains("threshold must be between 0 and 1"));
274 }
275
276 #[test]
277 fn test_record_votes() {
278 let meeting_id = Uuid::new_v4();
279 let mut resolution = Resolution::new(
280 meeting_id,
281 "Test Resolution".to_string(),
282 "Description".to_string(),
283 ResolutionType::Ordinary,
284 MajorityType::Simple,
285 Some(0),
286 )
287 .unwrap();
288
289 resolution.record_vote_pour(100.0);
290 resolution.record_vote_pour(150.0);
291 resolution.record_vote_contre(200.0);
292 resolution.record_abstention(50.0);
293
294 assert_eq!(resolution.vote_count_pour, 2);
295 assert_eq!(resolution.vote_count_contre, 1);
296 assert_eq!(resolution.vote_count_abstention, 1);
297 assert_eq!(resolution.total_voting_power_pour, 250.0);
298 assert_eq!(resolution.total_voting_power_contre, 200.0);
299 assert_eq!(resolution.total_voting_power_abstention, 50.0);
300 assert_eq!(resolution.total_votes(), 4);
301 }
302
303 #[test]
304 fn test_calculate_result_simple_majority_adopted() {
305 let meeting_id = Uuid::new_v4();
306 let mut resolution = Resolution::new(
307 meeting_id,
308 "Test Resolution".to_string(),
309 "Description".to_string(),
310 ResolutionType::Ordinary,
311 MajorityType::Simple,
312 Some(0),
313 )
314 .unwrap();
315
316 resolution.record_vote_pour(300.0); resolution.record_vote_contre(150.0);
318 resolution.record_abstention(50.0);
319
320 let result = resolution.calculate_result(1000.0);
321 assert_eq!(result, ResolutionStatus::Adopted);
322 }
323
324 #[test]
325 fn test_calculate_result_simple_majority_rejected() {
326 let meeting_id = Uuid::new_v4();
327 let mut resolution = Resolution::new(
328 meeting_id,
329 "Test Resolution".to_string(),
330 "Description".to_string(),
331 ResolutionType::Ordinary,
332 MajorityType::Simple,
333 Some(0),
334 )
335 .unwrap();
336
337 resolution.record_vote_pour(150.0);
338 resolution.record_vote_contre(300.0); resolution.record_abstention(50.0);
340
341 let result = resolution.calculate_result(1000.0);
342 assert_eq!(result, ResolutionStatus::Rejected);
343 }
344
345 #[test]
346 fn test_calculate_result_absolute_majority_adopted() {
347 let meeting_id = Uuid::new_v4();
348 let mut resolution = Resolution::new(
349 meeting_id,
350 "Test Resolution".to_string(),
351 "Description".to_string(),
352 ResolutionType::Ordinary,
353 MajorityType::Absolute,
354 Some(0),
355 )
356 .unwrap();
357
358 resolution.record_vote_pour(600.0); resolution.record_vote_contre(200.0);
360 resolution.record_abstention(100.0);
361
362 let result = resolution.calculate_result(1000.0);
363 assert_eq!(result, ResolutionStatus::Adopted);
364 }
365
366 #[test]
367 fn test_calculate_result_absolute_majority_rejected() {
368 let meeting_id = Uuid::new_v4();
369 let mut resolution = Resolution::new(
370 meeting_id,
371 "Test Resolution".to_string(),
372 "Description".to_string(),
373 ResolutionType::Ordinary,
374 MajorityType::Absolute,
375 Some(0),
376 )
377 .unwrap();
378
379 resolution.record_vote_pour(400.0); resolution.record_vote_contre(300.0);
381 resolution.record_abstention(100.0);
382
383 let result = resolution.calculate_result(1000.0);
384 assert_eq!(result, ResolutionStatus::Rejected);
385 }
386
387 #[test]
388 fn test_calculate_result_qualified_majority_adopted() {
389 let meeting_id = Uuid::new_v4();
390 let mut resolution = Resolution::new(
391 meeting_id,
392 "Test Resolution".to_string(),
393 "Description".to_string(),
394 ResolutionType::Extraordinary,
395 MajorityType::Qualified(0.67), Some(0),
397 )
398 .unwrap();
399
400 resolution.record_vote_pour(700.0); resolution.record_vote_contre(200.0);
402 resolution.record_abstention(100.0);
403
404 let result = resolution.calculate_result(1000.0);
405 assert_eq!(result, ResolutionStatus::Adopted);
406 }
407
408 #[test]
409 fn test_calculate_result_qualified_majority_rejected() {
410 let meeting_id = Uuid::new_v4();
411 let mut resolution = Resolution::new(
412 meeting_id,
413 "Test Resolution".to_string(),
414 "Description".to_string(),
415 ResolutionType::Extraordinary,
416 MajorityType::Qualified(0.67), Some(0),
418 )
419 .unwrap();
420
421 resolution.record_vote_pour(600.0); resolution.record_vote_contre(300.0);
423 resolution.record_abstention(100.0);
424
425 let result = resolution.calculate_result(1000.0);
426 assert_eq!(result, ResolutionStatus::Rejected);
427 }
428
429 #[test]
430 fn test_close_voting_success() {
431 let meeting_id = Uuid::new_v4();
432 let mut resolution = Resolution::new(
433 meeting_id,
434 "Test Resolution".to_string(),
435 "Description".to_string(),
436 ResolutionType::Ordinary,
437 MajorityType::Simple,
438 Some(0),
439 )
440 .unwrap();
441
442 resolution.record_vote_pour(300.0);
443 resolution.record_vote_contre(150.0);
444
445 let result = resolution.close_voting(1000.0);
446 assert!(result.is_ok());
447 assert_eq!(resolution.status, ResolutionStatus::Adopted);
448 assert!(resolution.voted_at.is_some());
449 }
450
451 #[test]
452 fn test_close_voting_already_closed_fails() {
453 let meeting_id = Uuid::new_v4();
454 let mut resolution = Resolution::new(
455 meeting_id,
456 "Test Resolution".to_string(),
457 "Description".to_string(),
458 ResolutionType::Ordinary,
459 MajorityType::Simple,
460 Some(0),
461 )
462 .unwrap();
463
464 resolution.record_vote_pour(300.0);
465 resolution.close_voting(1000.0).unwrap();
466
467 let result = resolution.close_voting(1000.0);
468 assert!(result.is_err());
469 assert_eq!(
470 result.unwrap_err(),
471 "Voting already closed for this resolution"
472 );
473 }
474
475 #[test]
476 fn test_percentages() {
477 let meeting_id = Uuid::new_v4();
478 let mut resolution = Resolution::new(
479 meeting_id,
480 "Test Resolution".to_string(),
481 "Description".to_string(),
482 ResolutionType::Ordinary,
483 MajorityType::Simple,
484 Some(0),
485 )
486 .unwrap();
487
488 resolution.record_vote_pour(100.0);
489 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); }
497}