koprogo_api/infrastructure/database/repositories/
age_request_repository_impl.rs

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(), // Chargé séparément
31        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}