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