koprogo_api/infrastructure/database/repositories/
resolution_repository_impl.rs

1use crate::application::ports::ResolutionRepository;
2use crate::domain::entities::{MajorityType, Resolution, ResolutionStatus, ResolutionType};
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresResolutionRepository {
9    pool: DbPool,
10}
11
12impl PostgresResolutionRepository {
13    pub fn new(pool: DbPool) -> Self {
14        Self { pool }
15    }
16
17    /// Parse MajorityType from database string format
18    fn parse_majority_type(s: &str) -> MajorityType {
19        if s.starts_with("Qualified:") {
20            let threshold = s
21                .strip_prefix("Qualified:")
22                .and_then(|t| t.parse::<f64>().ok())
23                .unwrap_or(0.67);
24            MajorityType::Qualified(threshold)
25        } else if s == "Absolute" {
26            MajorityType::Absolute
27        } else {
28            MajorityType::Simple
29        }
30    }
31
32    /// Convert MajorityType to database string format
33    fn majority_type_to_string(majority: &MajorityType) -> String {
34        match majority {
35            MajorityType::Simple => "Simple".to_string(),
36            MajorityType::Absolute => "Absolute".to_string(),
37            MajorityType::Qualified(threshold) => format!("Qualified:{}", threshold),
38        }
39    }
40}
41
42#[async_trait]
43impl ResolutionRepository for PostgresResolutionRepository {
44    async fn create(&self, resolution: &Resolution) -> Result<Resolution, String> {
45        let resolution_type_str = match resolution.resolution_type {
46            ResolutionType::Ordinary => "Ordinary",
47            ResolutionType::Extraordinary => "Extraordinary",
48        };
49
50        let status_str = match resolution.status {
51            ResolutionStatus::Pending => "Pending",
52            ResolutionStatus::Adopted => "Adopted",
53            ResolutionStatus::Rejected => "Rejected",
54        };
55
56        let majority_str = Self::majority_type_to_string(&resolution.majority_required);
57
58        sqlx::query(
59            r#"
60            INSERT INTO resolutions (
61                id, meeting_id, title, description, resolution_type, majority_required,
62                vote_count_pour, vote_count_contre, vote_count_abstention,
63                total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
64                status, created_at, voted_at
65            )
66            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
67            "#,
68        )
69        .bind(resolution.id)
70        .bind(resolution.meeting_id)
71        .bind(&resolution.title)
72        .bind(&resolution.description)
73        .bind(resolution_type_str)
74        .bind(majority_str)
75        .bind(resolution.vote_count_pour)
76        .bind(resolution.vote_count_contre)
77        .bind(resolution.vote_count_abstention)
78        .bind(resolution.total_voting_power_pour)
79        .bind(resolution.total_voting_power_contre)
80        .bind(resolution.total_voting_power_abstention)
81        .bind(status_str)
82        .bind(resolution.created_at)
83        .bind(resolution.voted_at)
84        .execute(&self.pool)
85        .await
86        .map_err(|e| format!("Database error creating resolution: {}", e))?;
87
88        Ok(resolution.clone())
89    }
90
91    async fn find_by_id(&self, id: Uuid) -> Result<Option<Resolution>, String> {
92        let row = sqlx::query(
93            r#"
94            SELECT id, meeting_id, title, description, resolution_type, majority_required,
95                   vote_count_pour, vote_count_contre, vote_count_abstention,
96                   total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
97                   status, created_at, voted_at
98            FROM resolutions
99            WHERE id = $1
100            "#,
101        )
102        .bind(id)
103        .fetch_optional(&self.pool)
104        .await
105        .map_err(|e| format!("Database error finding resolution: {}", e))?;
106
107        Ok(row.map(|row| {
108            let resolution_type_str: String = row.get("resolution_type");
109            let resolution_type = match resolution_type_str.as_str() {
110                "Extraordinary" => ResolutionType::Extraordinary,
111                _ => ResolutionType::Ordinary,
112            };
113
114            let status_str: String = row.get("status");
115            let status = match status_str.as_str() {
116                "Adopted" => ResolutionStatus::Adopted,
117                "Rejected" => ResolutionStatus::Rejected,
118                _ => ResolutionStatus::Pending,
119            };
120
121            let majority_str: String = row.get("majority_required");
122            let majority_required = Self::parse_majority_type(&majority_str);
123
124            Resolution {
125                id: row.get("id"),
126                meeting_id: row.get("meeting_id"),
127                title: row.get("title"),
128                description: row.get("description"),
129                resolution_type,
130                majority_required,
131                vote_count_pour: row.get("vote_count_pour"),
132                vote_count_contre: row.get("vote_count_contre"),
133                vote_count_abstention: row.get("vote_count_abstention"),
134                total_voting_power_pour: row.get("total_voting_power_pour"),
135                total_voting_power_contre: row.get("total_voting_power_contre"),
136                total_voting_power_abstention: row.get("total_voting_power_abstention"),
137                status,
138                created_at: row.get("created_at"),
139                voted_at: row.get("voted_at"),
140            }
141        }))
142    }
143
144    async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
145        let rows = sqlx::query(
146            r#"
147            SELECT id, meeting_id, title, description, resolution_type, majority_required,
148                   vote_count_pour, vote_count_contre, vote_count_abstention,
149                   total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
150                   status, created_at, voted_at
151            FROM resolutions
152            WHERE meeting_id = $1
153            ORDER BY created_at ASC
154            "#,
155        )
156        .bind(meeting_id)
157        .fetch_all(&self.pool)
158        .await
159        .map_err(|e| format!("Database error finding resolutions by meeting: {}", e))?;
160
161        Ok(rows
162            .into_iter()
163            .map(|row| {
164                let resolution_type_str: String = row.get("resolution_type");
165                let resolution_type = match resolution_type_str.as_str() {
166                    "Extraordinary" => ResolutionType::Extraordinary,
167                    _ => ResolutionType::Ordinary,
168                };
169
170                let status_str: String = row.get("status");
171                let status = match status_str.as_str() {
172                    "Adopted" => ResolutionStatus::Adopted,
173                    "Rejected" => ResolutionStatus::Rejected,
174                    _ => ResolutionStatus::Pending,
175                };
176
177                let majority_str: String = row.get("majority_required");
178                let majority_required = Self::parse_majority_type(&majority_str);
179
180                Resolution {
181                    id: row.get("id"),
182                    meeting_id: row.get("meeting_id"),
183                    title: row.get("title"),
184                    description: row.get("description"),
185                    resolution_type,
186                    majority_required,
187                    vote_count_pour: row.get("vote_count_pour"),
188                    vote_count_contre: row.get("vote_count_contre"),
189                    vote_count_abstention: row.get("vote_count_abstention"),
190                    total_voting_power_pour: row.get("total_voting_power_pour"),
191                    total_voting_power_contre: row.get("total_voting_power_contre"),
192                    total_voting_power_abstention: row.get("total_voting_power_abstention"),
193                    status,
194                    created_at: row.get("created_at"),
195                    voted_at: row.get("voted_at"),
196                }
197            })
198            .collect())
199    }
200
201    async fn find_by_status(&self, status: ResolutionStatus) -> Result<Vec<Resolution>, String> {
202        let status_str = match status {
203            ResolutionStatus::Pending => "Pending",
204            ResolutionStatus::Adopted => "Adopted",
205            ResolutionStatus::Rejected => "Rejected",
206        };
207
208        let rows = sqlx::query(
209            r#"
210            SELECT id, meeting_id, title, description, resolution_type, majority_required,
211                   vote_count_pour, vote_count_contre, vote_count_abstention,
212                   total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
213                   status, created_at, voted_at
214            FROM resolutions
215            WHERE status = $1
216            ORDER BY created_at DESC
217            "#,
218        )
219        .bind(status_str)
220        .fetch_all(&self.pool)
221        .await
222        .map_err(|e| format!("Database error finding resolutions by status: {}", e))?;
223
224        Ok(rows
225            .into_iter()
226            .map(|row| {
227                let resolution_type_str: String = row.get("resolution_type");
228                let resolution_type = match resolution_type_str.as_str() {
229                    "Extraordinary" => ResolutionType::Extraordinary,
230                    _ => ResolutionType::Ordinary,
231                };
232
233                let majority_str: String = row.get("majority_required");
234                let majority_required = Self::parse_majority_type(&majority_str);
235
236                Resolution {
237                    id: row.get("id"),
238                    meeting_id: row.get("meeting_id"),
239                    title: row.get("title"),
240                    description: row.get("description"),
241                    resolution_type,
242                    majority_required,
243                    vote_count_pour: row.get("vote_count_pour"),
244                    vote_count_contre: row.get("vote_count_contre"),
245                    vote_count_abstention: row.get("vote_count_abstention"),
246                    total_voting_power_pour: row.get("total_voting_power_pour"),
247                    total_voting_power_contre: row.get("total_voting_power_contre"),
248                    total_voting_power_abstention: row.get("total_voting_power_abstention"),
249                    status: status.clone(),
250                    created_at: row.get("created_at"),
251                    voted_at: row.get("voted_at"),
252                }
253            })
254            .collect())
255    }
256
257    async fn update(&self, resolution: &Resolution) -> Result<Resolution, String> {
258        let resolution_type_str = match resolution.resolution_type {
259            ResolutionType::Ordinary => "Ordinary",
260            ResolutionType::Extraordinary => "Extraordinary",
261        };
262
263        let status_str = match resolution.status {
264            ResolutionStatus::Pending => "Pending",
265            ResolutionStatus::Adopted => "Adopted",
266            ResolutionStatus::Rejected => "Rejected",
267        };
268
269        let majority_str = Self::majority_type_to_string(&resolution.majority_required);
270
271        sqlx::query(
272            r#"
273            UPDATE resolutions
274            SET meeting_id = $2, title = $3, description = $4, resolution_type = $5,
275                majority_required = $6, vote_count_pour = $7, vote_count_contre = $8,
276                vote_count_abstention = $9, total_voting_power_pour = $10,
277                total_voting_power_contre = $11, total_voting_power_abstention = $12,
278                status = $13, voted_at = $14
279            WHERE id = $1
280            "#,
281        )
282        .bind(resolution.id)
283        .bind(resolution.meeting_id)
284        .bind(&resolution.title)
285        .bind(&resolution.description)
286        .bind(resolution_type_str)
287        .bind(majority_str)
288        .bind(resolution.vote_count_pour)
289        .bind(resolution.vote_count_contre)
290        .bind(resolution.vote_count_abstention)
291        .bind(resolution.total_voting_power_pour)
292        .bind(resolution.total_voting_power_contre)
293        .bind(resolution.total_voting_power_abstention)
294        .bind(status_str)
295        .bind(resolution.voted_at)
296        .execute(&self.pool)
297        .await
298        .map_err(|e| format!("Database error updating resolution: {}", e))?;
299
300        Ok(resolution.clone())
301    }
302
303    async fn delete(&self, id: Uuid) -> Result<bool, String> {
304        let result = sqlx::query(
305            r#"
306            DELETE FROM resolutions WHERE id = $1
307            "#,
308        )
309        .bind(id)
310        .execute(&self.pool)
311        .await
312        .map_err(|e| format!("Database error deleting resolution: {}", e))?;
313
314        Ok(result.rows_affected() > 0)
315    }
316
317    async fn update_vote_counts(
318        &self,
319        resolution_id: Uuid,
320        vote_count_pour: i32,
321        vote_count_contre: i32,
322        vote_count_abstention: i32,
323        total_voting_power_pour: f64,
324        total_voting_power_contre: f64,
325        total_voting_power_abstention: f64,
326    ) -> Result<(), String> {
327        sqlx::query(
328            r#"
329            UPDATE resolutions
330            SET vote_count_pour = $2, vote_count_contre = $3, vote_count_abstention = $4,
331                total_voting_power_pour = $5, total_voting_power_contre = $6,
332                total_voting_power_abstention = $7
333            WHERE id = $1
334            "#,
335        )
336        .bind(resolution_id)
337        .bind(vote_count_pour)
338        .bind(vote_count_contre)
339        .bind(vote_count_abstention)
340        .bind(total_voting_power_pour)
341        .bind(total_voting_power_contre)
342        .bind(total_voting_power_abstention)
343        .execute(&self.pool)
344        .await
345        .map_err(|e| format!("Database error updating vote counts: {}", e))?;
346
347        Ok(())
348    }
349
350    async fn close_voting(
351        &self,
352        resolution_id: Uuid,
353        final_status: ResolutionStatus,
354    ) -> Result<(), String> {
355        let status_str = match final_status {
356            ResolutionStatus::Pending => "Pending",
357            ResolutionStatus::Adopted => "Adopted",
358            ResolutionStatus::Rejected => "Rejected",
359        };
360
361        sqlx::query(
362            r#"
363            UPDATE resolutions
364            SET status = $2, voted_at = CURRENT_TIMESTAMP
365            WHERE id = $1
366            "#,
367        )
368        .bind(resolution_id)
369        .bind(status_str)
370        .execute(&self.pool)
371        .await
372        .map_err(|e| format!("Database error closing voting: {}", e))?;
373
374        Ok(())
375    }
376
377    async fn get_meeting_vote_summary(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
378        // Same as find_by_meeting_id, but could be enhanced with additional stats
379        self.find_by_meeting_id(meeting_id).await
380    }
381}