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