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