1use super::meeting::MeetingMode;
2use chrono::{DateTime, Utc};
3use rust_decimal::Decimal;
4use rust_decimal_macros::dec;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8const MAX_VOTING_POWER: Decimal = dec!(10000);
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum VoteChoice {
15 Pour, Contre, Abstention, }
19
20#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, utoipa::ToSchema)]
27#[serde(rename_all = "snake_case")]
28pub enum VoteAuthMethod {
29 Presence,
30 Proxy,
31 Itsme,
32 Eid,
33}
34
35impl VoteAuthMethod {
36 pub fn from_db_string(s: &str) -> Result<Self, String> {
37 match s {
38 "presence" => Ok(Self::Presence),
39 "proxy" => Ok(Self::Proxy),
40 "itsme" => Ok(Self::Itsme),
41 "eid" => Ok(Self::Eid),
42 other => Err(format!("Unknown vote auth method: {other}")),
43 }
44 }
45
46 pub fn to_db_str(&self) -> &'static str {
47 match self {
48 Self::Presence => "presence",
49 Self::Proxy => "proxy",
50 Self::Itsme => "itsme",
51 Self::Eid => "eid",
52 }
53 }
54
55 pub fn is_strong(&self) -> bool {
60 matches!(self, Self::Itsme | Self::Eid)
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
68pub enum VoteAuthError {
69 #[error("La méthode d'authentification du vote est obligatoire")]
72 Missing,
73
74 #[error(
79 "Authentification insuffisante pour un vote en mode {mode:?} : {auth_method:?} \
80 n'engage pas le votant (Art. 3.87 §1er, §4 CC)"
81 )]
82 Insufficient {
83 mode: MeetingMode,
84 auth_method: VoteAuthMethod,
85 },
86}
87
88pub fn assert_vote_auth_sufficient(
99 mode: MeetingMode,
100 auth_method: Option<VoteAuthMethod>,
101 is_proxy_vote: bool,
102) -> Result<VoteAuthMethod, VoteAuthError> {
103 let auth_method = auth_method.ok_or(VoteAuthError::Missing)?;
104
105 if !mode.requires_strong_vote_auth() {
106 return Ok(auth_method);
107 }
108
109 let suffisant = match auth_method {
110 VoteAuthMethod::Proxy => is_proxy_vote,
111 other => other.is_strong(),
112 };
113
114 if suffisant {
115 Ok(auth_method)
116 } else {
117 Err(VoteAuthError::Insufficient { mode, auth_method })
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
123pub struct Vote {
124 pub id: Uuid,
125 pub resolution_id: Uuid,
126 pub owner_id: Uuid,
127 pub unit_id: Uuid,
128 pub vote_choice: VoteChoice,
129 pub voting_power: Decimal, pub proxy_owner_id: Option<Uuid>, pub voted_at: DateTime<Utc>,
132 pub auth_method: VoteAuthMethod,
136}
137
138impl Vote {
139 pub fn new(
141 resolution_id: Uuid,
142 owner_id: Uuid,
143 unit_id: Uuid,
144 vote_choice: VoteChoice,
145 voting_power: Decimal,
146 proxy_owner_id: Option<Uuid>,
147 ) -> Result<Self, String> {
148 if voting_power <= Decimal::ZERO {
150 return Err("Voting power must be positive".to_string());
151 }
152 if voting_power > MAX_VOTING_POWER {
153 return Err("Voting power exceeds maximum (10000 dix-millièmes)".to_string());
154 }
155
156 if let Some(proxy_id) = proxy_owner_id {
158 if proxy_id == owner_id {
159 return Err("Owner cannot be their own proxy".to_string());
160 }
161 }
162
163 Ok(Self {
164 id: Uuid::new_v4(),
165 resolution_id,
166 owner_id,
167 unit_id,
168 vote_choice,
169 voting_power,
170 proxy_owner_id,
171 voted_at: Utc::now(),
172 auth_method: VoteAuthMethod::Presence,
173 })
174 }
175
176 #[allow(clippy::too_many_arguments)]
184 pub fn new_with_auth_method(
185 resolution_id: Uuid,
186 owner_id: Uuid,
187 unit_id: Uuid,
188 vote_choice: VoteChoice,
189 voting_power: Decimal,
190 proxy_owner_id: Option<Uuid>,
191 auth_method: VoteAuthMethod,
192 ) -> Result<Self, String> {
193 let mut vote = Self::new(
194 resolution_id,
195 owner_id,
196 unit_id,
197 vote_choice,
198 voting_power,
199 proxy_owner_id,
200 )?;
201 vote.auth_method = auth_method;
202 Ok(vote)
203 }
204
205 pub fn is_proxy_vote(&self) -> bool {
207 self.proxy_owner_id.is_some()
208 }
209
210 pub fn effective_voter_id(&self) -> Uuid {
212 self.proxy_owner_id.unwrap_or(self.owner_id)
213 }
214
215 pub fn change_vote(&mut self, new_choice: VoteChoice) -> Result<(), String> {
217 self.vote_choice = new_choice;
221 self.voted_at = Utc::now();
222 Ok(())
223 }
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_create_vote_success() {
232 let resolution_id = Uuid::new_v4();
233 let owner_id = Uuid::new_v4();
234 let unit_id = Uuid::new_v4();
235
236 let vote = Vote::new(
237 resolution_id,
238 owner_id,
239 unit_id,
240 VoteChoice::Pour,
241 dec!(150), None,
243 );
244
245 assert!(vote.is_ok());
246 let vote = vote.unwrap();
247 assert_eq!(vote.resolution_id, resolution_id);
248 assert_eq!(vote.owner_id, owner_id);
249 assert_eq!(vote.unit_id, unit_id);
250 assert_eq!(vote.vote_choice, VoteChoice::Pour);
251 assert_eq!(vote.voting_power, dec!(150));
252 assert!(!vote.is_proxy_vote());
253 assert_eq!(vote.effective_voter_id(), owner_id);
254 }
255
256 #[test]
257 fn test_create_vote_with_proxy() {
258 let resolution_id = Uuid::new_v4();
259 let owner_id = Uuid::new_v4();
260 let unit_id = Uuid::new_v4();
261 let proxy_id = Uuid::new_v4();
262
263 let vote = Vote::new(
264 resolution_id,
265 owner_id,
266 unit_id,
267 VoteChoice::Contre,
268 dec!(200),
269 Some(proxy_id),
270 );
271
272 assert!(vote.is_ok());
273 let vote = vote.unwrap();
274 assert!(vote.is_proxy_vote());
275 assert_eq!(vote.effective_voter_id(), proxy_id);
276 assert_eq!(vote.proxy_owner_id, Some(proxy_id));
277 }
278
279 #[test]
280 fn test_create_vote_zero_voting_power_fails() {
281 let resolution_id = Uuid::new_v4();
282 let owner_id = Uuid::new_v4();
283 let unit_id = Uuid::new_v4();
284
285 let vote = Vote::new(
286 resolution_id,
287 owner_id,
288 unit_id,
289 VoteChoice::Pour,
290 dec!(0),
291 None,
292 );
293
294 assert!(vote.is_err());
295 assert_eq!(vote.unwrap_err(), "Voting power must be positive");
296 }
297
298 #[test]
299 fn test_create_vote_negative_voting_power_fails() {
300 let resolution_id = Uuid::new_v4();
301 let owner_id = Uuid::new_v4();
302 let unit_id = Uuid::new_v4();
303
304 let vote = Vote::new(
305 resolution_id,
306 owner_id,
307 unit_id,
308 VoteChoice::Pour,
309 dec!(-50),
310 None,
311 );
312
313 assert!(vote.is_err());
314 assert_eq!(vote.unwrap_err(), "Voting power must be positive");
315 }
316
317 #[test]
318 fn test_create_vote_excessive_voting_power_fails() {
319 let resolution_id = Uuid::new_v4();
320 let owner_id = Uuid::new_v4();
321 let unit_id = Uuid::new_v4();
322
323 let vote = Vote::new(
324 resolution_id,
325 owner_id,
326 unit_id,
327 VoteChoice::Pour,
328 dec!(15000), None,
330 );
331
332 assert!(vote.is_err());
333 assert!(vote.unwrap_err().contains("exceeds maximum"));
334 }
335
336 #[test]
337 fn test_create_vote_self_proxy_fails() {
338 let resolution_id = Uuid::new_v4();
339 let owner_id = Uuid::new_v4();
340 let unit_id = Uuid::new_v4();
341
342 let vote = Vote::new(
343 resolution_id,
344 owner_id,
345 unit_id,
346 VoteChoice::Pour,
347 dec!(150),
348 Some(owner_id), );
350
351 assert!(vote.is_err());
352 assert_eq!(vote.unwrap_err(), "Owner cannot be their own proxy");
353 }
354
355 #[test]
356 fn test_change_vote() {
357 let resolution_id = Uuid::new_v4();
358 let owner_id = Uuid::new_v4();
359 let unit_id = Uuid::new_v4();
360
361 let mut vote = Vote::new(
362 resolution_id,
363 owner_id,
364 unit_id,
365 VoteChoice::Pour,
366 dec!(150),
367 None,
368 )
369 .unwrap();
370
371 assert_eq!(vote.vote_choice, VoteChoice::Pour);
372
373 let result = vote.change_vote(VoteChoice::Contre);
374 assert!(result.is_ok());
375 assert_eq!(vote.vote_choice, VoteChoice::Contre);
376 }
377
378 #[test]
379 fn test_vote_choice_serialization() {
380 let pour = VoteChoice::Pour;
382 let contre = VoteChoice::Contre;
383 let abstention = VoteChoice::Abstention;
384
385 let json_pour = serde_json::to_string(&pour).unwrap();
386 let json_contre = serde_json::to_string(&contre).unwrap();
387 let json_abstention = serde_json::to_string(&abstention).unwrap();
388
389 assert_eq!(json_pour, "\"pour\"");
390 assert_eq!(json_contre, "\"contre\"");
391 assert_eq!(json_abstention, "\"abstention\"");
392 }
393
394 #[test]
395 fn test_vote_choice_deserialization() {
396 let pour: VoteChoice = serde_json::from_str("\"pour\"").unwrap();
398 let contre: VoteChoice = serde_json::from_str("\"contre\"").unwrap();
399 let abstention: VoteChoice = serde_json::from_str("\"abstention\"").unwrap();
400
401 assert_eq!(pour, VoteChoice::Pour);
402 assert_eq!(contre, VoteChoice::Contre);
403 assert_eq!(abstention, VoteChoice::Abstention);
404 }
405
406 #[test]
412 fn happy_itsme_suffit_pour_un_vote_distant() {
413 let resultat =
414 assert_vote_auth_sufficient(MeetingMode::Remote, Some(VoteAuthMethod::Itsme), false);
415 assert_eq!(resultat, Ok(VoteAuthMethod::Itsme));
416 }
417
418 #[test]
420 fn happy_eid_suffit_pour_un_vote_hybride() {
421 let resultat =
422 assert_vote_auth_sufficient(MeetingMode::Hybrid, Some(VoteAuthMethod::Eid), false);
423 assert_eq!(resultat, Ok(VoteAuthMethod::Eid));
424 }
425
426 #[test]
431 fn edge_procuration_reelle_autorisee_a_distance() {
432 let resultat =
433 assert_vote_auth_sufficient(MeetingMode::Remote, Some(VoteAuthMethod::Proxy), true);
434 assert_eq!(resultat, Ok(VoteAuthMethod::Proxy));
435 }
436
437 #[test]
441 fn edge_proxy_declare_sans_mandat_reel_est_insuffisant() {
442 let resultat =
443 assert_vote_auth_sufficient(MeetingMode::Remote, Some(VoteAuthMethod::Proxy), false);
444 assert_eq!(
445 resultat,
446 Err(VoteAuthError::Insufficient {
447 mode: MeetingMode::Remote,
448 auth_method: VoteAuthMethod::Proxy,
449 })
450 );
451 }
452
453 #[test]
456 fn edge_presence_suffit_en_ag_physique() {
457 let resultat = assert_vote_auth_sufficient(
458 MeetingMode::InPerson,
459 Some(VoteAuthMethod::Presence),
460 false,
461 );
462 assert_eq!(resultat, Ok(VoteAuthMethod::Presence));
463 }
464
465 #[test]
469 fn security_presence_insuffisante_pour_un_vote_distant() {
470 let resultat =
471 assert_vote_auth_sufficient(MeetingMode::Remote, Some(VoteAuthMethod::Presence), false);
472 assert_eq!(
473 resultat,
474 Err(VoteAuthError::Insufficient {
475 mode: MeetingMode::Remote,
476 auth_method: VoteAuthMethod::Presence,
477 })
478 );
479 }
480
481 #[test]
484 fn negative_auth_method_absent_est_refuse() {
485 let resultat = assert_vote_auth_sufficient(MeetingMode::InPerson, None, false);
486 assert_eq!(resultat, Err(VoteAuthError::Missing));
487 }
488
489 #[test]
490 fn edge_vote_auth_method_db_round_trip() {
491 for m in [
492 VoteAuthMethod::Presence,
493 VoteAuthMethod::Proxy,
494 VoteAuthMethod::Itsme,
495 VoteAuthMethod::Eid,
496 ] {
497 assert_eq!(VoteAuthMethod::from_db_string(m.to_db_str()), Ok(m));
498 }
499 }
500
501 #[test]
502 fn negative_vote_auth_method_unknown_db_string_is_rejected() {
503 assert!(VoteAuthMethod::from_db_string("carrier_pigeon").is_err());
504 }
505
506 #[test]
509 fn happy_vote_new_defaults_to_presence() {
510 let vote = Vote::new(
511 Uuid::new_v4(),
512 Uuid::new_v4(),
513 Uuid::new_v4(),
514 VoteChoice::Pour,
515 dec!(100),
516 None,
517 )
518 .expect("vote valide");
519 assert_eq!(vote.auth_method, VoteAuthMethod::Presence);
520 }
521
522 #[test]
523 fn happy_vote_new_with_auth_method_sets_field() {
524 let vote = Vote::new_with_auth_method(
525 Uuid::new_v4(),
526 Uuid::new_v4(),
527 Uuid::new_v4(),
528 VoteChoice::Pour,
529 dec!(100),
530 None,
531 VoteAuthMethod::Itsme,
532 )
533 .expect("vote valide");
534 assert_eq!(vote.auth_method, VoteAuthMethod::Itsme);
535 }
536}