koprogo_api/domain/entities/
vote.rs1use 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 VoteChoice {
9 Pour, Contre, Abstention, }
13
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, utoipa::ToSchema)]
16pub struct Vote {
17 pub id: Uuid,
18 pub resolution_id: Uuid,
19 pub owner_id: Uuid,
20 pub unit_id: Uuid,
21 pub vote_choice: VoteChoice,
22 pub voting_power: f64, pub proxy_owner_id: Option<Uuid>, pub voted_at: DateTime<Utc>,
25}
26
27impl Vote {
28 pub fn new(
30 resolution_id: Uuid,
31 owner_id: Uuid,
32 unit_id: Uuid,
33 vote_choice: VoteChoice,
34 voting_power: f64,
35 proxy_owner_id: Option<Uuid>,
36 ) -> Result<Self, String> {
37 if voting_power <= 0.0 {
39 return Err("Voting power must be positive".to_string());
40 }
41 if voting_power > 1000.0 {
42 return Err("Voting power exceeds maximum (1000 millièmes)".to_string());
43 }
44
45 if let Some(proxy_id) = proxy_owner_id {
47 if proxy_id == owner_id {
48 return Err("Owner cannot be their own proxy".to_string());
49 }
50 }
51
52 Ok(Self {
53 id: Uuid::new_v4(),
54 resolution_id,
55 owner_id,
56 unit_id,
57 vote_choice,
58 voting_power,
59 proxy_owner_id,
60 voted_at: Utc::now(),
61 })
62 }
63
64 pub fn is_proxy_vote(&self) -> bool {
66 self.proxy_owner_id.is_some()
67 }
68
69 pub fn effective_voter_id(&self) -> Uuid {
71 self.proxy_owner_id.unwrap_or(self.owner_id)
72 }
73
74 pub fn change_vote(&mut self, new_choice: VoteChoice) -> Result<(), String> {
76 self.vote_choice = new_choice;
80 self.voted_at = Utc::now();
81 Ok(())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn test_create_vote_success() {
91 let resolution_id = Uuid::new_v4();
92 let owner_id = Uuid::new_v4();
93 let unit_id = Uuid::new_v4();
94
95 let vote = Vote::new(
96 resolution_id,
97 owner_id,
98 unit_id,
99 VoteChoice::Pour,
100 150.0, None,
102 );
103
104 assert!(vote.is_ok());
105 let vote = vote.unwrap();
106 assert_eq!(vote.resolution_id, resolution_id);
107 assert_eq!(vote.owner_id, owner_id);
108 assert_eq!(vote.unit_id, unit_id);
109 assert_eq!(vote.vote_choice, VoteChoice::Pour);
110 assert_eq!(vote.voting_power, 150.0);
111 assert!(!vote.is_proxy_vote());
112 assert_eq!(vote.effective_voter_id(), owner_id);
113 }
114
115 #[test]
116 fn test_create_vote_with_proxy() {
117 let resolution_id = Uuid::new_v4();
118 let owner_id = Uuid::new_v4();
119 let unit_id = Uuid::new_v4();
120 let proxy_id = Uuid::new_v4();
121
122 let vote = Vote::new(
123 resolution_id,
124 owner_id,
125 unit_id,
126 VoteChoice::Contre,
127 200.0,
128 Some(proxy_id),
129 );
130
131 assert!(vote.is_ok());
132 let vote = vote.unwrap();
133 assert!(vote.is_proxy_vote());
134 assert_eq!(vote.effective_voter_id(), proxy_id);
135 assert_eq!(vote.proxy_owner_id, Some(proxy_id));
136 }
137
138 #[test]
139 fn test_create_vote_zero_voting_power_fails() {
140 let resolution_id = Uuid::new_v4();
141 let owner_id = Uuid::new_v4();
142 let unit_id = Uuid::new_v4();
143
144 let vote = Vote::new(
145 resolution_id,
146 owner_id,
147 unit_id,
148 VoteChoice::Pour,
149 0.0,
150 None,
151 );
152
153 assert!(vote.is_err());
154 assert_eq!(vote.unwrap_err(), "Voting power must be positive");
155 }
156
157 #[test]
158 fn test_create_vote_negative_voting_power_fails() {
159 let resolution_id = Uuid::new_v4();
160 let owner_id = Uuid::new_v4();
161 let unit_id = Uuid::new_v4();
162
163 let vote = Vote::new(
164 resolution_id,
165 owner_id,
166 unit_id,
167 VoteChoice::Pour,
168 -50.0,
169 None,
170 );
171
172 assert!(vote.is_err());
173 assert_eq!(vote.unwrap_err(), "Voting power must be positive");
174 }
175
176 #[test]
177 fn test_create_vote_excessive_voting_power_fails() {
178 let resolution_id = Uuid::new_v4();
179 let owner_id = Uuid::new_v4();
180 let unit_id = Uuid::new_v4();
181
182 let vote = Vote::new(
183 resolution_id,
184 owner_id,
185 unit_id,
186 VoteChoice::Pour,
187 1500.0, None,
189 );
190
191 assert!(vote.is_err());
192 assert!(vote.unwrap_err().contains("exceeds maximum"));
193 }
194
195 #[test]
196 fn test_create_vote_self_proxy_fails() {
197 let resolution_id = Uuid::new_v4();
198 let owner_id = Uuid::new_v4();
199 let unit_id = Uuid::new_v4();
200
201 let vote = Vote::new(
202 resolution_id,
203 owner_id,
204 unit_id,
205 VoteChoice::Pour,
206 150.0,
207 Some(owner_id), );
209
210 assert!(vote.is_err());
211 assert_eq!(vote.unwrap_err(), "Owner cannot be their own proxy");
212 }
213
214 #[test]
215 fn test_change_vote() {
216 let resolution_id = Uuid::new_v4();
217 let owner_id = Uuid::new_v4();
218 let unit_id = Uuid::new_v4();
219
220 let mut vote = Vote::new(
221 resolution_id,
222 owner_id,
223 unit_id,
224 VoteChoice::Pour,
225 150.0,
226 None,
227 )
228 .unwrap();
229
230 assert_eq!(vote.vote_choice, VoteChoice::Pour);
231
232 let result = vote.change_vote(VoteChoice::Contre);
233 assert!(result.is_ok());
234 assert_eq!(vote.vote_choice, VoteChoice::Contre);
235 }
236
237 #[test]
238 fn test_vote_choice_serialization() {
239 let pour = VoteChoice::Pour;
241 let contre = VoteChoice::Contre;
242 let abstention = VoteChoice::Abstention;
243
244 let json_pour = serde_json::to_string(&pour).unwrap();
245 let json_contre = serde_json::to_string(&contre).unwrap();
246 let json_abstention = serde_json::to_string(&abstention).unwrap();
247
248 assert_eq!(json_pour, "\"pour\"");
249 assert_eq!(json_contre, "\"contre\"");
250 assert_eq!(json_abstention, "\"abstention\"");
251 }
252
253 #[test]
254 fn test_vote_choice_deserialization() {
255 let pour: VoteChoice = serde_json::from_str("\"pour\"").unwrap();
257 let contre: VoteChoice = serde_json::from_str("\"contre\"").unwrap();
258 let abstention: VoteChoice = serde_json::from_str("\"abstention\"").unwrap();
259
260 assert_eq!(pour, VoteChoice::Pour);
261 assert_eq!(contre, VoteChoice::Contre);
262 assert_eq!(abstention, VoteChoice::Abstention);
263 }
264}
265
266#[derive(Debug, Clone, PartialEq)]
268pub enum ProxyValidationError {
269 TooManyMandates { current: usize, max: usize },
271 ExceedsQuotaThreshold {
273 current_pct: f64,
274 max_pct: f64,
275 total_quotas: i32,
276 },
277}
278
279impl std::fmt::Display for ProxyValidationError {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 match self {
282 Self::TooManyMandates { current, max } => {
283 write!(
284 f,
285 "Le mandataire détient déjà {} mandats (maximum: {} selon Art. 3.87 §6 CC)",
286 current, max
287 )
288 }
289 Self::ExceedsQuotaThreshold {
290 current_pct,
291 max_pct,
292 total_quotas: _,
293 } => {
294 write!(f, "Le mandataire représenterait {:.1}% des quotités (maximum: {:.0}% selon Art. 3.87 §6 CC)", current_pct, max_pct)
295 }
296 }
297 }
298}
299
300pub fn validate_proxy_mandate(
308 existing_mandate_count: usize,
309 existing_delegated_quotas: i32,
310 new_voting_power: i32,
311 total_building_quotas: i32,
312 max_mandates: usize, max_quota_pct: f64, ) -> Result<(), ProxyValidationError> {
315 if existing_mandate_count >= max_mandates {
317 return Err(ProxyValidationError::TooManyMandates {
318 current: existing_mandate_count,
319 max: max_mandates,
320 });
321 }
322
323 if total_building_quotas > 0 {
325 let new_total_quotas = existing_delegated_quotas + new_voting_power;
326 let new_pct = new_total_quotas as f64 / total_building_quotas as f64;
327 if new_pct > max_quota_pct {
328 return Err(ProxyValidationError::ExceedsQuotaThreshold {
329 current_pct: new_pct * 100.0,
330 max_pct: max_quota_pct * 100.0,
331 total_quotas: total_building_quotas,
332 });
333 }
334 }
335
336 Ok(())
337}
338
339#[cfg(test)]
340mod proxy_validation_tests {
341 use super::*;
342
343 #[test]
344 fn test_proxy_mandate_count_limit() {
345 let result = validate_proxy_mandate(3, 150, 50, 1000, 3, 0.10);
347 assert!(result.is_err());
348 assert!(matches!(
349 result,
350 Err(ProxyValidationError::TooManyMandates { current: 3, max: 3 })
351 ));
352 }
353
354 #[test]
355 fn test_proxy_mandate_count_ok() {
356 let result = validate_proxy_mandate(2, 40, 50, 1000, 3, 0.10);
358 assert!(result.is_ok());
359 }
360
361 #[test]
362 fn test_proxy_quota_threshold_exceeded() {
363 let result = validate_proxy_mandate(1, 80, 50, 1000, 3, 0.10);
365 assert!(result.is_err());
366 assert!(matches!(
367 result,
368 Err(ProxyValidationError::ExceedsQuotaThreshold { .. })
369 ));
370 }
371
372 #[test]
373 fn test_proxy_quota_threshold_ok() {
374 let result = validate_proxy_mandate(1, 50, 40, 1000, 3, 0.10);
376 assert!(result.is_ok());
377 }
378
379 #[test]
380 fn test_proxy_exactly_at_limit() {
381 let result = validate_proxy_mandate(2, 60, 40, 1000, 3, 0.10);
383 assert!(result.is_ok()); }
385}