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, 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 self.find_by_meeting_id(meeting_id).await
380 }
381}