koprogo_api/infrastructure/database/repositories/
vote_repository_impl.rs

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        // Get voting power sums as f64 (sqlx handles DECIMAL conversion)
313        Ok((
314            row.get::<f64, _>("pour_power"),
315            row.get::<f64, _>("contre_power"),
316            row.get::<f64, _>("abstention_power"),
317        ))
318    }
319}