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        match s {
20            "TwoThirds" => MajorityType::TwoThirds,
21            "FourFifths" => MajorityType::FourFifths,
22            "Unanimity" => MajorityType::Unanimity,
23            // "Absolute" and any legacy "Simple" values map to Absolute
24            _ => MajorityType::Absolute,
25        }
26    }
27
28    /// Convert MajorityType to database string format
29    fn majority_type_to_string(majority: &MajorityType) -> String {
30        match majority {
31            MajorityType::Absolute => "Absolute".to_string(),
32            MajorityType::TwoThirds => "TwoThirds".to_string(),
33            MajorityType::FourFifths => "FourFifths".to_string(),
34            MajorityType::Unanimity => "Unanimity".to_string(),
35        }
36    }
37}
38
39#[async_trait]
40impl ResolutionRepository for PostgresResolutionRepository {
41    async fn create(&self, resolution: &Resolution) -> Result<Resolution, String> {
42        let resolution_type_str = match resolution.resolution_type {
43            ResolutionType::Ordinary => "Ordinary",
44            ResolutionType::Extraordinary => "Extraordinary",
45        };
46
47        let status_str = match resolution.status {
48            ResolutionStatus::Pending => "Pending",
49            ResolutionStatus::Adopted => "Adopted",
50            ResolutionStatus::Rejected => "Rejected",
51        };
52
53        let majority_str = Self::majority_type_to_string(&resolution.majority_required);
54
55        sqlx::query(
56            r#"
57            INSERT INTO resolutions (
58                id, meeting_id, title, description, resolution_type, majority_required,
59                vote_count_pour, vote_count_contre, vote_count_abstention,
60                total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
61                status, created_at, voted_at
62            )
63            VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
64            "#,
65        )
66        .bind(resolution.id)
67        .bind(resolution.meeting_id)
68        .bind(&resolution.title)
69        .bind(&resolution.description)
70        .bind(resolution_type_str)
71        .bind(majority_str)
72        .bind(resolution.vote_count_pour)
73        .bind(resolution.vote_count_contre)
74        .bind(resolution.vote_count_abstention)
75        .bind(resolution.total_voting_power_pour)
76        .bind(resolution.total_voting_power_contre)
77        .bind(resolution.total_voting_power_abstention)
78        .bind(status_str)
79        .bind(resolution.created_at)
80        .bind(resolution.voted_at)
81        .execute(&self.pool)
82        .await
83        .map_err(|e| format!("Database error creating resolution: {}", e))?;
84
85        Ok(resolution.clone())
86    }
87
88    async fn find_by_id(&self, id: Uuid) -> Result<Option<Resolution>, String> {
89        let row = sqlx::query(
90            r#"
91            SELECT id, meeting_id, title, description, resolution_type, majority_required,
92                   vote_count_pour, vote_count_contre, vote_count_abstention,
93                   total_voting_power_pour::FLOAT8, total_voting_power_contre::FLOAT8, total_voting_power_abstention::FLOAT8,
94                   status, created_at, voted_at
95            FROM resolutions
96            WHERE id = $1
97            "#,
98        )
99        .bind(id)
100        .fetch_optional(&self.pool)
101        .await
102        .map_err(|e| format!("Database error finding resolution: {}", e))?;
103
104        Ok(row.map(|row| {
105            let resolution_type_str: String = row.get("resolution_type");
106            let resolution_type = match resolution_type_str.as_str() {
107                "Extraordinary" => ResolutionType::Extraordinary,
108                _ => ResolutionType::Ordinary,
109            };
110
111            let status_str: String = row.get("status");
112            let status = match status_str.as_str() {
113                "Adopted" => ResolutionStatus::Adopted,
114                "Rejected" => ResolutionStatus::Rejected,
115                _ => ResolutionStatus::Pending,
116            };
117
118            let majority_str: String = row.get("majority_required");
119            let majority_required = Self::parse_majority_type(&majority_str);
120
121            Resolution {
122                id: row.get("id"),
123                meeting_id: row.get("meeting_id"),
124                title: row.get("title"),
125                description: row.get("description"),
126                resolution_type,
127                majority_required,
128                vote_count_pour: row.get("vote_count_pour"),
129                vote_count_contre: row.get("vote_count_contre"),
130                vote_count_abstention: row.get("vote_count_abstention"),
131                total_voting_power_pour: row.get("total_voting_power_pour"),
132                total_voting_power_contre: row.get("total_voting_power_contre"),
133                total_voting_power_abstention: row.get("total_voting_power_abstention"),
134                status,
135                created_at: row.get("created_at"),
136                voted_at: row.get("voted_at"),
137                agenda_item_index: None,
138            }
139        }))
140    }
141
142    async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
143        let rows = sqlx::query(
144            r#"
145            SELECT id, meeting_id, title, description, resolution_type, majority_required,
146                   vote_count_pour, vote_count_contre, vote_count_abstention,
147                   total_voting_power_pour::FLOAT8, total_voting_power_contre::FLOAT8, total_voting_power_abstention::FLOAT8,
148                   status, created_at, voted_at
149            FROM resolutions
150            WHERE meeting_id = $1
151            ORDER BY created_at ASC
152            "#,
153        )
154        .bind(meeting_id)
155        .fetch_all(&self.pool)
156        .await
157        .map_err(|e| format!("Database error finding resolutions by meeting: {}", e))?;
158
159        Ok(rows
160            .into_iter()
161            .map(|row| {
162                let resolution_type_str: String = row.get("resolution_type");
163                let resolution_type = match resolution_type_str.as_str() {
164                    "Extraordinary" => ResolutionType::Extraordinary,
165                    _ => ResolutionType::Ordinary,
166                };
167
168                let status_str: String = row.get("status");
169                let status = match status_str.as_str() {
170                    "Adopted" => ResolutionStatus::Adopted,
171                    "Rejected" => ResolutionStatus::Rejected,
172                    _ => ResolutionStatus::Pending,
173                };
174
175                let majority_str: String = row.get("majority_required");
176                let majority_required = Self::parse_majority_type(&majority_str);
177
178                Resolution {
179                    id: row.get("id"),
180                    meeting_id: row.get("meeting_id"),
181                    title: row.get("title"),
182                    description: row.get("description"),
183                    resolution_type,
184                    majority_required,
185                    vote_count_pour: row.get("vote_count_pour"),
186                    vote_count_contre: row.get("vote_count_contre"),
187                    vote_count_abstention: row.get("vote_count_abstention"),
188                    total_voting_power_pour: row.get("total_voting_power_pour"),
189                    total_voting_power_contre: row.get("total_voting_power_contre"),
190                    total_voting_power_abstention: row.get("total_voting_power_abstention"),
191                    status,
192                    created_at: row.get("created_at"),
193                    voted_at: row.get("voted_at"),
194                    agenda_item_index: None,
195                }
196            })
197            .collect())
198    }
199
200    async fn find_by_status(&self, status: ResolutionStatus) -> Result<Vec<Resolution>, String> {
201        let status_str = match status {
202            ResolutionStatus::Pending => "Pending",
203            ResolutionStatus::Adopted => "Adopted",
204            ResolutionStatus::Rejected => "Rejected",
205        };
206
207        let rows = sqlx::query(
208            r#"
209            SELECT id, meeting_id, title, description, resolution_type, majority_required,
210                   vote_count_pour, vote_count_contre, vote_count_abstention,
211                   total_voting_power_pour::FLOAT8, total_voting_power_contre::FLOAT8, total_voting_power_abstention::FLOAT8,
212                   status, created_at, voted_at
213            FROM resolutions
214            WHERE status = $1
215            ORDER BY created_at DESC
216            "#,
217        )
218        .bind(status_str)
219        .fetch_all(&self.pool)
220        .await
221        .map_err(|e| format!("Database error finding resolutions by status: {}", e))?;
222
223        Ok(rows
224            .into_iter()
225            .map(|row| {
226                let resolution_type_str: String = row.get("resolution_type");
227                let resolution_type = match resolution_type_str.as_str() {
228                    "Extraordinary" => ResolutionType::Extraordinary,
229                    _ => ResolutionType::Ordinary,
230                };
231
232                let majority_str: String = row.get("majority_required");
233                let majority_required = Self::parse_majority_type(&majority_str);
234
235                Resolution {
236                    id: row.get("id"),
237                    meeting_id: row.get("meeting_id"),
238                    title: row.get("title"),
239                    description: row.get("description"),
240                    resolution_type,
241                    majority_required,
242                    vote_count_pour: row.get("vote_count_pour"),
243                    vote_count_contre: row.get("vote_count_contre"),
244                    vote_count_abstention: row.get("vote_count_abstention"),
245                    total_voting_power_pour: row.get("total_voting_power_pour"),
246                    total_voting_power_contre: row.get("total_voting_power_contre"),
247                    total_voting_power_abstention: row.get("total_voting_power_abstention"),
248                    status: status.clone(),
249                    created_at: row.get("created_at"),
250                    voted_at: row.get("voted_at"),
251                    agenda_item_index: None,
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}