1use crate::application::ports::ResolutionRepository;
2use crate::domain::entities::{
3 MajorityType, Resolution, ResolutionKind, ResolutionStatus, ResolutionType,
4};
5use crate::infrastructure::database::pool::DbPool;
6use async_trait::async_trait;
7use rust_decimal::Decimal;
8use sqlx::Row;
9use uuid::Uuid;
10
11pub struct PostgresResolutionRepository {
12 pool: DbPool,
13}
14
15impl PostgresResolutionRepository {
16 pub fn new(pool: DbPool) -> Self {
17 Self { pool }
18 }
19
20 fn parse_majority_type(s: &str) -> MajorityType {
22 match s {
23 "TwoThirds" => MajorityType::TwoThirds,
24 "FourFifths" => MajorityType::FourFifths,
25 "Unanimity" => MajorityType::Unanimity,
26 _ => MajorityType::Absolute,
28 }
29 }
30
31 fn majority_type_to_string(majority: &MajorityType) -> String {
33 match majority {
34 MajorityType::Absolute => "Absolute".to_string(),
35 MajorityType::TwoThirds => "TwoThirds".to_string(),
36 MajorityType::FourFifths => "FourFifths".to_string(),
37 MajorityType::Unanimity => "Unanimity".to_string(),
38 }
39 }
40
41 fn kind_to_string(kind: &ResolutionKind) -> &'static str {
43 match kind {
44 ResolutionKind::Standard => "standard",
45 ResolutionKind::EvaluationContractorsAuto => "evaluation_contractors_auto",
46 }
47 }
48
49 fn parse_kind(s: &str) -> ResolutionKind {
52 match s {
53 "evaluation_contractors_auto" => ResolutionKind::EvaluationContractorsAuto,
54 _ => ResolutionKind::Standard,
55 }
56 }
57}
58
59#[async_trait]
60impl ResolutionRepository for PostgresResolutionRepository {
61 async fn create(&self, resolution: &Resolution) -> Result<Resolution, String> {
62 let resolution_type_str = match resolution.resolution_type {
63 ResolutionType::Ordinary => "Ordinary",
64 ResolutionType::Extraordinary => "Extraordinary",
65 };
66
67 let status_str = match resolution.status {
68 ResolutionStatus::Pending => "Pending",
69 ResolutionStatus::Adopted => "Adopted",
70 ResolutionStatus::Rejected => "Rejected",
71 };
72
73 let majority_str = Self::majority_type_to_string(&resolution.majority_required);
74 let kind_str = Self::kind_to_string(&resolution.kind);
75
76 sqlx::query(
77 r#"
78 INSERT INTO resolutions (
79 id, meeting_id, title, description, resolution_type, majority_required,
80 vote_count_pour, vote_count_contre, vote_count_abstention,
81 total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
82 status, created_at, voted_at, agenda_item_index, prestataire_de_la_mission, kind
83 )
84 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)
85 "#,
86 )
87 .bind(resolution.id)
88 .bind(resolution.meeting_id)
89 .bind(&resolution.title)
90 .bind(&resolution.description)
91 .bind(resolution_type_str)
92 .bind(majority_str)
93 .bind(resolution.vote_count_pour)
94 .bind(resolution.vote_count_contre)
95 .bind(resolution.vote_count_abstention)
96 .bind(resolution.total_voting_power_pour)
97 .bind(resolution.total_voting_power_contre)
98 .bind(resolution.total_voting_power_abstention)
99 .bind(status_str)
100 .bind(resolution.created_at)
101 .bind(resolution.voted_at)
102 .bind(resolution.agenda_item_index.map(|i| i as i32))
105 .bind(resolution.prestataire_de_la_mission)
106 .bind(kind_str)
107 .execute(&self.pool)
108 .await
109 .map_err(|e| format!("Database error creating resolution: {}", e))?;
110
111 Ok(resolution.clone())
112 }
113
114 async fn find_by_id(&self, id: Uuid) -> Result<Option<Resolution>, String> {
115 let row = sqlx::query(
116 r#"
117 SELECT id, meeting_id, title, description, resolution_type, majority_required,
118 vote_count_pour, vote_count_contre, vote_count_abstention,
119 total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
120 status, created_at, voted_at, agenda_item_index, prestataire_de_la_mission, kind
121 FROM resolutions
122 WHERE id = $1
123 "#,
124 )
125 .bind(id)
126 .fetch_optional(&self.pool)
127 .await
128 .map_err(|e| format!("Database error finding resolution: {}", e))?;
129
130 Ok(row.map(|row| {
131 let resolution_type_str: String = row.get("resolution_type");
132 let resolution_type = match resolution_type_str.as_str() {
133 "Extraordinary" => ResolutionType::Extraordinary,
134 _ => ResolutionType::Ordinary,
135 };
136
137 let status_str: String = row.get("status");
138 let status = match status_str.as_str() {
139 "Adopted" => ResolutionStatus::Adopted,
140 "Rejected" => ResolutionStatus::Rejected,
141 _ => ResolutionStatus::Pending,
142 };
143
144 let majority_str: String = row.get("majority_required");
145 let majority_required = Self::parse_majority_type(&majority_str);
146 let kind_str: String = row.get("kind");
147
148 Resolution {
149 id: row.get("id"),
150 meeting_id: row.get("meeting_id"),
151 title: row.get("title"),
152 description: row.get("description"),
153 resolution_type,
154 majority_required,
155 vote_count_pour: row.get("vote_count_pour"),
156 vote_count_contre: row.get("vote_count_contre"),
157 vote_count_abstention: row.get("vote_count_abstention"),
158 total_voting_power_pour: row.get("total_voting_power_pour"),
159 total_voting_power_contre: row.get("total_voting_power_contre"),
160 total_voting_power_abstention: row.get("total_voting_power_abstention"),
161 status,
162 created_at: row.get("created_at"),
163 voted_at: row.get("voted_at"),
164 agenda_item_index: row
165 .get::<Option<i32>, _>("agenda_item_index")
166 .map(|i| i as usize),
167 prestataire_de_la_mission: row.get("prestataire_de_la_mission"),
168 kind: Self::parse_kind(&kind_str),
169 }
170 }))
171 }
172
173 async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
174 let rows = sqlx::query(
175 r#"
176 SELECT id, meeting_id, title, description, resolution_type, majority_required,
177 vote_count_pour, vote_count_contre, vote_count_abstention,
178 total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
179 status, created_at, voted_at, agenda_item_index, prestataire_de_la_mission, kind
180 FROM resolutions
181 WHERE meeting_id = $1
182 ORDER BY created_at ASC
183 "#,
184 )
185 .bind(meeting_id)
186 .fetch_all(&self.pool)
187 .await
188 .map_err(|e| format!("Database error finding resolutions by meeting: {}", e))?;
189
190 Ok(rows
191 .into_iter()
192 .map(|row| {
193 let resolution_type_str: String = row.get("resolution_type");
194 let resolution_type = match resolution_type_str.as_str() {
195 "Extraordinary" => ResolutionType::Extraordinary,
196 _ => ResolutionType::Ordinary,
197 };
198
199 let status_str: String = row.get("status");
200 let status = match status_str.as_str() {
201 "Adopted" => ResolutionStatus::Adopted,
202 "Rejected" => ResolutionStatus::Rejected,
203 _ => ResolutionStatus::Pending,
204 };
205
206 let majority_str: String = row.get("majority_required");
207 let majority_required = Self::parse_majority_type(&majority_str);
208 let kind_str: String = row.get("kind");
209
210 Resolution {
211 id: row.get("id"),
212 meeting_id: row.get("meeting_id"),
213 title: row.get("title"),
214 description: row.get("description"),
215 resolution_type,
216 majority_required,
217 vote_count_pour: row.get("vote_count_pour"),
218 vote_count_contre: row.get("vote_count_contre"),
219 vote_count_abstention: row.get("vote_count_abstention"),
220 total_voting_power_pour: row.get("total_voting_power_pour"),
221 total_voting_power_contre: row.get("total_voting_power_contre"),
222 total_voting_power_abstention: row.get("total_voting_power_abstention"),
223 status,
224 created_at: row.get("created_at"),
225 voted_at: row.get("voted_at"),
226 agenda_item_index: row
227 .get::<Option<i32>, _>("agenda_item_index")
228 .map(|i| i as usize),
229 prestataire_de_la_mission: row.get("prestataire_de_la_mission"),
230 kind: Self::parse_kind(&kind_str),
231 }
232 })
233 .collect())
234 }
235
236 async fn find_by_status(&self, status: ResolutionStatus) -> Result<Vec<Resolution>, String> {
237 let status_str = match status {
238 ResolutionStatus::Pending => "Pending",
239 ResolutionStatus::Adopted => "Adopted",
240 ResolutionStatus::Rejected => "Rejected",
241 };
242
243 let rows = sqlx::query(
244 r#"
245 SELECT id, meeting_id, title, description, resolution_type, majority_required,
246 vote_count_pour, vote_count_contre, vote_count_abstention,
247 total_voting_power_pour, total_voting_power_contre, total_voting_power_abstention,
248 status, created_at, voted_at, agenda_item_index, prestataire_de_la_mission, kind
249 FROM resolutions
250 WHERE status = $1
251 ORDER BY created_at DESC
252 "#,
253 )
254 .bind(status_str)
255 .fetch_all(&self.pool)
256 .await
257 .map_err(|e| format!("Database error finding resolutions by status: {}", e))?;
258
259 Ok(rows
260 .into_iter()
261 .map(|row| {
262 let resolution_type_str: String = row.get("resolution_type");
263 let resolution_type = match resolution_type_str.as_str() {
264 "Extraordinary" => ResolutionType::Extraordinary,
265 _ => ResolutionType::Ordinary,
266 };
267
268 let majority_str: String = row.get("majority_required");
269 let majority_required = Self::parse_majority_type(&majority_str);
270 let kind_str: String = row.get("kind");
271
272 Resolution {
273 id: row.get("id"),
274 meeting_id: row.get("meeting_id"),
275 title: row.get("title"),
276 description: row.get("description"),
277 resolution_type,
278 majority_required,
279 vote_count_pour: row.get("vote_count_pour"),
280 vote_count_contre: row.get("vote_count_contre"),
281 vote_count_abstention: row.get("vote_count_abstention"),
282 total_voting_power_pour: row.get("total_voting_power_pour"),
283 total_voting_power_contre: row.get("total_voting_power_contre"),
284 total_voting_power_abstention: row.get("total_voting_power_abstention"),
285 status: status.clone(),
286 created_at: row.get("created_at"),
287 voted_at: row.get("voted_at"),
288 agenda_item_index: row
289 .get::<Option<i32>, _>("agenda_item_index")
290 .map(|i| i as usize),
291 prestataire_de_la_mission: row.get("prestataire_de_la_mission"),
292 kind: Self::parse_kind(&kind_str),
293 }
294 })
295 .collect())
296 }
297
298 async fn update(&self, resolution: &Resolution) -> Result<Resolution, String> {
299 let resolution_type_str = match resolution.resolution_type {
300 ResolutionType::Ordinary => "Ordinary",
301 ResolutionType::Extraordinary => "Extraordinary",
302 };
303
304 let status_str = match resolution.status {
305 ResolutionStatus::Pending => "Pending",
306 ResolutionStatus::Adopted => "Adopted",
307 ResolutionStatus::Rejected => "Rejected",
308 };
309
310 let majority_str = Self::majority_type_to_string(&resolution.majority_required);
311
312 sqlx::query(
313 r#"
314 UPDATE resolutions
315 SET meeting_id = $2, title = $3, description = $4, resolution_type = $5,
316 majority_required = $6, vote_count_pour = $7, vote_count_contre = $8,
317 vote_count_abstention = $9, total_voting_power_pour = $10,
318 total_voting_power_contre = $11, total_voting_power_abstention = $12,
319 status = $13, voted_at = $14
320 WHERE id = $1
321 "#,
322 )
323 .bind(resolution.id)
324 .bind(resolution.meeting_id)
325 .bind(&resolution.title)
326 .bind(&resolution.description)
327 .bind(resolution_type_str)
328 .bind(majority_str)
329 .bind(resolution.vote_count_pour)
330 .bind(resolution.vote_count_contre)
331 .bind(resolution.vote_count_abstention)
332 .bind(resolution.total_voting_power_pour)
333 .bind(resolution.total_voting_power_contre)
334 .bind(resolution.total_voting_power_abstention)
335 .bind(status_str)
336 .bind(resolution.voted_at)
337 .execute(&self.pool)
338 .await
339 .map_err(|e| format!("Database error updating resolution: {}", e))?;
340
341 Ok(resolution.clone())
342 }
343
344 async fn delete(&self, id: Uuid) -> Result<bool, String> {
345 let result = sqlx::query(
346 r#"
347 DELETE FROM resolutions WHERE id = $1
348 "#,
349 )
350 .bind(id)
351 .execute(&self.pool)
352 .await
353 .map_err(|e| format!("Database error deleting resolution: {}", e))?;
354
355 Ok(result.rows_affected() > 0)
356 }
357
358 async fn update_vote_counts(
359 &self,
360 resolution_id: Uuid,
361 vote_count_pour: i32,
362 vote_count_contre: i32,
363 vote_count_abstention: i32,
364 total_voting_power_pour: Decimal,
365 total_voting_power_contre: Decimal,
366 total_voting_power_abstention: Decimal,
367 ) -> Result<(), String> {
368 sqlx::query(
369 r#"
370 UPDATE resolutions
371 SET vote_count_pour = $2, vote_count_contre = $3, vote_count_abstention = $4,
372 total_voting_power_pour = $5, total_voting_power_contre = $6,
373 total_voting_power_abstention = $7
374 WHERE id = $1
375 "#,
376 )
377 .bind(resolution_id)
378 .bind(vote_count_pour)
379 .bind(vote_count_contre)
380 .bind(vote_count_abstention)
381 .bind(total_voting_power_pour)
382 .bind(total_voting_power_contre)
383 .bind(total_voting_power_abstention)
384 .execute(&self.pool)
385 .await
386 .map_err(|e| format!("Database error updating vote counts: {}", e))?;
387
388 Ok(())
389 }
390
391 async fn close_voting(
392 &self,
393 resolution_id: Uuid,
394 final_status: ResolutionStatus,
395 voix_plafonnees: Option<serde_json::Value>,
396 ) -> Result<(), String> {
397 let status_str = match final_status {
398 ResolutionStatus::Pending => "Pending",
399 ResolutionStatus::Adopted => "Adopted",
400 ResolutionStatus::Rejected => "Rejected",
401 };
402
403 sqlx::query(
404 r#"
405 UPDATE resolutions
406 SET status = $2, voted_at = CURRENT_TIMESTAMP, voix_plafonnees = $3
407 WHERE id = $1
408 "#,
409 )
410 .bind(resolution_id)
411 .bind(status_str)
412 .bind(voix_plafonnees)
413 .execute(&self.pool)
414 .await
415 .map_err(|e| format!("Database error closing voting: {}", e))?;
416
417 Ok(())
418 }
419
420 async fn get_meeting_vote_summary(&self, meeting_id: Uuid) -> Result<Vec<Resolution>, String> {
421 self.find_by_meeting_id(meeting_id).await
423 }
424}