koprogo_api/infrastructure/database/repositories/
board_decision_repository_impl.rs

1use crate::application::ports::BoardDecisionRepository;
2use crate::domain::entities::{BoardDecision, DecisionStatus};
3use crate::infrastructure::database::pool::DbPool;
4use async_trait::async_trait;
5use sqlx::Row;
6use uuid::Uuid;
7
8pub struct PostgresBoardDecisionRepository {
9    pool: DbPool,
10}
11
12impl PostgresBoardDecisionRepository {
13    pub fn new(pool: DbPool) -> Self {
14        Self { pool }
15    }
16}
17
18#[async_trait]
19impl BoardDecisionRepository for PostgresBoardDecisionRepository {
20    async fn create(&self, decision: &BoardDecision) -> Result<BoardDecision, String> {
21        let status_str = decision.status.to_string();
22
23        // Get organization_id from building
24        let organization_id: Uuid =
25            sqlx::query_scalar("SELECT organization_id FROM buildings WHERE id = $1")
26                .bind(decision.building_id)
27                .fetch_one(&self.pool)
28                .await
29                .map_err(|e| format!("Failed to get building organization: {}", e))?;
30
31        sqlx::query(
32            r#"
33            INSERT INTO board_decisions (id, building_id, meeting_id, organization_id, subject, decision_text, deadline, status, completed_at, notes, created_at, updated_at)
34            VALUES ($1, $2, $3, $4, $5, $6, $7, $8::decision_status, $9, $10, $11, $12)
35            "#,
36        )
37        .bind(decision.id)
38        .bind(decision.building_id)
39        .bind(decision.meeting_id)
40        .bind(organization_id)
41        .bind(&decision.subject)
42        .bind(&decision.decision_text)
43        .bind(decision.deadline)
44        .bind(&status_str)
45        .bind(decision.completed_at)
46        .bind(&decision.notes)
47        .bind(decision.created_at)
48        .bind(decision.updated_at)
49        .execute(&self.pool)
50        .await
51        .map_err(|e| format!("Database error: {}", e))?;
52
53        Ok(decision.clone())
54    }
55
56    async fn find_by_id(&self, id: Uuid) -> Result<Option<BoardDecision>, String> {
57        let row = sqlx::query(
58            r#"
59            SELECT id, building_id, meeting_id, subject, decision_text, deadline, status::TEXT as status, completed_at, notes, created_at, updated_at
60            FROM board_decisions
61            WHERE id = $1
62            "#,
63        )
64        .bind(id)
65        .fetch_optional(&self.pool)
66        .await
67        .map_err(|e| format!("Database error: {}", e))?;
68
69        Ok(row.map(|row| {
70            let status_str: String = row.get("status");
71            let status = status_str
72                .parse::<DecisionStatus>()
73                .unwrap_or(DecisionStatus::Pending);
74
75            BoardDecision {
76                id: row.get("id"),
77                building_id: row.get("building_id"),
78                meeting_id: row.get("meeting_id"),
79                subject: row.get("subject"),
80                decision_text: row.get("decision_text"),
81                deadline: row.get("deadline"),
82                status,
83                completed_at: row.get("completed_at"),
84                notes: row.get("notes"),
85                created_at: row.get("created_at"),
86                updated_at: row.get("updated_at"),
87            }
88        }))
89    }
90
91    async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<BoardDecision>, String> {
92        let rows = sqlx::query(
93            r#"
94            SELECT id, building_id, meeting_id, subject, decision_text, deadline, status::TEXT as status, completed_at, notes, created_at, updated_at
95            FROM board_decisions
96            WHERE building_id = $1
97            ORDER BY created_at DESC
98            "#,
99        )
100        .bind(building_id)
101        .fetch_all(&self.pool)
102        .await
103        .map_err(|e| format!("Database error: {}", e))?;
104
105        Ok(rows
106            .into_iter()
107            .map(|row| {
108                let status_str: String = row.get("status");
109                let status = status_str
110                    .parse::<DecisionStatus>()
111                    .unwrap_or(DecisionStatus::Pending);
112
113                BoardDecision {
114                    id: row.get("id"),
115                    building_id: row.get("building_id"),
116                    meeting_id: row.get("meeting_id"),
117                    subject: row.get("subject"),
118                    decision_text: row.get("decision_text"),
119                    deadline: row.get("deadline"),
120                    status,
121                    completed_at: row.get("completed_at"),
122                    notes: row.get("notes"),
123                    created_at: row.get("created_at"),
124                    updated_at: row.get("updated_at"),
125                }
126            })
127            .collect())
128    }
129
130    async fn find_by_meeting(&self, meeting_id: Uuid) -> Result<Vec<BoardDecision>, String> {
131        let rows = sqlx::query(
132            r#"
133            SELECT id, building_id, meeting_id, subject, decision_text, deadline, status::TEXT as status, completed_at, notes, created_at, updated_at
134            FROM board_decisions
135            WHERE meeting_id = $1
136            ORDER BY created_at DESC
137            "#,
138        )
139        .bind(meeting_id)
140        .fetch_all(&self.pool)
141        .await
142        .map_err(|e| format!("Database error: {}", e))?;
143
144        Ok(rows
145            .into_iter()
146            .map(|row| {
147                let status_str: String = row.get("status");
148                let status = status_str
149                    .parse::<DecisionStatus>()
150                    .unwrap_or(DecisionStatus::Pending);
151
152                BoardDecision {
153                    id: row.get("id"),
154                    building_id: row.get("building_id"),
155                    meeting_id: row.get("meeting_id"),
156                    subject: row.get("subject"),
157                    decision_text: row.get("decision_text"),
158                    deadline: row.get("deadline"),
159                    status,
160                    completed_at: row.get("completed_at"),
161                    notes: row.get("notes"),
162                    created_at: row.get("created_at"),
163                    updated_at: row.get("updated_at"),
164                }
165            })
166            .collect())
167    }
168
169    async fn find_by_status(
170        &self,
171        building_id: Uuid,
172        status: DecisionStatus,
173    ) -> Result<Vec<BoardDecision>, String> {
174        let status_str = status.to_string();
175
176        let rows = sqlx::query(
177            r#"
178            SELECT id, building_id, meeting_id, subject, decision_text, deadline, status::TEXT as status, completed_at, notes, created_at, updated_at
179            FROM board_decisions
180            WHERE building_id = $1 AND status = $2::decision_status
181            ORDER BY created_at DESC
182            "#,
183        )
184        .bind(building_id)
185        .bind(&status_str)
186        .fetch_all(&self.pool)
187        .await
188        .map_err(|e| format!("Database error: {}", e))?;
189
190        Ok(rows
191            .into_iter()
192            .map(|row| {
193                let status_str: String = row.get("status");
194                let status = status_str
195                    .parse::<DecisionStatus>()
196                    .unwrap_or(DecisionStatus::Pending);
197
198                BoardDecision {
199                    id: row.get("id"),
200                    building_id: row.get("building_id"),
201                    meeting_id: row.get("meeting_id"),
202                    subject: row.get("subject"),
203                    decision_text: row.get("decision_text"),
204                    deadline: row.get("deadline"),
205                    status,
206                    completed_at: row.get("completed_at"),
207                    notes: row.get("notes"),
208                    created_at: row.get("created_at"),
209                    updated_at: row.get("updated_at"),
210                }
211            })
212            .collect())
213    }
214
215    async fn find_overdue(&self, building_id: Uuid) -> Result<Vec<BoardDecision>, String> {
216        let rows = sqlx::query(
217            r#"
218            SELECT id, building_id, meeting_id, subject, decision_text, deadline, status::TEXT as status, completed_at, notes, created_at, updated_at
219            FROM board_decisions
220            WHERE building_id = $1
221              AND deadline IS NOT NULL
222              AND deadline < CURRENT_TIMESTAMP
223              AND status NOT IN ('completed', 'cancelled')
224            ORDER BY deadline ASC
225            "#,
226        )
227        .bind(building_id)
228        .fetch_all(&self.pool)
229        .await
230        .map_err(|e| format!("Database error: {}", e))?;
231
232        Ok(rows
233            .into_iter()
234            .map(|row| {
235                let status_str: String = row.get("status");
236                let status = status_str
237                    .parse::<DecisionStatus>()
238                    .unwrap_or(DecisionStatus::Pending);
239
240                BoardDecision {
241                    id: row.get("id"),
242                    building_id: row.get("building_id"),
243                    meeting_id: row.get("meeting_id"),
244                    subject: row.get("subject"),
245                    decision_text: row.get("decision_text"),
246                    deadline: row.get("deadline"),
247                    status,
248                    completed_at: row.get("completed_at"),
249                    notes: row.get("notes"),
250                    created_at: row.get("created_at"),
251                    updated_at: row.get("updated_at"),
252                }
253            })
254            .collect())
255    }
256
257    async fn find_deadline_approaching(
258        &self,
259        building_id: Uuid,
260        days_threshold: i32,
261    ) -> Result<Vec<BoardDecision>, String> {
262        let rows = sqlx::query(
263            r#"
264            SELECT id, building_id, meeting_id, subject, decision_text, deadline, status::TEXT as status, completed_at, notes, created_at, updated_at
265            FROM board_decisions
266            WHERE building_id = $1
267              AND deadline IS NOT NULL
268              AND deadline > CURRENT_TIMESTAMP
269              AND deadline <= (CURRENT_TIMESTAMP + INTERVAL '1 day' * $2)
270              AND status NOT IN ('completed', 'cancelled')
271            ORDER BY deadline ASC
272            "#,
273        )
274        .bind(building_id)
275        .bind(days_threshold)
276        .fetch_all(&self.pool)
277        .await
278        .map_err(|e| format!("Database error: {}", e))?;
279
280        Ok(rows
281            .into_iter()
282            .map(|row| {
283                let status_str: String = row.get("status");
284                let status = status_str
285                    .parse::<DecisionStatus>()
286                    .unwrap_or(DecisionStatus::Pending);
287
288                BoardDecision {
289                    id: row.get("id"),
290                    building_id: row.get("building_id"),
291                    meeting_id: row.get("meeting_id"),
292                    subject: row.get("subject"),
293                    decision_text: row.get("decision_text"),
294                    deadline: row.get("deadline"),
295                    status,
296                    completed_at: row.get("completed_at"),
297                    notes: row.get("notes"),
298                    created_at: row.get("created_at"),
299                    updated_at: row.get("updated_at"),
300                }
301            })
302            .collect())
303    }
304
305    async fn update(&self, decision: &BoardDecision) -> Result<BoardDecision, String> {
306        let status_str = decision.status.to_string();
307
308        sqlx::query(
309            r#"
310            UPDATE board_decisions
311            SET subject = $1,
312                decision_text = $2,
313                deadline = $3,
314                status = $4::decision_status,
315                completed_at = $5,
316                notes = $6,
317                updated_at = $7
318            WHERE id = $8
319            "#,
320        )
321        .bind(&decision.subject)
322        .bind(&decision.decision_text)
323        .bind(decision.deadline)
324        .bind(&status_str)
325        .bind(decision.completed_at)
326        .bind(&decision.notes)
327        .bind(decision.updated_at)
328        .bind(decision.id)
329        .execute(&self.pool)
330        .await
331        .map_err(|e| format!("Database error: {}", e))?;
332
333        Ok(decision.clone())
334    }
335
336    async fn delete(&self, id: Uuid) -> Result<bool, String> {
337        let result = sqlx::query(
338            r#"
339            DELETE FROM board_decisions
340            WHERE id = $1
341            "#,
342        )
343        .bind(id)
344        .execute(&self.pool)
345        .await
346        .map_err(|e| format!("Database error: {}", e))?;
347
348        Ok(result.rows_affected() > 0)
349    }
350
351    async fn count_by_status(
352        &self,
353        building_id: Uuid,
354        status: DecisionStatus,
355    ) -> Result<i64, String> {
356        let status_str = status.to_string();
357
358        let count: i64 = sqlx::query_scalar(
359            r#"
360            SELECT COUNT(*)
361            FROM board_decisions
362            WHERE building_id = $1 AND status = $2::decision_status
363            "#,
364        )
365        .bind(building_id)
366        .bind(&status_str)
367        .fetch_one(&self.pool)
368        .await
369        .map_err(|e| format!("Database error: {}", e))?;
370
371        Ok(count)
372    }
373
374    async fn count_overdue(&self, building_id: Uuid) -> Result<i64, String> {
375        let count: i64 = sqlx::query_scalar(
376            r#"
377            SELECT COUNT(*)
378            FROM board_decisions
379            WHERE building_id = $1
380              AND deadline IS NOT NULL
381              AND deadline < CURRENT_TIMESTAMP
382              AND status NOT IN ('completed', 'cancelled')
383            "#,
384        )
385        .bind(building_id)
386        .fetch_one(&self.pool)
387        .await
388        .map_err(|e| format!("Database error: {}", e))?;
389
390        Ok(count)
391    }
392}