1use crate::application::ports::ConvocationRepository;
2use crate::domain::entities::{Convocation, ConvocationStatus, ConvocationType};
3use async_trait::async_trait;
4use chrono::{DateTime, Duration, Utc};
5use sqlx::PgPool;
6use uuid::Uuid;
7
8pub struct PostgresConvocationRepository {
10 pool: PgPool,
11}
12
13impl PostgresConvocationRepository {
14 pub fn new(pool: PgPool) -> Self {
15 Self { pool }
16 }
17
18 fn convocation_type_to_db(meeting_type: &ConvocationType) -> &'static str {
20 meeting_type.to_db_string()
21 }
22
23 fn convocation_type_from_db(s: &str) -> Result<ConvocationType, String> {
25 ConvocationType::from_db_string(s)
26 }
27
28 fn status_to_db(status: &ConvocationStatus) -> &'static str {
30 status.to_db_string()
31 }
32
33 fn status_from_db(s: &str) -> Result<ConvocationStatus, String> {
35 ConvocationStatus::from_db_string(s)
36 }
37}
38
39#[async_trait]
40impl ConvocationRepository for PostgresConvocationRepository {
41 async fn create(&self, convocation: &Convocation) -> Result<Convocation, String> {
42 let meeting_type_str = Self::convocation_type_to_db(&convocation.meeting_type);
43 let status_str = Self::status_to_db(&convocation.status);
44
45 let row = sqlx::query!(
46 r#"
47 INSERT INTO convocations (
48 id, organization_id, building_id, meeting_id, meeting_type, meeting_date,
49 status, minimum_send_date, actual_send_date, scheduled_send_date,
50 pdf_file_path, language, total_recipients, opened_count,
51 will_attend_count, will_not_attend_count, reminder_sent_at,
52 created_at, updated_at, created_by
53 )
54 VALUES ($1, $2, $3, $4, $5::TEXT::convocation_type, $6, $7::TEXT::convocation_status, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
55 RETURNING id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
56 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
57 pdf_file_path, language, total_recipients, opened_count,
58 will_attend_count, will_not_attend_count, reminder_sent_at,
59 created_at, updated_at, created_by
60 "#,
61 convocation.id,
62 convocation.organization_id,
63 convocation.building_id,
64 convocation.meeting_id,
65 meeting_type_str,
66 convocation.meeting_date,
67 status_str,
68 convocation.minimum_send_date,
69 convocation.actual_send_date,
70 convocation.scheduled_send_date,
71 convocation.pdf_file_path,
72 convocation.language,
73 convocation.total_recipients,
74 convocation.opened_count,
75 convocation.will_attend_count,
76 convocation.will_not_attend_count,
77 convocation.reminder_sent_at,
78 convocation.created_at,
79 convocation.updated_at,
80 convocation.created_by,
81 )
82 .fetch_one(&self.pool)
83 .await
84 .map_err(|e| format!("Failed to create convocation: {}", e))?;
85
86 Ok(Convocation {
87 id: row.id,
88 organization_id: row.organization_id,
89 building_id: row.building_id,
90 meeting_id: row.meeting_id,
91 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
92 meeting_date: row.meeting_date,
93 status: Self::status_from_db(&row.status)?,
94 minimum_send_date: row.minimum_send_date,
95 actual_send_date: row.actual_send_date,
96 scheduled_send_date: row.scheduled_send_date,
97 pdf_file_path: row.pdf_file_path,
98 language: row.language,
99 total_recipients: row.total_recipients,
100 opened_count: row.opened_count,
101 will_attend_count: row.will_attend_count,
102 will_not_attend_count: row.will_not_attend_count,
103 reminder_sent_at: row.reminder_sent_at,
104 created_at: row.created_at,
105 updated_at: row.updated_at,
106 created_by: row.created_by,
107 })
108 }
109
110 async fn find_by_id(&self, id: Uuid) -> Result<Option<Convocation>, String> {
111 let row = sqlx::query!(
112 r#"
113 SELECT id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
114 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
115 pdf_file_path, language, total_recipients, opened_count,
116 will_attend_count, will_not_attend_count, reminder_sent_at,
117 created_at, updated_at, created_by
118 FROM convocations
119 WHERE id = $1
120 "#,
121 id
122 )
123 .fetch_optional(&self.pool)
124 .await
125 .map_err(|e| format!("Failed to find convocation by id: {}", e))?;
126
127 match row {
128 Some(row) => Ok(Some(Convocation {
129 id: row.id,
130 organization_id: row.organization_id,
131 building_id: row.building_id,
132 meeting_id: row.meeting_id,
133 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
134 meeting_date: row.meeting_date,
135 status: Self::status_from_db(&row.status)?,
136 minimum_send_date: row.minimum_send_date,
137 actual_send_date: row.actual_send_date,
138 scheduled_send_date: row.scheduled_send_date,
139 pdf_file_path: row.pdf_file_path,
140 language: row.language,
141 total_recipients: row.total_recipients,
142 opened_count: row.opened_count,
143 will_attend_count: row.will_attend_count,
144 will_not_attend_count: row.will_not_attend_count,
145 reminder_sent_at: row.reminder_sent_at,
146 created_at: row.created_at,
147 updated_at: row.updated_at,
148 created_by: row.created_by,
149 })),
150 None => Ok(None),
151 }
152 }
153
154 async fn find_by_meeting_id(&self, meeting_id: Uuid) -> Result<Option<Convocation>, String> {
155 let row = sqlx::query!(
156 r#"
157 SELECT id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
158 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
159 pdf_file_path, language, total_recipients, opened_count,
160 will_attend_count, will_not_attend_count, reminder_sent_at,
161 created_at, updated_at, created_by
162 FROM convocations
163 WHERE meeting_id = $1
164 "#,
165 meeting_id
166 )
167 .fetch_optional(&self.pool)
168 .await
169 .map_err(|e| format!("Failed to find convocation by meeting_id: {}", e))?;
170
171 match row {
172 Some(row) => Ok(Some(Convocation {
173 id: row.id,
174 organization_id: row.organization_id,
175 building_id: row.building_id,
176 meeting_id: row.meeting_id,
177 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
178 meeting_date: row.meeting_date,
179 status: Self::status_from_db(&row.status)?,
180 minimum_send_date: row.minimum_send_date,
181 actual_send_date: row.actual_send_date,
182 scheduled_send_date: row.scheduled_send_date,
183 pdf_file_path: row.pdf_file_path,
184 language: row.language,
185 total_recipients: row.total_recipients,
186 opened_count: row.opened_count,
187 will_attend_count: row.will_attend_count,
188 will_not_attend_count: row.will_not_attend_count,
189 reminder_sent_at: row.reminder_sent_at,
190 created_at: row.created_at,
191 updated_at: row.updated_at,
192 created_by: row.created_by,
193 })),
194 None => Ok(None),
195 }
196 }
197
198 async fn find_by_building(&self, building_id: Uuid) -> Result<Vec<Convocation>, String> {
199 let rows = sqlx::query!(
200 r#"
201 SELECT id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
202 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
203 pdf_file_path, language, total_recipients, opened_count,
204 will_attend_count, will_not_attend_count, reminder_sent_at,
205 created_at, updated_at, created_by
206 FROM convocations
207 WHERE building_id = $1
208 ORDER BY meeting_date DESC
209 "#,
210 building_id
211 )
212 .fetch_all(&self.pool)
213 .await
214 .map_err(|e| format!("Failed to find convocations by building: {}", e))?;
215
216 rows.into_iter()
217 .map(|row| {
218 Ok(Convocation {
219 id: row.id,
220 organization_id: row.organization_id,
221 building_id: row.building_id,
222 meeting_id: row.meeting_id,
223 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
224 meeting_date: row.meeting_date,
225 status: Self::status_from_db(&row.status)?,
226 minimum_send_date: row.minimum_send_date,
227 actual_send_date: row.actual_send_date,
228 scheduled_send_date: row.scheduled_send_date,
229 pdf_file_path: row.pdf_file_path,
230 language: row.language,
231 total_recipients: row.total_recipients,
232 opened_count: row.opened_count,
233 will_attend_count: row.will_attend_count,
234 will_not_attend_count: row.will_not_attend_count,
235 reminder_sent_at: row.reminder_sent_at,
236 created_at: row.created_at,
237 updated_at: row.updated_at,
238 created_by: row.created_by,
239 })
240 })
241 .collect()
242 }
243
244 async fn find_by_organization(
245 &self,
246 organization_id: Uuid,
247 ) -> Result<Vec<Convocation>, String> {
248 let rows = sqlx::query!(
249 r#"
250 SELECT id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
251 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
252 pdf_file_path, language, total_recipients, opened_count,
253 will_attend_count, will_not_attend_count, reminder_sent_at,
254 created_at, updated_at, created_by
255 FROM convocations
256 WHERE organization_id = $1
257 ORDER BY meeting_date DESC
258 "#,
259 organization_id
260 )
261 .fetch_all(&self.pool)
262 .await
263 .map_err(|e| format!("Failed to find convocations by organization: {}", e))?;
264
265 rows.into_iter()
266 .map(|row| {
267 Ok(Convocation {
268 id: row.id,
269 organization_id: row.organization_id,
270 building_id: row.building_id,
271 meeting_id: row.meeting_id,
272 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
273 meeting_date: row.meeting_date,
274 status: Self::status_from_db(&row.status)?,
275 minimum_send_date: row.minimum_send_date,
276 actual_send_date: row.actual_send_date,
277 scheduled_send_date: row.scheduled_send_date,
278 pdf_file_path: row.pdf_file_path,
279 language: row.language,
280 total_recipients: row.total_recipients,
281 opened_count: row.opened_count,
282 will_attend_count: row.will_attend_count,
283 will_not_attend_count: row.will_not_attend_count,
284 reminder_sent_at: row.reminder_sent_at,
285 created_at: row.created_at,
286 updated_at: row.updated_at,
287 created_by: row.created_by,
288 })
289 })
290 .collect()
291 }
292
293 async fn find_by_status(
294 &self,
295 organization_id: Uuid,
296 status: ConvocationStatus,
297 ) -> Result<Vec<Convocation>, String> {
298 let status_str = Self::status_to_db(&status);
299
300 let rows = sqlx::query!(
301 r#"
302 SELECT id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
303 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
304 pdf_file_path, language, total_recipients, opened_count,
305 will_attend_count, will_not_attend_count, reminder_sent_at,
306 created_at, updated_at, created_by
307 FROM convocations
308 WHERE organization_id = $1 AND status = $2::TEXT::convocation_status
309 ORDER BY meeting_date DESC
310 "#,
311 organization_id,
312 status_str
313 )
314 .fetch_all(&self.pool)
315 .await
316 .map_err(|e| format!("Failed to find convocations by status: {}", e))?;
317
318 rows.into_iter()
319 .map(|row| {
320 Ok(Convocation {
321 id: row.id,
322 organization_id: row.organization_id,
323 building_id: row.building_id,
324 meeting_id: row.meeting_id,
325 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
326 meeting_date: row.meeting_date,
327 status: Self::status_from_db(&row.status)?,
328 minimum_send_date: row.minimum_send_date,
329 actual_send_date: row.actual_send_date,
330 scheduled_send_date: row.scheduled_send_date,
331 pdf_file_path: row.pdf_file_path,
332 language: row.language,
333 total_recipients: row.total_recipients,
334 opened_count: row.opened_count,
335 will_attend_count: row.will_attend_count,
336 will_not_attend_count: row.will_not_attend_count,
337 reminder_sent_at: row.reminder_sent_at,
338 created_at: row.created_at,
339 updated_at: row.updated_at,
340 created_by: row.created_by,
341 })
342 })
343 .collect()
344 }
345
346 async fn find_pending_scheduled(&self, now: DateTime<Utc>) -> Result<Vec<Convocation>, String> {
347 let rows = sqlx::query!(
348 r#"
349 SELECT id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
350 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
351 pdf_file_path, language, total_recipients, opened_count,
352 will_attend_count, will_not_attend_count, reminder_sent_at,
353 created_at, updated_at, created_by
354 FROM convocations
355 WHERE status = 'scheduled'
356 AND scheduled_send_date IS NOT NULL
357 AND scheduled_send_date <= $1
358 ORDER BY scheduled_send_date ASC
359 "#,
360 now
361 )
362 .fetch_all(&self.pool)
363 .await
364 .map_err(|e| format!("Failed to find pending scheduled convocations: {}", e))?;
365
366 rows.into_iter()
367 .map(|row| {
368 Ok(Convocation {
369 id: row.id,
370 organization_id: row.organization_id,
371 building_id: row.building_id,
372 meeting_id: row.meeting_id,
373 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
374 meeting_date: row.meeting_date,
375 status: Self::status_from_db(&row.status)?,
376 minimum_send_date: row.minimum_send_date,
377 actual_send_date: row.actual_send_date,
378 scheduled_send_date: row.scheduled_send_date,
379 pdf_file_path: row.pdf_file_path,
380 language: row.language,
381 total_recipients: row.total_recipients,
382 opened_count: row.opened_count,
383 will_attend_count: row.will_attend_count,
384 will_not_attend_count: row.will_not_attend_count,
385 reminder_sent_at: row.reminder_sent_at,
386 created_at: row.created_at,
387 updated_at: row.updated_at,
388 created_by: row.created_by,
389 })
390 })
391 .collect()
392 }
393
394 async fn find_needing_reminder(&self, now: DateTime<Utc>) -> Result<Vec<Convocation>, String> {
395 let three_days_from_now = now + Duration::days(3);
400
401 let rows = sqlx::query!(
402 r#"
403 SELECT id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
404 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
405 pdf_file_path, language, total_recipients, opened_count,
406 will_attend_count, will_not_attend_count, reminder_sent_at,
407 created_at, updated_at, created_by
408 FROM convocations
409 WHERE status = 'sent'
410 AND meeting_date >= $1
411 AND meeting_date <= $2
412 AND reminder_sent_at IS NULL
413 ORDER BY meeting_date ASC
414 "#,
415 now,
416 three_days_from_now
417 )
418 .fetch_all(&self.pool)
419 .await
420 .map_err(|e| format!("Failed to find convocations needing reminder: {}", e))?;
421
422 rows.into_iter()
423 .map(|row| {
424 Ok(Convocation {
425 id: row.id,
426 organization_id: row.organization_id,
427 building_id: row.building_id,
428 meeting_id: row.meeting_id,
429 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
430 meeting_date: row.meeting_date,
431 status: Self::status_from_db(&row.status)?,
432 minimum_send_date: row.minimum_send_date,
433 actual_send_date: row.actual_send_date,
434 scheduled_send_date: row.scheduled_send_date,
435 pdf_file_path: row.pdf_file_path,
436 language: row.language,
437 total_recipients: row.total_recipients,
438 opened_count: row.opened_count,
439 will_attend_count: row.will_attend_count,
440 will_not_attend_count: row.will_not_attend_count,
441 reminder_sent_at: row.reminder_sent_at,
442 created_at: row.created_at,
443 updated_at: row.updated_at,
444 created_by: row.created_by,
445 })
446 })
447 .collect()
448 }
449
450 async fn update(&self, convocation: &Convocation) -> Result<Convocation, String> {
451 let meeting_type_str = Self::convocation_type_to_db(&convocation.meeting_type);
452 let status_str = Self::status_to_db(&convocation.status);
453
454 let row = sqlx::query!(
455 r#"
456 UPDATE convocations
457 SET organization_id = $2, building_id = $3, meeting_id = $4, meeting_type = $5::TEXT::convocation_type, meeting_date = $6,
458 status = $7::TEXT::convocation_status, minimum_send_date = $8, actual_send_date = $9, scheduled_send_date = $10,
459 pdf_file_path = $11, language = $12, total_recipients = $13, opened_count = $14,
460 will_attend_count = $15, will_not_attend_count = $16, reminder_sent_at = $17,
461 updated_at = $18
462 WHERE id = $1
463 RETURNING id, organization_id, building_id, meeting_id, meeting_type::text AS "meeting_type!", meeting_date,
464 status::text AS "status!", minimum_send_date, actual_send_date, scheduled_send_date,
465 pdf_file_path, language, total_recipients, opened_count,
466 will_attend_count, will_not_attend_count, reminder_sent_at,
467 created_at, updated_at, created_by
468 "#,
469 convocation.id,
470 convocation.organization_id,
471 convocation.building_id,
472 convocation.meeting_id,
473 meeting_type_str,
474 convocation.meeting_date,
475 status_str,
476 convocation.minimum_send_date,
477 convocation.actual_send_date,
478 convocation.scheduled_send_date,
479 convocation.pdf_file_path,
480 convocation.language,
481 convocation.total_recipients,
482 convocation.opened_count,
483 convocation.will_attend_count,
484 convocation.will_not_attend_count,
485 convocation.reminder_sent_at,
486 convocation.updated_at,
487 )
488 .fetch_one(&self.pool)
489 .await
490 .map_err(|e| format!("Failed to update convocation: {}", e))?;
491
492 Ok(Convocation {
493 id: row.id,
494 organization_id: row.organization_id,
495 building_id: row.building_id,
496 meeting_id: row.meeting_id,
497 meeting_type: Self::convocation_type_from_db(&row.meeting_type)?,
498 meeting_date: row.meeting_date,
499 status: Self::status_from_db(&row.status)?,
500 minimum_send_date: row.minimum_send_date,
501 actual_send_date: row.actual_send_date,
502 scheduled_send_date: row.scheduled_send_date,
503 pdf_file_path: row.pdf_file_path,
504 language: row.language,
505 total_recipients: row.total_recipients,
506 opened_count: row.opened_count,
507 will_attend_count: row.will_attend_count,
508 will_not_attend_count: row.will_not_attend_count,
509 reminder_sent_at: row.reminder_sent_at,
510 created_at: row.created_at,
511 updated_at: row.updated_at,
512 created_by: row.created_by,
513 })
514 }
515
516 async fn delete(&self, id: Uuid) -> Result<bool, String> {
517 let result = sqlx::query!(
518 r#"
519 DELETE FROM convocations
520 WHERE id = $1
521 "#,
522 id
523 )
524 .execute(&self.pool)
525 .await
526 .map_err(|e| format!("Failed to delete convocation: {}", e))?;
527
528 Ok(result.rows_affected() > 0)
529 }
530
531 async fn count_by_building(&self, building_id: Uuid) -> Result<i64, String> {
532 let row = sqlx::query!(
533 r#"
534 SELECT COUNT(*) AS "count!"
535 FROM convocations
536 WHERE building_id = $1
537 "#,
538 building_id
539 )
540 .fetch_one(&self.pool)
541 .await
542 .map_err(|e| format!("Failed to count convocations by building: {}", e))?;
543
544 Ok(row.count)
545 }
546
547 async fn count_by_status(
548 &self,
549 organization_id: Uuid,
550 status: ConvocationStatus,
551 ) -> Result<i64, String> {
552 let status_str = Self::status_to_db(&status);
553
554 let row = sqlx::query!(
555 r#"
556 SELECT COUNT(*) AS "count!"
557 FROM convocations
558 WHERE organization_id = $1 AND status = $2::TEXT::convocation_status
559 "#,
560 organization_id,
561 status_str
562 )
563 .fetch_one(&self.pool)
564 .await
565 .map_err(|e| format!("Failed to count convocations by status: {}", e))?;
566
567 Ok(row.count)
568 }
569}