Skip to main content

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