Skip to main content

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