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