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 let user_ids: Vec<Uuid> = new_registrations.iter().map(|r| r.user_id).collect();
219 let mut tx = conn.begin().await?;
220 insert_bulk(&mut tx, new_registrations).await?;
221 let mut savepoint = tx.begin().await?;
223 match crate::verified_student_numbers::link_numbers_reported_by_study_registry(
224 &mut savepoint,
225 &user_ids,
226 )
227 .await
228 {
229 Ok(_) => savepoint.commit().await?,
230 Err(error) => {
231 warn!("Could not link student numbers the study registry reported: {error}");
232 savepoint.rollback().await?;
233 }
234 }
235 tx.commit().await?;
236
237 Ok(())
238}
239
240pub async fn get_by_id(
241 conn: &mut PgConnection,
242 id: Uuid,
243) -> ModelResult<CourseModuleCompletionRegisteredToStudyRegistry> {
244 let res = sqlx::query_as!(
245 CourseModuleCompletionRegisteredToStudyRegistry,
246 "
247SELECT *
248FROM course_module_completion_registered_to_study_registries
249WHERE id = $1
250 AND deleted_at IS NULL
251 ",
252 id,
253 )
254 .fetch_one(conn)
255 .await?;
256 Ok(res)
257}
258
259pub async fn get_platform_registered_row_for_completion(
262 conn: &mut PgConnection,
263 completion_id: Uuid,
264) -> ModelResult<Option<CourseModuleCompletionRegisteredToStudyRegistry>> {
265 let res = sqlx::query_as!(
266 CourseModuleCompletionRegisteredToStudyRegistry,
267 "
268SELECT *
269FROM course_module_completion_registered_to_study_registries
270WHERE course_module_completion_id = $1
271 AND study_registry_registrar_id IS NULL
272 AND deleted_at IS NULL
273 ",
274 completion_id,
275 )
276 .fetch_optional(conn)
277 .await?;
278 Ok(res)
279}
280
281pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
282 sqlx::query!(
283 "
284UPDATE course_module_completion_registered_to_study_registries
285SET deleted_at = now()
286WHERE id = $1
287AND deleted_at IS NULL
288 ",
289 id
290 )
291 .execute(conn)
292 .await?;
293 Ok(())
294}
295
296pub async fn get_count_of_distinct_users_with_registrations_by_course_id(
298 conn: &mut PgConnection,
299 course_id: Uuid,
300) -> ModelResult<i64> {
301 let res = sqlx::query!(
302 "
303SELECT COUNT(DISTINCT user_id) as count
304FROM course_module_completion_registered_to_study_registries
305WHERE course_id = $1
306 AND deleted_at IS NULL
307",
308 course_id,
309 )
310 .fetch_one(conn)
311 .await?;
312 Ok(res.count.unwrap_or(0))
313}
314
315pub async fn get_by_completion_id_and_registrar_id(
316 conn: &mut PgConnection,
317 completion_id: Uuid,
318 study_registry_registrar_id: Uuid,
319) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
320 let registrations = sqlx::query_as!(
321 CourseModuleCompletionRegisteredToStudyRegistry,
322 r#"
323 SELECT *
324 FROM course_module_completion_registered_to_study_registries
325 WHERE course_module_completion_id = $1 AND study_registry_registrar_id = $2
326 AND deleted_at IS NULL
327 "#,
328 completion_id,
329 study_registry_registrar_id
330 )
331 .fetch_all(conn)
332 .await?;
333
334 Ok(registrations)
335}
336
337pub async fn get_by_registrar_id_and_completion_ids(
339 conn: &mut PgConnection,
340 study_registry_registrar_id: Uuid,
341 completion_ids: &[Uuid],
342) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
343 let registrations = sqlx::query_as!(
344 CourseModuleCompletionRegisteredToStudyRegistry,
345 r#"
346SELECT *
347FROM course_module_completion_registered_to_study_registries
348WHERE study_registry_registrar_id = $1
349 AND course_module_completion_id = ANY($2)
350 AND deleted_at IS NULL
351 "#,
352 study_registry_registrar_id,
353 completion_ids
354 )
355 .fetch_all(conn)
356 .await?;
357
358 Ok(registrations)
359}
360
361#[derive(Clone, PartialEq, Deserialize, Serialize)]
362pub struct CourseModuleCompletionRegistrationState {
365 pub course_module_completion_id: Uuid,
366 pub course_module_id: Uuid,
367 pub study_registry_registrar_id: Option<Uuid>,
368 pub created_at: DateTime<Utc>,
369}
370
371pub async fn get_registrations_by_completion_ids(
374 conn: &mut PgConnection,
375 completion_ids: &[Uuid],
376) -> ModelResult<Vec<CourseModuleCompletionRegistrationState>> {
377 let registrations = sqlx::query_as!(
378 CourseModuleCompletionRegistrationState,
379 r#"
380SELECT course_module_completion_id,
381 course_module_id,
382 study_registry_registrar_id,
383 created_at
384FROM course_module_completion_registered_to_study_registries
385WHERE course_module_completion_id = ANY($1)
386 AND deleted_at IS NULL
387 "#,
388 completion_ids
389 )
390 .fetch_all(conn)
391 .await?;
392
393 Ok(registrations)
394}
395
396pub async fn completion_ids_registered_by_a_registrar(
401 conn: &mut PgConnection,
402 completion_ids: &[Uuid],
403) -> ModelResult<Vec<Uuid>> {
404 let ids = sqlx::query_scalar!(
405 r#"
406SELECT DISTINCT course_module_completion_id
407FROM course_module_completion_registered_to_study_registries
408WHERE course_module_completion_id = ANY($1::uuid [])
409 AND study_registry_registrar_id IS NOT NULL
410 AND deleted_at IS NULL
411 "#,
412 completion_ids,
413 )
414 .fetch_all(conn)
415 .await?;
416 Ok(ids)
417}
418
419#[cfg(test)]
420mod test {
421 use super::*;
422 use crate::{course_module_completions::CourseModuleCompletionGranter, test_helper::*};
423
424 #[tokio::test]
425 async fn bulk_insert_works() {
426 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
427
428 let registrar_id = crate::study_registry_registrars::insert(
429 tx.as_mut(),
430 PKeyPolicy::Generate,
431 "Test Registrar",
432 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
433 )
434 .await
435 .unwrap();
436
437 let completion_1 = crate::course_module_completions::insert(
438 tx.as_mut(),
439 PKeyPolicy::Generate,
440 &crate::course_module_completions::NewCourseModuleCompletion {
441 course_id: course,
442 course_module_id: course_module.id,
443 user_id: user,
444 completion_date: Utc::now(),
445 completion_registration_attempt_date: None,
446 completion_language: "en-US".to_string(),
447 eligible_for_ects: true,
448 email: "test@example.com".to_string(),
449 grade: Some(10),
450 passed: true,
451 },
452 CourseModuleCompletionGranter::User(user),
453 )
454 .await
455 .unwrap();
456 let completion_2 = crate::course_module_completions::insert(
457 tx.as_mut(),
458 PKeyPolicy::Generate,
459 &crate::course_module_completions::NewCourseModuleCompletion {
460 course_id: course,
461 course_module_id: course_module.id,
462 user_id: user,
463 completion_date: Utc::now(),
464 completion_registration_attempt_date: None,
465 completion_language: "en-US".to_string(),
466 eligible_for_ects: true,
467 email: "test2@example.com".to_string(),
468 grade: Some(9),
469 passed: true,
470 },
471 CourseModuleCompletionGranter::User(user),
472 )
473 .await
474 .unwrap();
475
476 let registrations = vec![
477 NewCourseModuleCompletionRegisteredToStudyRegistry {
478 course_id: course,
479 course_module_completion_id: completion_1.id,
480 course_module_id: course_module.id,
481 study_registry_registrar_id: registrar_id,
482 user_id: user,
483 real_student_number: "12345".to_string(),
484 },
485 NewCourseModuleCompletionRegisteredToStudyRegistry {
486 course_id: course,
487 course_module_completion_id: completion_2.id,
488 course_module_id: course_module.id,
489 study_registry_registrar_id: registrar_id,
490 user_id: user,
491 real_student_number: "67890".to_string(),
492 },
493 ];
494
495 let inserted_ids = insert_bulk(tx.as_mut(), registrations).await.unwrap();
496 assert_eq!(inserted_ids.len(), 2);
497
498 let mut registered_completion_ids = Vec::new();
499 for id in inserted_ids {
500 let registration = get_by_id(tx.as_mut(), id).await.unwrap();
501 assert_eq!(registration.course_id, course);
502 assert_eq!(registration.course_module_id, course_module.id);
503 assert_eq!(registration.study_registry_registrar_id, Some(registrar_id));
504 assert_eq!(registration.user_id, user);
505 registered_completion_ids.push(registration.course_module_completion_id);
506 }
507 registered_completion_ids.sort();
508 let mut expected = vec![completion_1.id, completion_2.id];
509 expected.sort();
510 assert_eq!(registered_completion_ids, expected);
511 }
512
513 #[tokio::test]
514 async fn bulk_insert_empty_vec_works() {
515 insert_data!(:tx);
516
517 let empty_vec = vec![];
518 let result = insert_bulk(tx.as_mut(), empty_vec).await.unwrap();
519 assert!(result.is_empty());
520 }
521
522 #[tokio::test]
523 async fn insert_completions_works() {
524 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
525
526 let registrar_id = crate::study_registry_registrars::insert(
527 tx.as_mut(),
528 PKeyPolicy::Generate,
529 "Test Registrar",
530 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
531 )
532 .await
533 .unwrap();
534
535 let completion = crate::course_module_completions::insert(
536 tx.as_mut(),
537 PKeyPolicy::Generate,
538 &crate::course_module_completions::NewCourseModuleCompletion {
539 course_id: course,
540 course_module_id: course_module.id,
541 user_id: user,
542 completion_date: Utc::now(),
543 completion_registration_attempt_date: None,
544 completion_language: "en-US".to_string(),
545 eligible_for_ects: true,
546 email: "test@example.com".to_string(),
547 grade: Some(5),
548 passed: true,
549 },
550 CourseModuleCompletionGranter::User(user),
551 )
552 .await
553 .unwrap();
554
555 let registered_completions = vec![RegisteredCompletion {
556 completion_id: completion.id,
557 student_number: "12345".to_string(),
558 registration_date: Utc::now(),
559 }];
560
561 mark_completions_as_registered_to_study_registry(
562 tx.as_mut(),
563 registered_completions,
564 registrar_id,
565 )
566 .await
567 .unwrap();
568
569 let registrations =
570 get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
571 .await
572 .unwrap();
573
574 assert_eq!(registrations.len(), 1);
575 assert_eq!(registrations[0].course_id, course);
576 assert_eq!(registrations[0].course_module_id, course_module.id);
577 assert_eq!(registrations[0].user_id, user);
578 assert_eq!(registrations[0].real_student_number, "12345");
579 }
580
581 #[tokio::test]
582 async fn insert_completions_with_invalid_completion_id_fails() {
583 insert_data!(:tx);
584
585 let registrar_id = crate::study_registry_registrars::insert(
586 tx.as_mut(),
587 PKeyPolicy::Generate,
588 "Test Registrar",
589 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
590 )
591 .await
592 .unwrap();
593
594 let invalid_uuid = Uuid::new_v4(); let registered_completions = vec![RegisteredCompletion {
596 completion_id: invalid_uuid,
597 student_number: "12345".to_string(),
598 registration_date: Utc::now(),
599 }];
600
601 let result = mark_completions_as_registered_to_study_registry(
603 tx.as_mut(),
604 registered_completions,
605 registrar_id,
606 )
607 .await;
608
609 assert!(result.is_err());
610 let error = result.unwrap_err();
611 assert_eq!(*error.error_type(), ModelErrorType::PreconditionFailed);
612 assert!(error.message().contains("Cannot find completion with id"));
613 assert!(error.message().contains(&invalid_uuid.to_string()));
614 }
615}