koprogo_api/infrastructure/database/repositories/
vote_repository_impl.rs1use crate::application::ports::VoteRepository;
2use crate::domain::entities::{Vote, VoteAuthMethod, VoteChoice};
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use rust_decimal::Decimal;
6use sqlx::Row;
7use uuid::Uuid;
8
9pub struct PostgresVoteRepository {
10 pool: DbPool,
11}
12
13impl PostgresVoteRepository {
14 pub fn new(pool: DbPool) -> Self {
15 Self { pool }
16 }
17
18 fn vote_from_row(row: sqlx::postgres::PgRow) -> Result<Vote, String> {
23 let vote_choice_str: String = row.get("vote_choice");
24 let vote_choice = match vote_choice_str.as_str() {
25 "Contre" => VoteChoice::Contre,
26 "Abstention" => VoteChoice::Abstention,
27 _ => VoteChoice::Pour,
28 };
29 let auth_method_str: String = row.get("auth_method");
30 let auth_method = VoteAuthMethod::from_db_string(&auth_method_str)?;
31
32 Ok(Vote {
33 id: row.get("id"),
34 resolution_id: row.get("resolution_id"),
35 owner_id: row.get("owner_id"),
36 unit_id: row.get("unit_id"),
37 vote_choice,
38 voting_power: row.get("voting_power"),
39 proxy_owner_id: row.get("proxy_owner_id"),
40 voted_at: row.get("voted_at"),
41 auth_method,
42 })
43 }
44}
45
46#[async_trait]
47impl VoteRepository for PostgresVoteRepository {
48 async fn create(&self, vote: &Vote) -> Result<Vote, String> {
49 let vote_choice_str = match vote.vote_choice {
50 VoteChoice::Pour => "Pour",
51 VoteChoice::Contre => "Contre",
52 VoteChoice::Abstention => "Abstention",
53 };
54
55 sqlx::query(
56 r#"
57 INSERT INTO votes (
58 id, resolution_id, owner_id, unit_id, vote_choice,
59 voting_power, proxy_owner_id, voted_at, auth_method
60 )
61 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
62 "#,
63 )
64 .bind(vote.id)
65 .bind(vote.resolution_id)
66 .bind(vote.owner_id)
67 .bind(vote.unit_id)
68 .bind(vote_choice_str)
69 .bind(vote.voting_power)
70 .bind(vote.proxy_owner_id)
71 .bind(vote.voted_at)
72 .bind(vote.auth_method.to_db_str())
73 .execute(&self.pool)
74 .await
75 .map_err(|e| format!("Database error creating vote: {}", e))?;
76
77 Ok(vote.clone())
78 }
79
80 async fn find_by_id(&self, id: Uuid) -> Result<Option<Vote>, String> {
81 let row = sqlx::query(
82 r#"
83 SELECT id, resolution_id, owner_id, unit_id, vote_choice,
84 voting_power, proxy_owner_id, voted_at, auth_method
85 FROM votes
86 WHERE id = $1
87 "#,
88 )
89 .bind(id)
90 .fetch_optional(&self.pool)
91 .await
92 .map_err(|e| format!("Database error finding vote: {}", e))?;
93
94 row.map(Self::vote_from_row).transpose()
95 }
96
97 async fn find_by_resolution_id(&self, resolution_id: Uuid) -> Result<Vec<Vote>, String> {
98 let rows = sqlx::query(
99 r#"
100 SELECT id, resolution_id, owner_id, unit_id, vote_choice,
101 voting_power, proxy_owner_id, voted_at, auth_method
102 FROM votes
103 WHERE resolution_id = $1
104 ORDER BY voted_at ASC
105 "#,
106 )
107 .bind(resolution_id)
108 .fetch_all(&self.pool)
109 .await
110 .map_err(|e| format!("Database error finding votes by resolution: {}", e))?;
111
112 rows.into_iter().map(Self::vote_from_row).collect()
113 }
114
115 async fn find_by_owner_id(&self, owner_id: Uuid) -> Result<Vec<Vote>, String> {
116 let rows = sqlx::query(
117 r#"
118 SELECT id, resolution_id, owner_id, unit_id, vote_choice,
119 voting_power, proxy_owner_id, voted_at, auth_method
120 FROM votes
121 WHERE owner_id = $1
122 ORDER BY voted_at DESC
123 "#,
124 )
125 .bind(owner_id)
126 .fetch_all(&self.pool)
127 .await
128 .map_err(|e| format!("Database error finding votes by owner: {}", e))?;
129
130 rows.into_iter().map(Self::vote_from_row).collect()
131 }
132
133 async fn find_by_resolution_and_unit(
134 &self,
135 resolution_id: Uuid,
136 unit_id: Uuid,
137 ) -> Result<Option<Vote>, String> {
138 let row = sqlx::query(
139 r#"
140 SELECT id, resolution_id, owner_id, unit_id, vote_choice,
141 voting_power, proxy_owner_id, voted_at, auth_method
142 FROM votes
143 WHERE resolution_id = $1 AND unit_id = $2
144 "#,
145 )
146 .bind(resolution_id)
147 .bind(unit_id)
148 .fetch_optional(&self.pool)
149 .await
150 .map_err(|e| format!("Database error finding vote by resolution and unit: {}", e))?;
151
152 row.map(Self::vote_from_row).transpose()
153 }
154
155 async fn has_voted(&self, resolution_id: Uuid, unit_id: Uuid) -> Result<bool, String> {
156 let row = sqlx::query(
157 r#"
158 SELECT EXISTS(SELECT 1 FROM votes WHERE resolution_id = $1 AND unit_id = $2) AS has_voted
159 "#,
160 )
161 .bind(resolution_id)
162 .bind(unit_id)
163 .fetch_one(&self.pool)
164 .await
165 .map_err(|e| format!("Database error checking if voted: {}", e))?;
166
167 Ok(row.get("has_voted"))
168 }
169
170 async fn update(&self, vote: &Vote) -> Result<Vote, String> {
171 let vote_choice_str = match vote.vote_choice {
172 VoteChoice::Pour => "Pour",
173 VoteChoice::Contre => "Contre",
174 VoteChoice::Abstention => "Abstention",
175 };
176
177 sqlx::query(
178 r#"
179 UPDATE votes
180 SET resolution_id = $2, owner_id = $3, unit_id = $4, vote_choice = $5,
181 voting_power = $6, proxy_owner_id = $7, voted_at = $8, auth_method = $9
182 WHERE id = $1
183 "#,
184 )
185 .bind(vote.id)
186 .bind(vote.resolution_id)
187 .bind(vote.owner_id)
188 .bind(vote.unit_id)
189 .bind(vote_choice_str)
190 .bind(vote.voting_power)
191 .bind(vote.proxy_owner_id)
192 .bind(vote.voted_at)
193 .bind(vote.auth_method.to_db_str())
194 .execute(&self.pool)
195 .await
196 .map_err(|e| format!("Database error updating vote: {}", e))?;
197
198 Ok(vote.clone())
199 }
200
201 async fn delete(&self, id: Uuid) -> Result<bool, String> {
202 let result = sqlx::query(
203 r#"
204 DELETE FROM votes WHERE id = $1
205 "#,
206 )
207 .bind(id)
208 .execute(&self.pool)
209 .await
210 .map_err(|e| format!("Database error deleting vote: {}", e))?;
211
212 Ok(result.rows_affected() > 0)
213 }
214
215 async fn count_by_resolution_and_choice(
216 &self,
217 resolution_id: Uuid,
218 ) -> Result<(i32, i32, i32), String> {
219 let row = sqlx::query(
220 r#"
221 SELECT
222 COUNT(*) FILTER (WHERE vote_choice = 'Pour') AS pour_count,
223 COUNT(*) FILTER (WHERE vote_choice = 'Contre') AS contre_count,
224 COUNT(*) FILTER (WHERE vote_choice = 'Abstention') AS abstention_count
225 FROM votes
226 WHERE resolution_id = $1
227 "#,
228 )
229 .bind(resolution_id)
230 .fetch_one(&self.pool)
231 .await
232 .map_err(|e| format!("Database error counting votes: {}", e))?;
233
234 let pour_count: Option<i64> = row.get("pour_count");
235 let contre_count: Option<i64> = row.get("contre_count");
236 let abstention_count: Option<i64> = row.get("abstention_count");
237
238 Ok((
239 pour_count.unwrap_or(0) as i32,
240 contre_count.unwrap_or(0) as i32,
241 abstention_count.unwrap_or(0) as i32,
242 ))
243 }
244
245 async fn sum_voting_power_by_resolution(
246 &self,
247 resolution_id: Uuid,
248 ) -> Result<(Decimal, Decimal, Decimal), String> {
249 let row = sqlx::query(
250 r#"
251 SELECT
252 COALESCE(SUM(voting_power) FILTER (WHERE vote_choice = 'Pour'), 0)::NUMERIC(10,4) AS pour_power,
253 COALESCE(SUM(voting_power) FILTER (WHERE vote_choice = 'Contre'), 0)::NUMERIC(10,4) AS contre_power,
254 COALESCE(SUM(voting_power) FILTER (WHERE vote_choice = 'Abstention'), 0)::NUMERIC(10,4) AS abstention_power
255 FROM votes
256 WHERE resolution_id = $1
257 "#,
258 )
259 .bind(resolution_id)
260 .fetch_one(&self.pool)
261 .await
262 .map_err(|e| format!("Database error summing voting power: {}", e))?;
263
264 Ok((
266 row.get::<Decimal, _>("pour_power"),
267 row.get::<Decimal, _>("contre_power"),
268 row.get::<Decimal, _>("abstention_power"),
269 ))
270 }
271
272 async fn count_proxy_votes_for_mandataire(
274 &self,
275 resolution_id: Uuid,
276 proxy_owner_id: Uuid,
277 ) -> Result<(i64, Decimal), String> {
278 let row = sqlx::query(
279 r#"
280 SELECT
281 COUNT(*)::BIGINT AS proxy_count,
282 COALESCE(SUM(voting_power), 0)::NUMERIC(10,4) AS total_proxy_power
283 FROM votes
284 WHERE resolution_id = $1
285 AND proxy_owner_id = $2
286 "#,
287 )
288 .bind(resolution_id)
289 .bind(proxy_owner_id)
290 .fetch_one(&self.pool)
291 .await
292 .map_err(|e| format!("Database error counting proxies: {}", e))?;
293
294 Ok((
295 row.get::<i64, _>("proxy_count"),
296 row.get::<Decimal, _>("total_proxy_power"),
297 ))
298 }
299}