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 match s {
20 "TwoThirds" => MajorityType::TwoThirds,
21 "FourFifths" => MajorityType::FourFifths,
22 "Unanimity" => MajorityType::Unanimity,
23 _ => MajorityType::Absolute,
25 }
26 }
27
28 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 self.find_by_meeting_id(meeting_id).await
380 }
381}