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