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 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}