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 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 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 self.find_by_meeting_id(meeting_id).await
383 }
384}