1use crate::{course_module_completions, error::missing_model_error, prelude::*};
2
3#[derive(Clone, PartialEq, Deserialize, Serialize)]
4pub struct CourseModuleCompletionRegisteredToStudyRegistry {
5 pub id: Uuid,
6 pub created_at: DateTime<Utc>,
7 pub updated_at: DateTime<Utc>,
8 pub deleted_at: Option<DateTime<Utc>>,
9 pub course_id: Uuid,
10 pub course_module_completion_id: Uuid,
11 pub course_module_id: Uuid,
12 pub study_registry_registrar_id: Option<Uuid>,
14 pub user_id: Uuid,
15 pub real_student_number: String,
16}
17
18#[derive(Clone, PartialEq, Deserialize, Serialize)]
19pub struct NewCourseModuleCompletionRegisteredToStudyRegistry {
20 pub course_id: Uuid,
21 pub course_module_completion_id: Uuid,
22 pub course_module_id: Uuid,
23 pub study_registry_registrar_id: Uuid,
24 pub user_id: Uuid,
25 pub real_student_number: String,
26}
27
28pub async fn insert(
29 conn: &mut PgConnection,
30 pkey_policy: PKeyPolicy<Uuid>,
31 new_completion_registration: &NewCourseModuleCompletionRegisteredToStudyRegistry,
32) -> ModelResult<Uuid> {
33 let res = sqlx::query!(
34 "
35INSERT INTO course_module_completion_registered_to_study_registries (
36 id,
37 course_id,
38 course_module_completion_id,
39 course_module_id,
40 study_registry_registrar_id,
41 user_id,
42 real_student_number
43 )
44VALUES (
45 $1,
46 $2,
47 $3,
48 $4,
49 $5,
50 $6,
51 $7
52)
53RETURNING *
54 ",
55 pkey_policy.into_uuid(),
56 new_completion_registration.course_id,
57 new_completion_registration.course_module_completion_id,
58 new_completion_registration.course_module_id,
59 new_completion_registration.study_registry_registrar_id,
60 new_completion_registration.user_id,
61 new_completion_registration.real_student_number,
62 )
63 .fetch_one(conn)
64 .await?;
65 Ok(res.id)
66}
67
68pub async fn insert_or_ignore(
71 conn: &mut PgConnection,
72 new_completion_registration: &NewCourseModuleCompletionRegisteredToStudyRegistry,
73) -> ModelResult<()> {
74 sqlx::query!(
75 "
76INSERT INTO course_module_completion_registered_to_study_registries (
77 course_id,
78 course_module_completion_id,
79 course_module_id,
80 study_registry_registrar_id,
81 user_id,
82 real_student_number
83 )
84VALUES ($1, $2, $3, $4, $5, $6)
85ON CONFLICT DO NOTHING
86 ",
87 new_completion_registration.course_id,
88 new_completion_registration.course_module_completion_id,
89 new_completion_registration.course_module_id,
90 new_completion_registration.study_registry_registrar_id,
91 new_completion_registration.user_id,
92 new_completion_registration.real_student_number,
93 )
94 .execute(conn)
95 .await?;
96 Ok(())
97}
98
99pub async fn insert_bulk(
100 conn: &mut PgConnection,
101 new_completion_registrations: Vec<NewCourseModuleCompletionRegisteredToStudyRegistry>,
102) -> ModelResult<Vec<Uuid>> {
103 if new_completion_registrations.is_empty() {
104 return Ok(vec![]);
105 }
106
107 let ids: Vec<Uuid> = (0..new_completion_registrations.len())
108 .map(|_| Uuid::new_v4())
109 .collect();
110 let course_ids: Vec<Uuid> = new_completion_registrations
111 .iter()
112 .map(|r| r.course_id)
113 .collect();
114 let completion_ids: Vec<Uuid> = new_completion_registrations
115 .iter()
116 .map(|r| r.course_module_completion_id)
117 .collect();
118 let module_ids: Vec<Uuid> = new_completion_registrations
119 .iter()
120 .map(|r| r.course_module_id)
121 .collect();
122 let registrar_ids: Vec<Uuid> = new_completion_registrations
123 .iter()
124 .map(|r| r.study_registry_registrar_id)
125 .collect();
126 let user_ids: Vec<Uuid> = new_completion_registrations
127 .iter()
128 .map(|r| r.user_id)
129 .collect();
130 let student_numbers: Vec<String> = new_completion_registrations
131 .iter()
132 .map(|r| r.real_student_number.clone())
133 .collect();
134
135 let res = sqlx::query!(
136 r#"
137INSERT INTO course_module_completion_registered_to_study_registries (
138 id,
139 course_id,
140 course_module_completion_id,
141 course_module_id,
142 study_registry_registrar_id,
143 user_id,
144 real_student_number
145)
146SELECT * FROM UNNEST(
147 $1::uuid[],
148 $2::uuid[],
149 $3::uuid[],
150 $4::uuid[],
151 $5::uuid[],
152 $6::uuid[],
153 $7::text[]
154)
155-- Matches cmc_registered_to_study_registries_completion_registrar_idx, so a registrar's repeated
156-- POST for a completion it already registered stays idempotent instead of erroring.
157ON CONFLICT (course_module_completion_id, study_registry_registrar_id) WHERE deleted_at IS NULL DO NOTHING
158RETURNING *
159 "#,
160 &ids[..],
161 &course_ids[..],
162 &completion_ids[..],
163 &module_ids[..],
164 ®istrar_ids[..],
165 &user_ids[..],
166 &student_numbers[..],
167 )
168 .fetch_all(conn)
169 .await?;
170
171 Ok(res.into_iter().map(|r| r.id).collect())
172}
173
174#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)]
175pub struct RegisteredCompletion {
177 pub completion_id: Uuid,
179 pub student_number: String,
181 pub registration_date: DateTime<Utc>,
183}
184
185pub async fn mark_completions_as_registered_to_study_registry(
186 conn: &mut PgConnection,
187 completions: Vec<RegisteredCompletion>,
188 study_registry_registrar_id: Uuid,
189) -> ModelResult<()> {
190 if completions.is_empty() {
191 return Ok(());
192 }
193
194 let ids: Vec<Uuid> = completions.iter().map(|x| x.completion_id).collect();
195 let completions_by_id = course_module_completions::get_by_ids_as_map(conn, &ids).await?;
196
197 let mut new_registrations = Vec::with_capacity(completions.len());
198 for completion in completions {
199 let module_completion = completions_by_id
200 .get(&completion.completion_id)
201 .ok_or_else(missing_model_error(
202 ModelErrorType::PreconditionFailed,
203 format!(
204 "Cannot find completion with id: {}. This completion does not exist in the database.",
205 completion.completion_id
206 ),
207 ))?;
208 new_registrations.push(NewCourseModuleCompletionRegisteredToStudyRegistry {
209 course_id: module_completion.course_id,
210 course_module_completion_id: completion.completion_id,
211 course_module_id: module_completion.course_module_id,
212 study_registry_registrar_id,
213 user_id: module_completion.user_id,
214 real_student_number: completion.student_number,
215 });
216 }
217
218 insert_bulk(conn, new_registrations).await?;
219
220 Ok(())
221}
222
223pub async fn get_by_id(
224 conn: &mut PgConnection,
225 id: Uuid,
226) -> ModelResult<CourseModuleCompletionRegisteredToStudyRegistry> {
227 let res = sqlx::query_as!(
228 CourseModuleCompletionRegisteredToStudyRegistry,
229 "
230SELECT *
231FROM course_module_completion_registered_to_study_registries
232WHERE id = $1
233 AND deleted_at IS NULL
234 ",
235 id,
236 )
237 .fetch_one(conn)
238 .await?;
239 Ok(res)
240}
241
242pub async fn get_platform_registered_row_for_completion(
245 conn: &mut PgConnection,
246 completion_id: Uuid,
247) -> ModelResult<Option<CourseModuleCompletionRegisteredToStudyRegistry>> {
248 let res = sqlx::query_as!(
249 CourseModuleCompletionRegisteredToStudyRegistry,
250 "
251SELECT *
252FROM course_module_completion_registered_to_study_registries
253WHERE course_module_completion_id = $1
254 AND study_registry_registrar_id IS NULL
255 AND deleted_at IS NULL
256 ",
257 completion_id,
258 )
259 .fetch_optional(conn)
260 .await?;
261 Ok(res)
262}
263
264pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
265 sqlx::query!(
266 "
267UPDATE course_module_completion_registered_to_study_registries
268SET deleted_at = now()
269WHERE id = $1
270AND deleted_at IS NULL
271 ",
272 id
273 )
274 .execute(conn)
275 .await?;
276 Ok(())
277}
278
279pub async fn get_count_of_distinct_users_with_registrations_by_course_id(
281 conn: &mut PgConnection,
282 course_id: Uuid,
283) -> ModelResult<i64> {
284 let res = sqlx::query!(
285 "
286SELECT COUNT(DISTINCT user_id) as count
287FROM course_module_completion_registered_to_study_registries
288WHERE course_id = $1
289 AND deleted_at IS NULL
290",
291 course_id,
292 )
293 .fetch_one(conn)
294 .await?;
295 Ok(res.count.unwrap_or(0))
296}
297
298pub async fn get_by_completion_id_and_registrar_id(
299 conn: &mut PgConnection,
300 completion_id: Uuid,
301 study_registry_registrar_id: Uuid,
302) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
303 let registrations = sqlx::query_as!(
304 CourseModuleCompletionRegisteredToStudyRegistry,
305 r#"
306 SELECT *
307 FROM course_module_completion_registered_to_study_registries
308 WHERE course_module_completion_id = $1 AND study_registry_registrar_id = $2
309 AND deleted_at IS NULL
310 "#,
311 completion_id,
312 study_registry_registrar_id
313 )
314 .fetch_all(conn)
315 .await?;
316
317 Ok(registrations)
318}
319
320pub async fn get_by_registrar_id_and_completion_ids(
322 conn: &mut PgConnection,
323 study_registry_registrar_id: Uuid,
324 completion_ids: &[Uuid],
325) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
326 let registrations = sqlx::query_as!(
327 CourseModuleCompletionRegisteredToStudyRegistry,
328 r#"
329SELECT *
330FROM course_module_completion_registered_to_study_registries
331WHERE study_registry_registrar_id = $1
332 AND course_module_completion_id = ANY($2)
333 AND deleted_at IS NULL
334 "#,
335 study_registry_registrar_id,
336 completion_ids
337 )
338 .fetch_all(conn)
339 .await?;
340
341 Ok(registrations)
342}
343
344#[derive(Clone, PartialEq, Deserialize, Serialize)]
345pub struct CourseModuleCompletionRegistrationState {
348 pub course_module_completion_id: Uuid,
349 pub course_module_id: Uuid,
350 pub study_registry_registrar_id: Option<Uuid>,
351 pub created_at: DateTime<Utc>,
352}
353
354pub async fn get_registrations_by_completion_ids(
357 conn: &mut PgConnection,
358 completion_ids: &[Uuid],
359) -> ModelResult<Vec<CourseModuleCompletionRegistrationState>> {
360 let registrations = sqlx::query_as!(
361 CourseModuleCompletionRegistrationState,
362 r#"
363SELECT course_module_completion_id,
364 course_module_id,
365 study_registry_registrar_id,
366 created_at
367FROM course_module_completion_registered_to_study_registries
368WHERE course_module_completion_id = ANY($1)
369 AND deleted_at IS NULL
370 "#,
371 completion_ids
372 )
373 .fetch_all(conn)
374 .await?;
375
376 Ok(registrations)
377}
378
379pub async fn completion_ids_registered_by_a_registrar(
384 conn: &mut PgConnection,
385 completion_ids: &[Uuid],
386) -> ModelResult<Vec<Uuid>> {
387 let ids = sqlx::query_scalar!(
388 r#"
389SELECT DISTINCT course_module_completion_id
390FROM course_module_completion_registered_to_study_registries
391WHERE course_module_completion_id = ANY($1::uuid [])
392 AND study_registry_registrar_id IS NOT NULL
393 AND deleted_at IS NULL
394 "#,
395 completion_ids,
396 )
397 .fetch_all(conn)
398 .await?;
399 Ok(ids)
400}
401
402#[cfg(test)]
403mod test {
404 use super::*;
405 use crate::{course_module_completions::CourseModuleCompletionGranter, test_helper::*};
406
407 #[tokio::test]
408 async fn bulk_insert_works() {
409 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
410
411 let registrar_id = crate::study_registry_registrars::insert(
412 tx.as_mut(),
413 PKeyPolicy::Generate,
414 "Test Registrar",
415 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
416 )
417 .await
418 .unwrap();
419
420 let completion_1 = crate::course_module_completions::insert(
421 tx.as_mut(),
422 PKeyPolicy::Generate,
423 &crate::course_module_completions::NewCourseModuleCompletion {
424 course_id: course,
425 course_module_id: course_module.id,
426 user_id: user,
427 completion_date: Utc::now(),
428 completion_registration_attempt_date: None,
429 completion_language: "en-US".to_string(),
430 eligible_for_ects: true,
431 email: "test@example.com".to_string(),
432 grade: Some(10),
433 passed: true,
434 },
435 CourseModuleCompletionGranter::User(user),
436 )
437 .await
438 .unwrap();
439 let completion_2 = crate::course_module_completions::insert(
440 tx.as_mut(),
441 PKeyPolicy::Generate,
442 &crate::course_module_completions::NewCourseModuleCompletion {
443 course_id: course,
444 course_module_id: course_module.id,
445 user_id: user,
446 completion_date: Utc::now(),
447 completion_registration_attempt_date: None,
448 completion_language: "en-US".to_string(),
449 eligible_for_ects: true,
450 email: "test2@example.com".to_string(),
451 grade: Some(9),
452 passed: true,
453 },
454 CourseModuleCompletionGranter::User(user),
455 )
456 .await
457 .unwrap();
458
459 let registrations = vec![
460 NewCourseModuleCompletionRegisteredToStudyRegistry {
461 course_id: course,
462 course_module_completion_id: completion_1.id,
463 course_module_id: course_module.id,
464 study_registry_registrar_id: registrar_id,
465 user_id: user,
466 real_student_number: "12345".to_string(),
467 },
468 NewCourseModuleCompletionRegisteredToStudyRegistry {
469 course_id: course,
470 course_module_completion_id: completion_2.id,
471 course_module_id: course_module.id,
472 study_registry_registrar_id: registrar_id,
473 user_id: user,
474 real_student_number: "67890".to_string(),
475 },
476 ];
477
478 let inserted_ids = insert_bulk(tx.as_mut(), registrations).await.unwrap();
479 assert_eq!(inserted_ids.len(), 2);
480
481 let mut registered_completion_ids = Vec::new();
482 for id in inserted_ids {
483 let registration = get_by_id(tx.as_mut(), id).await.unwrap();
484 assert_eq!(registration.course_id, course);
485 assert_eq!(registration.course_module_id, course_module.id);
486 assert_eq!(registration.study_registry_registrar_id, Some(registrar_id));
487 assert_eq!(registration.user_id, user);
488 registered_completion_ids.push(registration.course_module_completion_id);
489 }
490 registered_completion_ids.sort();
491 let mut expected = vec![completion_1.id, completion_2.id];
492 expected.sort();
493 assert_eq!(registered_completion_ids, expected);
494 }
495
496 #[tokio::test]
497 async fn bulk_insert_empty_vec_works() {
498 insert_data!(:tx);
499
500 let empty_vec = vec![];
501 let result = insert_bulk(tx.as_mut(), empty_vec).await.unwrap();
502 assert!(result.is_empty());
503 }
504
505 #[tokio::test]
506 async fn insert_completions_works() {
507 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
508
509 let registrar_id = crate::study_registry_registrars::insert(
510 tx.as_mut(),
511 PKeyPolicy::Generate,
512 "Test Registrar",
513 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
514 )
515 .await
516 .unwrap();
517
518 let completion = crate::course_module_completions::insert(
519 tx.as_mut(),
520 PKeyPolicy::Generate,
521 &crate::course_module_completions::NewCourseModuleCompletion {
522 course_id: course,
523 course_module_id: course_module.id,
524 user_id: user,
525 completion_date: Utc::now(),
526 completion_registration_attempt_date: None,
527 completion_language: "en-US".to_string(),
528 eligible_for_ects: true,
529 email: "test@example.com".to_string(),
530 grade: Some(5),
531 passed: true,
532 },
533 CourseModuleCompletionGranter::User(user),
534 )
535 .await
536 .unwrap();
537
538 let registered_completions = vec![RegisteredCompletion {
539 completion_id: completion.id,
540 student_number: "12345".to_string(),
541 registration_date: Utc::now(),
542 }];
543
544 mark_completions_as_registered_to_study_registry(
545 tx.as_mut(),
546 registered_completions,
547 registrar_id,
548 )
549 .await
550 .unwrap();
551
552 let registrations =
553 get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
554 .await
555 .unwrap();
556
557 assert_eq!(registrations.len(), 1);
558 assert_eq!(registrations[0].course_id, course);
559 assert_eq!(registrations[0].course_module_id, course_module.id);
560 assert_eq!(registrations[0].user_id, user);
561 assert_eq!(registrations[0].real_student_number, "12345");
562 }
563
564 #[tokio::test]
565 async fn insert_completions_with_invalid_completion_id_fails() {
566 insert_data!(:tx);
567
568 let registrar_id = crate::study_registry_registrars::insert(
569 tx.as_mut(),
570 PKeyPolicy::Generate,
571 "Test Registrar",
572 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
573 )
574 .await
575 .unwrap();
576
577 let invalid_uuid = Uuid::new_v4(); let registered_completions = vec![RegisteredCompletion {
579 completion_id: invalid_uuid,
580 student_number: "12345".to_string(),
581 registration_date: Utc::now(),
582 }];
583
584 let result = mark_completions_as_registered_to_study_registry(
586 tx.as_mut(),
587 registered_completions,
588 registrar_id,
589 )
590 .await;
591
592 assert!(result.is_err());
593 let error = result.unwrap_err();
594 assert_eq!(*error.error_type(), ModelErrorType::PreconditionFailed);
595 assert!(error.message().contains("Cannot find completion with id"));
596 assert!(error.message().contains(&invalid_uuid.to_string()));
597 }
598}