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::FLOAT8, total_voting_power_contre::FLOAT8, total_voting_power_abstention::FLOAT8,
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                agenda_item_index: None,
141            }
142        }))
143    }
144
145    async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
146        let rows = sqlx::query(
147            r#"
148            SELECT id, meeting_id, title, description, resolution_type, majority_required,
149                   vote_count_pour, vote_count_contre, vote_count_abstention,
150                   total_voting_power_pour::FLOAT8, total_voting_power_contre::FLOAT8, total_voting_power_abstention::FLOAT8,
151                   status, created_at, voted_at
152            FROM resolutions
153            WHERE meeting_id = $1
154            ORDER BY created_at ASC
155            "#,
156        )
157        .bind(meeting_id)
158        .fetch_all(&self.pool)
159        .await
160        .map_err(|e| format!("Database error finding resolutions by meeting: {}", e))?;
161
162        Ok(rows
163            .into_iter()
164            .map(|row| {
165                let resolution_type_str: String = row.get("resolution_type");
166                let resolution_type = match resolution_type_str.as_str() {
167                    "Extraordinary" => ResolutionType::Extraordinary,
168                    _ => ResolutionType::Ordinary,
169                };
170
171                let status_str: String = row.get("status");
172                let status = match status_str.as_str() {
173                    "Adopted" => ResolutionStatus::Adopted,
174                    "Rejected" => ResolutionStatus::Rejected,
175                    _ => ResolutionStatus::Pending,
176                };
177
178                let majority_str: String = row.get("majority_required");
179                let majority_required = Self::parse_majority_type(&majority_str);
180
181                Resolution {
182                    id: row.get("id"),
183                    meeting_id: row.get("meeting_id"),
184                    title: row.get("title"),
185                    description: row.get("description"),
186                    resolution_type,
187                    majority_required,
188                    vote_count_pour: row.get("vote_count_pour"),
189                    vote_count_contre: row.get("vote_count_contre"),
190                    vote_count_abstention: row.get("vote_count_abstention"),
191                    total_voting_power_pour: row.get("total_voting_power_pour"),
192                    total_voting_power_contre: row.get("total_voting_power_contre"),
193                    total_voting_power_abstention: row.get("total_voting_power_abstention"),
194                    status,
195                    created_at: row.get("created_at"),
196                    voted_at: row.get("voted_at"),
197                    agenda_item_index: None,
198                }
199            })
200            .collect())
201    }
202
203    async fn find_by_status(&self, status: ResolutionStatus) -> Result<Vec<Resolution>, String> {
204        let status_str = match status {
205            ResolutionStatus::Pending => "Pending",
206            ResolutionStatus::Adopted => "Adopted",
207            ResolutionStatus::Rejected => "Rejected",
208        };
209
210        let rows = sqlx::query(
211            r#"
212            SELECT id, meeting_id, title, description, resolution_type, majority_required,
213                   vote_count_pour, vote_count_contre, vote_count_abstention,
214                   total_voting_power_pour::FLOAT8, total_voting_power_contre::FLOAT8, total_voting_power_abstention::FLOAT8,
215                   status, created_at, voted_at
216            FROM resolutions
217            WHERE status = $1
218            ORDER BY created_at DESC
219            "#,
220        )
221        .bind(status_str)
222        .fetch_all(&self.pool)
223        .await
224        .map_err(|e| format!("Database error finding resolutions by status: {}", e))?;
225
226        Ok(rows
227            .into_iter()
228            .map(|row| {
229                let resolution_type_str: String = row.get("resolution_type");
230                let resolution_type = match resolution_type_str.as_str() {
231                    "Extraordinary" => ResolutionType::Extraordinary,
232                    _ => ResolutionType::Ordinary,
233                };
234
235                let majority_str: String = row.get("majority_required");
236                let majority_required = Self::parse_majority_type(&majority_str);
237
238                Resolution {
239                    id: row.get("id"),
240                    meeting_id: row.get("meeting_id"),
241                    title: row.get("title"),
242                    description: row.get("description"),
243                    resolution_type,
244                    majority_required,
245                    vote_count_pour: row.get("vote_count_pour"),
246                    vote_count_contre: row.get("vote_count_contre"),
247                    vote_count_abstention: row.get("vote_count_abstention"),
248                    total_voting_power_pour: row.get("total_voting_power_pour"),
249                    total_voting_power_contre: row.get("total_voting_power_contre"),
250                    total_voting_power_abstention: row.get("total_voting_power_abstention"),
251                    status: status.clone(),
252                    created_at: row.get("created_at"),
253                    voted_at: row.get("voted_at"),
254                    agenda_item_index: None,
255                }
256            })
257            .collect())
258    }
259
260    async fn update(&self, resolution: &Resolution) -> Result<Resolution, String> {
261        let resolution_type_str = match resolution.resolution_type {
262            ResolutionType::Ordinary => "Ordinary",
263            ResolutionType::Extraordinary => "Extraordinary",
264        };
265
266        let status_str = match resolution.status {
267            ResolutionStatus::Pending => "Pending",
268            ResolutionStatus::Adopted => "Adopted",
269            ResolutionStatus::Rejected => "Rejected",
270        };
271
272        let majority_str = Self::majority_type_to_string(&resolution.majority_required);
273
274        sqlx::query(
275            r#"
276            UPDATE resolutions
277            SET meeting_id = $2, title = $3, description = $4, resolution_type = $5,
278                majority_required = $6, vote_count_pour = $7, vote_count_contre = $8,
279                vote_count_abstention = $9, total_voting_power_pour = $10,
280                total_voting_power_contre = $11, total_voting_power_abstention = $12,
281                status = $13, voted_at = $14
282            WHERE id = $1
283            "#,
284        )
285        .bind(resolution.id)
286        .bind(resolution.meeting_id)
287        .bind(&resolution.title)
288        .bind(&resolution.description)
289        .bind(resolution_type_str)
290        .bind(majority_str)
291        .bind(resolution.vote_count_pour)
292        .bind(resolution.vote_count_contre)
293        .bind(resolution.vote_count_abstention)
294        .bind(resolution.total_voting_power_pour)
295        .bind(resolution.total_voting_power_contre)
296        .bind(resolution.total_voting_power_abstention)
297        .bind(status_str)
298        .bind(resolution.voted_at)
299        .execute(&self.pool)
300        .await
301        .map_err(|e| format!("Database error updating resolution: {}", e))?;
302
303        Ok(resolution.clone())
304    }
305
306    async fn delete(&self, id: Uuid) -> Result<bool, String> {
307        let result = sqlx::query(
308            r#"
309            DELETE FROM resolutions WHERE id = $1
310            "#,
311        )
312        .bind(id)
313        .execute(&self.pool)
314        .await
315        .map_err(|e| format!("Database error deleting resolution: {}", e))?;
316
317        Ok(result.rows_affected() > 0)
318    }
319
320    async fn update_vote_counts(
321        &self,
322        resolution_id: Uuid,
323        vote_count_pour: i32,
324        vote_count_contre: i32,
325        vote_count_abstention: i32,
326        total_voting_power_pour: f64,
327        total_voting_power_contre: f64,
328        total_voting_power_abstention: f64,
329    ) -> Result<(), String> {
330        sqlx::query(
331            r#"
332            UPDATE resolutions
333            SET vote_count_pour = $2, vote_count_contre = $3, vote_count_abstention = $4,
334                total_voting_power_pour = $5, total_voting_power_contre = $6,
335                total_voting_power_abstention = $7
336            WHERE id = $1
337            "#,
338        )
339        .bind(resolution_id)
340        .bind(vote_count_pour)
341        .bind(vote_count_contre)
342        .bind(vote_count_abstention)
343        .bind(total_voting_power_pour)
344        .bind(total_voting_power_contre)
345        .bind(total_voting_power_abstention)
346        .execute(&self.pool)
347        .await
348        .map_err(|e| format!("Database error updating vote counts: {}", e))?;
349
350        Ok(())
351    }
352
353    async fn close_voting(
354        &self,
355        resolution_id: Uuid,
356        final_status: ResolutionStatus,
357    ) -> Result<(), String> {
358        let status_str = match final_status {
359            ResolutionStatus::Pending => "Pending",
360            ResolutionStatus::Adopted => "Adopted",
361            ResolutionStatus::Rejected => "Rejected",
362        };
363
364        sqlx::query(
365            r#"
366            UPDATE resolutions
367            SET status = $2, voted_at = CURRENT_TIMESTAMP
368            WHERE id = $1
369            "#,
370        )
371        .bind(resolution_id)
372        .bind(status_str)
373        .execute(&self.pool)
374        .await
375        .map_err(|e| format!("Database error closing voting: {}", e))?;
376
377        Ok(())
378    }
379
380    async fn get_meeting_vote_summary(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
381        // Same as find_by_meeting_id, but could be enhanced with additional stats
382        self.find_by_meeting_id(meeting_id).await
383    }
384}