1use crate::application::ports::age_request_repository::AgeRequestRepository;
2use crate::domain::entities::age_request::{AgeRequest, AgeRequestCosignatory, AgeRequestStatus};
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresAgeRequestRepository {
9 pool: DbPool,
10}
11
12impl PostgresAgeRequestRepository {
13 pub fn new(pool: DbPool) -> Self {
14 Self { pool }
15 }
16}
17
18fn row_to_age_request(row: &sqlx::postgres::PgRow) -> AgeRequest {
19 let status_str: String = row.get("status");
20 let status = AgeRequestStatus::from_db_string(&status_str).unwrap_or(AgeRequestStatus::Draft);
21
22 AgeRequest {
23 id: row.get("id"),
24 organization_id: row.get("organization_id"),
25 building_id: row.get("building_id"),
26 title: row.get("title"),
27 description: row.get("description"),
28 status,
29 created_by: row.get("created_by"),
30 cosignatories: Vec::new(), total_shares_pct: row.get::<f64, _>("total_shares_pct"),
32 threshold_pct: row.get::<f64, _>("threshold_pct"),
33 threshold_reached: row.get("threshold_reached"),
34 threshold_reached_at: row.get("threshold_reached_at"),
35 submitted_to_syndic_at: row.get("submitted_to_syndic_at"),
36 syndic_deadline_at: row.get("syndic_deadline_at"),
37 syndic_response_at: row.get("syndic_response_at"),
38 syndic_notes: row.get("syndic_notes"),
39 auto_convocation_triggered: row.get("auto_convocation_triggered"),
40 meeting_id: row.get("meeting_id"),
41 concertation_poll_id: row.get("concertation_poll_id"),
42 created_at: row.get("created_at"),
43 updated_at: row.get("updated_at"),
44 }
45}
46
47fn row_to_cosignatory(row: &sqlx::postgres::PgRow) -> AgeRequestCosignatory {
48 AgeRequestCosignatory {
49 id: row.get("id"),
50 age_request_id: row.get("age_request_id"),
51 owner_id: row.get("owner_id"),
52 shares_pct: row.get::<f64, _>("shares_pct"),
53 signed_at: row.get("signed_at"),
54 }
55}
56
57#[async_trait]
58impl AgeRequestRepository for PostgresAgeRequestRepository {
59 async fn create(&self, req: &AgeRequest) -> Result<AgeRequest, String> {
60 sqlx::query(
61 r#"
62 INSERT INTO age_requests (
63 id, organization_id, building_id, title, description,
64 status, created_by,
65 total_shares_pct, threshold_pct, threshold_reached, threshold_reached_at,
66 submitted_to_syndic_at, syndic_deadline_at, syndic_response_at, syndic_notes,
67 auto_convocation_triggered, meeting_id, concertation_poll_id,
68 created_at, updated_at
69 ) VALUES (
70 $1, $2, $3, $4, $5,
71 $6::age_request_status, $7,
72 $8, $9, $10, $11,
73 $12, $13, $14, $15,
74 $16, $17, $18,
75 $19, $20
76 )
77 "#,
78 )
79 .bind(req.id)
80 .bind(req.organization_id)
81 .bind(req.building_id)
82 .bind(&req.title)
83 .bind(&req.description)
84 .bind(req.status.to_db_str())
85 .bind(req.created_by)
86 .bind(req.total_shares_pct)
87 .bind(req.threshold_pct)
88 .bind(req.threshold_reached)
89 .bind(req.threshold_reached_at)
90 .bind(req.submitted_to_syndic_at)
91 .bind(req.syndic_deadline_at)
92 .bind(req.syndic_response_at)
93 .bind(&req.syndic_notes)
94 .bind(req.auto_convocation_triggered)
95 .bind(req.meeting_id)
96 .bind(req.concertation_poll_id)
97 .bind(req.created_at)
98 .bind(req.updated_at)
99 .execute(&self.pool)
100 .await
101 .map_err(|e| format!("Database error creating age_request: {}", e))?;
102
103 Ok(req.clone())
104 }
105
106 async fn find_by_id(&self, id: Uuid) -> Result<Option<AgeRequest>, String> {
107 let row = sqlx::query(
108 r#"
109 SELECT id, organization_id, building_id, title, description,
110 status::TEXT, created_by,
111 total_shares_pct::FLOAT8, threshold_pct::FLOAT8,
112 threshold_reached, threshold_reached_at,
113 submitted_to_syndic_at, syndic_deadline_at, syndic_response_at, syndic_notes,
114 auto_convocation_triggered, meeting_id, concertation_poll_id,
115 created_at, updated_at
116 FROM age_requests
117 WHERE id = $1
118 "#,
119 )
120 .bind(id)
121 .fetch_optional(&self.pool)
122 .await
123 .map_err(|e| format!("Database error finding age_request: {}", e))?;
124
125 let Some(row) = row else {
126 return Ok(None);
127 };
128
129 let mut req = row_to_age_request(&row);
130 req.cosignatories = self.find_cosignatories(id).await?;
131 Ok(Some(req))
132 }
133
134 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<AgeRequest>, String> {
135 let rows = sqlx::query(
136 r#"
137 SELECT id, organization_id, building_id, title, description,
138 status::TEXT, created_by,
139 total_shares_pct::FLOAT8, threshold_pct::FLOAT8,
140 threshold_reached, threshold_reached_at,
141 submitted_to_syndic_at, syndic_deadline_at, syndic_response_at, syndic_notes,
142 auto_convocation_triggered, meeting_id, concertation_poll_id,
143 created_at, updated_at
144 FROM age_requests
145 WHERE building_id = $1
146 ORDER BY created_at DESC
147 "#,
148 )
149 .bind(building_id)
150 .fetch_all(&self.pool)
151 .await
152 .map_err(|e| format!("Database error listing age_requests by building: {}", e))?;
153
154 let mut requests = Vec::new();
155 for row in &rows {
156 let mut req = row_to_age_request(row);
157 req.cosignatories = self.find_cosignatories(req.id).await?;
158 requests.push(req);
159 }
160 Ok(requests)
161 }
162
163 async fn find_by_organization(&self, organization_id: Uuid) -> Result<Vec<AgeRequest>, String> {
164 let rows = sqlx::query(
165 r#"
166 SELECT id, organization_id, building_id, title, description,
167 status::TEXT, created_by,
168 total_shares_pct::FLOAT8, threshold_pct::FLOAT8,
169 threshold_reached, threshold_reached_at,
170 submitted_to_syndic_at, syndic_deadline_at, syndic_response_at, syndic_notes,
171 auto_convocation_triggered, meeting_id, concertation_poll_id,
172 created_at, updated_at
173 FROM age_requests
174 WHERE organization_id = $1
175 ORDER BY created_at DESC
176 "#,
177 )
178 .bind(organization_id)
179 .fetch_all(&self.pool)
180 .await
181 .map_err(|e| format!("Database error listing age_requests by org: {}", e))?;
182
183 let mut requests = Vec::new();
184 for row in &rows {
185 let mut req = row_to_age_request(row);
186 req.cosignatories = self.find_cosignatories(req.id).await?;
187 requests.push(req);
188 }
189 Ok(requests)
190 }
191
192 async fn update(&self, req: &AgeRequest) -> Result<AgeRequest, String> {
193 sqlx::query(
194 r#"
195 UPDATE age_requests SET
196 title = $2,
197 description = $3,
198 status = $4::age_request_status,
199 total_shares_pct = $5,
200 threshold_pct = $6,
201 threshold_reached = $7,
202 threshold_reached_at = $8,
203 submitted_to_syndic_at = $9,
204 syndic_deadline_at = $10,
205 syndic_response_at = $11,
206 syndic_notes = $12,
207 auto_convocation_triggered = $13,
208 meeting_id = $14,
209 concertation_poll_id = $15,
210 updated_at = $16
211 WHERE id = $1
212 "#,
213 )
214 .bind(req.id)
215 .bind(&req.title)
216 .bind(&req.description)
217 .bind(req.status.to_db_str())
218 .bind(req.total_shares_pct)
219 .bind(req.threshold_pct)
220 .bind(req.threshold_reached)
221 .bind(req.threshold_reached_at)
222 .bind(req.submitted_to_syndic_at)
223 .bind(req.syndic_deadline_at)
224 .bind(req.syndic_response_at)
225 .bind(&req.syndic_notes)
226 .bind(req.auto_convocation_triggered)
227 .bind(req.meeting_id)
228 .bind(req.concertation_poll_id)
229 .bind(req.updated_at)
230 .execute(&self.pool)
231 .await
232 .map_err(|e| format!("Database error updating age_request: {}", e))?;
233
234 Ok(req.clone())
235 }
236
237 async fn delete(&self, id: Uuid) -> Result<bool, String> {
238 let result = sqlx::query("DELETE FROM age_requests WHERE id = $1")
239 .bind(id)
240 .execute(&self.pool)
241 .await
242 .map_err(|e| format!("Database error deleting age_request: {}", e))?;
243
244 Ok(result.rows_affected() > 0)
245 }
246
247 async fn add_cosignatory(&self, cosignatory: &AgeRequestCosignatory) -> Result<(), String> {
248 sqlx::query(
249 r#"
250 INSERT INTO age_request_cosignatories (id, age_request_id, owner_id, shares_pct, signed_at)
251 VALUES ($1, $2, $3, $4, $5)
252 ON CONFLICT (age_request_id, owner_id) DO NOTHING
253 "#,
254 )
255 .bind(cosignatory.id)
256 .bind(cosignatory.age_request_id)
257 .bind(cosignatory.owner_id)
258 .bind(cosignatory.shares_pct)
259 .bind(cosignatory.signed_at)
260 .execute(&self.pool)
261 .await
262 .map_err(|e| format!("Database error adding cosignatory: {}", e))?;
263
264 Ok(())
265 }
266
267 async fn remove_cosignatory(
268 &self,
269 age_request_id: Uuid,
270 owner_id: Uuid,
271 ) -> Result<bool, String> {
272 let result = sqlx::query(
273 "DELETE FROM age_request_cosignatories WHERE age_request_id = $1 AND owner_id = $2",
274 )
275 .bind(age_request_id)
276 .bind(owner_id)
277 .execute(&self.pool)
278 .await
279 .map_err(|e| format!("Database error removing cosignatory: {}", e))?;
280
281 Ok(result.rows_affected() > 0)
282 }
283
284 async fn find_cosignatories(
285 &self,
286 age_request_id: Uuid,
287 ) -> Result<Vec<AgeRequestCosignatory>, String> {
288 let rows = sqlx::query(
289 r#"
290 SELECT id, age_request_id, owner_id, shares_pct::FLOAT8, signed_at
291 FROM age_request_cosignatories
292 WHERE age_request_id = $1
293 ORDER BY signed_at ASC
294 "#,
295 )
296 .bind(age_request_id)
297 .fetch_all(&self.pool)
298 .await
299 .map_err(|e| format!("Database error loading cosignatories: {}", e))?;
300
301 Ok(rows.iter().map(row_to_cosignatory).collect())
302 }
303
304 async fn find_expired_deadlines(&self) -> Result<Vec<AgeRequest>, String> {
305 let rows = sqlx::query(
306 r#"
307 SELECT id, organization_id, building_id, title, description,
308 status::TEXT, created_by,
309 total_shares_pct::FLOAT8, threshold_pct::FLOAT8,
310 threshold_reached, threshold_reached_at,
311 submitted_to_syndic_at, syndic_deadline_at, syndic_response_at, syndic_notes,
312 auto_convocation_triggered, meeting_id, concertation_poll_id,
313 created_at, updated_at
314 FROM age_requests
315 WHERE status = 'submitted'
316 AND syndic_deadline_at IS NOT NULL
317 AND syndic_deadline_at <= NOW()
318 ORDER BY syndic_deadline_at ASC
319 "#,
320 )
321 .fetch_all(&self.pool)
322 .await
323 .map_err(|e| format!("Database error finding expired deadlines: {}", e))?;
324
325 let mut requests = Vec::new();
326 for row in &rows {
327 let mut req = row_to_age_request(row);
328 req.cosignatories = self.find_cosignatories(req.id).await?;
329 requests.push(req);
330 }
331 Ok(requests)
332 }
333}