1use crate::{course_module_completions, prelude::*};
2use rand::RngExt;
3
4#[derive(Clone, PartialEq, Deserialize, Serialize)]
5pub struct CourseModuleCompletionRegisteredToStudyRegistry {
6 pub id: Uuid,
7 pub created_at: DateTime<Utc>,
8 pub updated_at: DateTime<Utc>,
9 pub deleted_at: Option<DateTime<Utc>>,
10 pub course_id: Uuid,
11 pub course_module_completion_id: Uuid,
12 pub course_module_id: Uuid,
13 pub study_registry_registrar_id: 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_bulk(
69 conn: &mut PgConnection,
70 new_completion_registrations: Vec<NewCourseModuleCompletionRegisteredToStudyRegistry>,
71) -> ModelResult<Vec<Uuid>> {
72 if new_completion_registrations.is_empty() {
73 return Ok(vec![]);
74 }
75
76 let ids: Vec<Uuid> = (0..new_completion_registrations.len())
78 .map(|_| Uuid::new_v4())
79 .collect();
80 let course_ids: Vec<Uuid> = new_completion_registrations
81 .iter()
82 .map(|r| r.course_id)
83 .collect();
84 let completion_ids: Vec<Uuid> = new_completion_registrations
85 .iter()
86 .map(|r| r.course_module_completion_id)
87 .collect();
88 let module_ids: Vec<Uuid> = new_completion_registrations
89 .iter()
90 .map(|r| r.course_module_id)
91 .collect();
92 let registrar_ids: Vec<Uuid> = new_completion_registrations
93 .iter()
94 .map(|r| r.study_registry_registrar_id)
95 .collect();
96 let user_ids: Vec<Uuid> = new_completion_registrations
97 .iter()
98 .map(|r| r.user_id)
99 .collect();
100 let student_numbers: Vec<String> = new_completion_registrations
101 .iter()
102 .map(|r| r.real_student_number.clone())
103 .collect();
104
105 let res = sqlx::query!(
106 r#"
107INSERT INTO course_module_completion_registered_to_study_registries (
108 id,
109 course_id,
110 course_module_completion_id,
111 course_module_id,
112 study_registry_registrar_id,
113 user_id,
114 real_student_number
115)
116SELECT * FROM UNNEST(
117 $1::uuid[],
118 $2::uuid[],
119 $3::uuid[],
120 $4::uuid[],
121 $5::uuid[],
122 $6::uuid[],
123 $7::text[]
124)
125RETURNING *
126 "#,
127 &ids[..],
128 &course_ids[..],
129 &completion_ids[..],
130 &module_ids[..],
131 ®istrar_ids[..],
132 &user_ids[..],
133 &student_numbers[..],
134 )
135 .fetch_all(conn)
136 .await?;
137
138 Ok(res.into_iter().map(|r| r.id).collect())
139}
140
141#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)]
142pub struct RegisteredCompletion {
144 pub completion_id: Uuid,
146 pub student_number: String,
148 pub registration_date: DateTime<Utc>,
150}
151
152pub async fn mark_completions_as_registered_to_study_registry(
153 conn: &mut PgConnection,
154 completions: Vec<RegisteredCompletion>,
155 study_registry_registrar_id: Uuid,
156) -> ModelResult<()> {
157 if completions.is_empty() {
158 return Ok(());
159 }
160
161 let ids: Vec<Uuid> = completions.iter().map(|x| x.completion_id).collect();
162 let completions_by_id = course_module_completions::get_by_ids_as_map(conn, &ids).await?;
163
164 for completion in &completions {
166 if !completions_by_id.contains_key(&completion.completion_id) {
167 return Err(ModelError::new(
168 ModelErrorType::PreconditionFailed,
169 format!(
170 "Cannot find completion with id: {}. This completion does not exist in the database.",
171 completion.completion_id
172 ),
173 None,
174 ));
175 }
176 }
177
178 let new_registrations = completions
179 .into_iter()
180 .map(|completion| {
181 let module_completion = completions_by_id
182 .get(&completion.completion_id)
183 .ok_or_else(|| {
184 ModelError::new(
185 ModelErrorType::PreconditionFailed,
186 format!(
187 "Completion with id {} not found after validation - this should never happen",
188 completion.completion_id
189 ),
190 None,
191 )
192 })?;
193 Ok(NewCourseModuleCompletionRegisteredToStudyRegistry {
194 course_id: module_completion.course_id,
195 course_module_completion_id: completion.completion_id,
196 course_module_id: module_completion.course_module_id,
197 study_registry_registrar_id,
198 user_id: module_completion.user_id,
199 real_student_number: completion.student_number,
200 })
201 })
202 .collect::<ModelResult<Vec<_>>>()?;
203
204 let mut tx = conn.begin().await?;
205
206 insert_bulk(&mut tx, new_registrations).await?;
207
208 let mut rng = rand::rng();
213 let should_cleanup = rng.random_range(0..50) == 0;
214 if should_cleanup {
215 cleanup_duplicates_by_registrar_id_and_completion_ids(
216 &mut tx,
217 study_registry_registrar_id,
218 &ids,
219 )
220 .await?;
221 }
222
223 tx.commit().await?;
224
225 Ok(())
226}
227
228pub async fn get_by_id(
229 conn: &mut PgConnection,
230 id: Uuid,
231) -> ModelResult<CourseModuleCompletionRegisteredToStudyRegistry> {
232 let res = sqlx::query_as!(
233 CourseModuleCompletionRegisteredToStudyRegistry,
234 "
235SELECT *
236FROM course_module_completion_registered_to_study_registries
237WHERE id = $1
238 AND deleted_at IS NULL
239 ",
240 id,
241 )
242 .fetch_one(conn)
243 .await?;
244 Ok(res)
245}
246
247pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
248 sqlx::query!(
249 "
250UPDATE course_module_completion_registered_to_study_registries
251SET deleted_at = now()
252WHERE id = $1
253AND deleted_at IS NULL
254 ",
255 id
256 )
257 .execute(conn)
258 .await?;
259 Ok(())
260}
261
262pub async fn get_count_of_distinct_users_with_registrations_by_course_id(
264 conn: &mut PgConnection,
265 course_id: Uuid,
266) -> ModelResult<i64> {
267 let res = sqlx::query!(
268 "
269SELECT COUNT(DISTINCT user_id) as count
270FROM course_module_completion_registered_to_study_registries
271WHERE course_id = $1
272 AND deleted_at IS NULL
273",
274 course_id,
275 )
276 .fetch_one(conn)
277 .await?;
278 Ok(res.count.unwrap_or(0))
279}
280
281pub async fn get_by_completion_id_and_registrar_id(
282 conn: &mut PgConnection,
283 completion_id: Uuid,
284 study_registry_registrar_id: Uuid,
285) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
286 let registrations = sqlx::query_as!(
287 CourseModuleCompletionRegisteredToStudyRegistry,
288 r#"
289 SELECT *
290 FROM course_module_completion_registered_to_study_registries
291 WHERE course_module_completion_id = $1 AND study_registry_registrar_id = $2
292 AND deleted_at IS NULL
293 "#,
294 completion_id,
295 study_registry_registrar_id
296 )
297 .fetch_all(conn)
298 .await?;
299
300 Ok(registrations)
301}
302
303pub async fn get_by_registrar_id_and_completion_ids(
305 conn: &mut PgConnection,
306 study_registry_registrar_id: Uuid,
307 completion_ids: &[Uuid],
308) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
309 let registrations = sqlx::query_as!(
310 CourseModuleCompletionRegisteredToStudyRegistry,
311 r#"
312SELECT *
313FROM course_module_completion_registered_to_study_registries
314WHERE study_registry_registrar_id = $1
315 AND course_module_completion_id = ANY($2)
316 AND deleted_at IS NULL
317 "#,
318 study_registry_registrar_id,
319 completion_ids
320 )
321 .fetch_all(conn)
322 .await?;
323
324 Ok(registrations)
325}
326
327pub async fn cleanup_duplicates_by_registrar_id_and_completion_ids(
331 conn: &mut PgConnection,
332 study_registry_registrar_id: Uuid,
333 completion_ids: &[Uuid],
334) -> ModelResult<i64> {
335 let res = sqlx::query!(
336 r#"
337WITH duplicate_rows AS (
338 SELECT id,
339 ROW_NUMBER() OVER (
340 PARTITION BY study_registry_registrar_id, course_module_completion_id
341 ORDER BY created_at ASC, id ASC
342 ) AS rn
343 FROM course_module_completion_registered_to_study_registries
344 WHERE study_registry_registrar_id = $1
345 AND course_module_completion_id = ANY($2)
346 AND deleted_at IS NULL
347)
348UPDATE course_module_completion_registered_to_study_registries
349SET deleted_at = NOW()
350WHERE id IN (
351 SELECT id
352 FROM duplicate_rows
353 WHERE rn > 1
354 )
355 AND deleted_at IS NULL
356 "#,
357 study_registry_registrar_id,
358 completion_ids
359 )
360 .execute(conn)
361 .await?;
362
363 Ok(res.rows_affected() as i64)
364}
365
366pub async fn insert_record(
367 conn: &mut PgConnection,
368 course_id: Uuid,
369 completion_id: Uuid,
370 module_id: Uuid,
371 registrar_id: Uuid,
372 user_id: Uuid,
373 real_student_number: &str,
374) -> ModelResult<()> {
375 sqlx::query!(
376 r#"
377 INSERT INTO course_module_completion_registered_to_study_registries (
378 course_id,
379 course_module_completion_id,
380 course_module_id,
381 study_registry_registrar_id,
382 user_id,
383 real_student_number
384 )
385 VALUES ($1,$2,$3,$4,$5,$6)
386 ON CONFLICT DO NOTHING
387 "#,
388 course_id,
389 completion_id,
390 module_id,
391 registrar_id,
392 user_id,
393 real_student_number
394 )
395 .execute(conn)
396 .await?;
397
398 Ok(())
399}
400
401#[cfg(test)]
402mod test {
403 use super::*;
404 use crate::{course_module_completions::CourseModuleCompletionGranter, test_helper::*};
405
406 #[tokio::test]
407 async fn bulk_insert_works() {
408 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
409
410 let registrar_id = crate::study_registry_registrars::insert(
411 tx.as_mut(),
412 PKeyPolicy::Generate,
413 "Test Registrar",
414 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
415 )
416 .await
417 .unwrap();
418
419 let new_completion_id = crate::course_module_completions::insert(
420 tx.as_mut(),
421 PKeyPolicy::Generate,
422 &crate::course_module_completions::NewCourseModuleCompletion {
423 course_id: course,
424 course_module_id: course_module.id,
425 user_id: user,
426 completion_date: Utc::now(),
427 completion_registration_attempt_date: None,
428 completion_language: "en-US".to_string(),
429 eligible_for_ects: true,
430 email: "test@example.com".to_string(),
431 grade: Some(10),
432 passed: true,
433 },
434 CourseModuleCompletionGranter::User(user),
435 )
436 .await
437 .unwrap();
438
439 let registrations = vec![
440 NewCourseModuleCompletionRegisteredToStudyRegistry {
441 course_id: course,
442 course_module_completion_id: new_completion_id.id,
443 course_module_id: course_module.id,
444 study_registry_registrar_id: registrar_id,
445 user_id: user,
446 real_student_number: "12345".to_string(),
447 },
448 NewCourseModuleCompletionRegisteredToStudyRegistry {
449 course_id: course,
450 course_module_completion_id: new_completion_id.id,
451 course_module_id: course_module.id,
452 study_registry_registrar_id: registrar_id,
453 user_id: user,
454 real_student_number: "67890".to_string(),
455 },
456 ];
457
458 let inserted_ids = insert_bulk(tx.as_mut(), registrations).await.unwrap();
459 assert_eq!(inserted_ids.len(), 2);
460
461 for id in inserted_ids {
463 let registration = get_by_id(tx.as_mut(), id).await.unwrap();
464 assert_eq!(registration.course_id, course);
465 assert_eq!(
466 registration.course_module_completion_id,
467 new_completion_id.id
468 );
469 assert_eq!(registration.course_module_id, course_module.id);
470 assert_eq!(registration.study_registry_registrar_id, registrar_id);
471 assert_eq!(registration.user_id, user);
472 }
473 }
474
475 #[tokio::test]
476 async fn bulk_insert_empty_vec_works() {
477 insert_data!(:tx);
478
479 let empty_vec = vec![];
480 let result = insert_bulk(tx.as_mut(), empty_vec).await.unwrap();
481 assert!(result.is_empty());
482 }
483
484 #[tokio::test]
485 async fn insert_completions_works() {
486 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
487
488 let registrar_id = crate::study_registry_registrars::insert(
489 tx.as_mut(),
490 PKeyPolicy::Generate,
491 "Test Registrar",
492 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
493 )
494 .await
495 .unwrap();
496
497 let completion = crate::course_module_completions::insert(
498 tx.as_mut(),
499 PKeyPolicy::Generate,
500 &crate::course_module_completions::NewCourseModuleCompletion {
501 course_id: course,
502 course_module_id: course_module.id,
503 user_id: user,
504 completion_date: Utc::now(),
505 completion_registration_attempt_date: None,
506 completion_language: "en-US".to_string(),
507 eligible_for_ects: true,
508 email: "test@example.com".to_string(),
509 grade: Some(5),
510 passed: true,
511 },
512 CourseModuleCompletionGranter::User(user),
513 )
514 .await
515 .unwrap();
516
517 let registered_completions = vec![RegisteredCompletion {
518 completion_id: completion.id,
519 student_number: "12345".to_string(),
520 registration_date: Utc::now(),
521 }];
522
523 mark_completions_as_registered_to_study_registry(
524 tx.as_mut(),
525 registered_completions,
526 registrar_id,
527 )
528 .await
529 .unwrap();
530
531 let registrations =
532 get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
533 .await
534 .unwrap();
535
536 assert_eq!(registrations.len(), 1);
537 assert_eq!(registrations[0].course_id, course);
538 assert_eq!(registrations[0].course_module_id, course_module.id);
539 assert_eq!(registrations[0].user_id, user);
540 assert_eq!(registrations[0].real_student_number, "12345");
541 }
542
543 #[tokio::test]
544 async fn insert_completions_with_invalid_completion_id_fails() {
545 insert_data!(:tx);
546
547 let registrar_id = crate::study_registry_registrars::insert(
548 tx.as_mut(),
549 PKeyPolicy::Generate,
550 "Test Registrar",
551 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
552 )
553 .await
554 .unwrap();
555
556 let invalid_uuid = Uuid::new_v4(); let registered_completions = vec![RegisteredCompletion {
558 completion_id: invalid_uuid,
559 student_number: "12345".to_string(),
560 registration_date: Utc::now(),
561 }];
562
563 let result = mark_completions_as_registered_to_study_registry(
565 tx.as_mut(),
566 registered_completions,
567 registrar_id,
568 )
569 .await;
570
571 assert!(result.is_err());
572 let error = result.unwrap_err();
573 assert_eq!(*error.error_type(), ModelErrorType::PreconditionFailed);
574 assert!(error.message().contains("Cannot find completion with id"));
575 assert!(error.message().contains(&invalid_uuid.to_string()));
576 }
577
578 #[tokio::test]
579 async fn delete_duplicate_registrations_works() {
580 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
581
582 let registrar_id = crate::study_registry_registrars::insert(
583 tx.as_mut(),
584 PKeyPolicy::Generate,
585 "Test Registrar",
586 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
587 )
588 .await
589 .unwrap();
590
591 let completion = crate::course_module_completions::insert(
592 tx.as_mut(),
593 PKeyPolicy::Generate,
594 &crate::course_module_completions::NewCourseModuleCompletion {
595 course_id: course,
596 course_module_id: course_module.id,
597 user_id: user,
598 completion_date: Utc::now(),
599 completion_registration_attempt_date: None,
600 completion_language: "en-US".to_string(),
601 eligible_for_ects: true,
602 email: "test@example.com".to_string(),
603 grade: Some(5),
604 passed: true,
605 },
606 CourseModuleCompletionGranter::User(user),
607 )
608 .await
609 .unwrap();
610
611 let first_registration = NewCourseModuleCompletionRegisteredToStudyRegistry {
613 course_id: course,
614 course_module_completion_id: completion.id,
615 course_module_id: course_module.id,
616 study_registry_registrar_id: registrar_id,
617 user_id: user,
618 real_student_number: "12345".to_string(),
619 };
620 insert(tx.as_mut(), PKeyPolicy::Generate, &first_registration)
621 .await
622 .unwrap();
623
624 let later_registrations = vec![
626 NewCourseModuleCompletionRegisteredToStudyRegistry {
627 course_id: course,
628 course_module_completion_id: completion.id,
629 course_module_id: course_module.id,
630 study_registry_registrar_id: registrar_id,
631 user_id: user,
632 real_student_number: "67890".to_string(),
633 },
634 NewCourseModuleCompletionRegisteredToStudyRegistry {
635 course_id: course,
636 course_module_completion_id: completion.id,
637 course_module_id: course_module.id,
638 study_registry_registrar_id: registrar_id,
639 user_id: user,
640 real_student_number: "54321".to_string(),
641 },
642 ];
643 insert_bulk(tx.as_mut(), later_registrations).await.unwrap();
644
645 let before_registrations =
646 get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
647 .await
648 .unwrap();
649 assert_eq!(before_registrations.len(), 3);
650
651 let deleted_count = cleanup_duplicates_by_registrar_id_and_completion_ids(
652 tx.as_mut(),
653 registrar_id,
654 &[completion.id],
655 )
656 .await
657 .unwrap();
658 assert_eq!(deleted_count, 2); let after_registrations =
661 get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
662 .await
663 .unwrap();
664 assert_eq!(after_registrations.len(), 1);
665
666 assert!(
668 before_registrations
669 .iter()
670 .any(|registration| registration.id == after_registrations[0].id)
671 );
672 }
673
674 #[tokio::test]
675 async fn delete_duplicate_registrations_with_no_duplicates() {
676 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
677
678 let registrar_id = crate::study_registry_registrars::insert(
679 tx.as_mut(),
680 PKeyPolicy::Generate,
681 "Test Registrar",
682 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
683 )
684 .await
685 .unwrap();
686
687 let completion1 = crate::course_module_completions::insert(
688 tx.as_mut(),
689 PKeyPolicy::Generate,
690 &crate::course_module_completions::NewCourseModuleCompletion {
691 course_id: course,
692 course_module_id: course_module.id,
693 user_id: user,
694 completion_date: Utc::now(),
695 completion_registration_attempt_date: None,
696 completion_language: "en-US".to_string(),
697 eligible_for_ects: true,
698 email: "test1@example.com".to_string(),
699 grade: Some(5),
700 passed: true,
701 },
702 CourseModuleCompletionGranter::User(user),
703 )
704 .await
705 .unwrap();
706
707 let completion2 = crate::course_module_completions::insert(
708 tx.as_mut(),
709 PKeyPolicy::Generate,
710 &crate::course_module_completions::NewCourseModuleCompletion {
711 course_id: course,
712 course_module_id: course_module.id,
713 user_id: user,
714 completion_date: Utc::now(),
715 completion_registration_attempt_date: None,
716 completion_language: "en-US".to_string(),
717 eligible_for_ects: true,
718 email: "test2@example.com".to_string(),
719 grade: Some(4),
720 passed: true,
721 },
722 CourseModuleCompletionGranter::User(user),
723 )
724 .await
725 .unwrap();
726
727 let registrations = vec![
729 NewCourseModuleCompletionRegisteredToStudyRegistry {
730 course_id: course,
731 course_module_completion_id: completion1.id,
732 course_module_id: course_module.id,
733 study_registry_registrar_id: registrar_id,
734 user_id: user,
735 real_student_number: "12345".to_string(),
736 },
737 NewCourseModuleCompletionRegisteredToStudyRegistry {
738 course_id: course,
739 course_module_completion_id: completion2.id,
740 course_module_id: course_module.id,
741 study_registry_registrar_id: registrar_id,
742 user_id: user,
743 real_student_number: "67890".to_string(),
744 },
745 ];
746 insert_bulk(tx.as_mut(), registrations).await.unwrap();
747
748 let before_reg1 =
750 get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
751 .await
752 .unwrap();
753 let before_reg2 =
754 get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
755 .await
756 .unwrap();
757 assert_eq!(before_reg1.len(), 1);
758 assert_eq!(before_reg2.len(), 1);
759
760 let deleted_count = cleanup_duplicates_by_registrar_id_and_completion_ids(
762 tx.as_mut(),
763 registrar_id,
764 &[completion1.id, completion2.id],
765 )
766 .await
767 .unwrap();
768 assert_eq!(deleted_count, 0); let after_reg1 =
772 get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
773 .await
774 .unwrap();
775 let after_reg2 =
776 get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
777 .await
778 .unwrap();
779 assert_eq!(after_reg1.len(), 1);
780 assert_eq!(after_reg2.len(), 1);
781 }
782
783 #[tokio::test]
784 async fn delete_duplicate_registrations_filters_by_completion_id() {
785 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
786
787 let registrar_id = crate::study_registry_registrars::insert(
788 tx.as_mut(),
789 PKeyPolicy::Generate,
790 "Test Registrar",
791 "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
792 )
793 .await
794 .unwrap();
795
796 let completion1 = crate::course_module_completions::insert(
798 tx.as_mut(),
799 PKeyPolicy::Generate,
800 &crate::course_module_completions::NewCourseModuleCompletion {
801 course_id: course,
802 course_module_id: course_module.id,
803 user_id: user,
804 completion_date: Utc::now(),
805 completion_registration_attempt_date: None,
806 completion_language: "en-US".to_string(),
807 eligible_for_ects: true,
808 email: "test1@example.com".to_string(),
809 grade: Some(5),
810 passed: true,
811 },
812 CourseModuleCompletionGranter::User(user),
813 )
814 .await
815 .unwrap();
816
817 let completion2 = crate::course_module_completions::insert(
818 tx.as_mut(),
819 PKeyPolicy::Generate,
820 &crate::course_module_completions::NewCourseModuleCompletion {
821 course_id: course,
822 course_module_id: course_module.id,
823 user_id: user,
824 completion_date: Utc::now(),
825 completion_registration_attempt_date: None,
826 completion_language: "en-US".to_string(),
827 eligible_for_ects: true,
828 email: "test2@example.com".to_string(),
829 grade: Some(5),
830 passed: true,
831 },
832 CourseModuleCompletionGranter::User(user),
833 )
834 .await
835 .unwrap();
836
837 let first_registrations = vec![
839 NewCourseModuleCompletionRegisteredToStudyRegistry {
840 course_id: course,
841 course_module_completion_id: completion1.id,
842 course_module_id: course_module.id,
843 study_registry_registrar_id: registrar_id,
844 user_id: user,
845 real_student_number: "12345-1".to_string(),
846 },
847 NewCourseModuleCompletionRegisteredToStudyRegistry {
848 course_id: course,
849 course_module_completion_id: completion2.id,
850 course_module_id: course_module.id,
851 study_registry_registrar_id: registrar_id,
852 user_id: user,
853 real_student_number: "12345-2".to_string(),
854 },
855 ];
856 insert_bulk(tx.as_mut(), first_registrations).await.unwrap();
857
858 let second_registrations = vec![
860 NewCourseModuleCompletionRegisteredToStudyRegistry {
861 course_id: course,
862 course_module_completion_id: completion1.id,
863 course_module_id: course_module.id,
864 study_registry_registrar_id: registrar_id,
865 user_id: user,
866 real_student_number: "67890-1".to_string(),
867 },
868 NewCourseModuleCompletionRegisteredToStudyRegistry {
869 course_id: course,
870 course_module_completion_id: completion2.id,
871 course_module_id: course_module.id,
872 study_registry_registrar_id: registrar_id,
873 user_id: user,
874 real_student_number: "67890-2".to_string(),
875 },
876 ];
877 insert_bulk(tx.as_mut(), second_registrations)
878 .await
879 .unwrap();
880
881 let before_reg1 =
883 get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
884 .await
885 .unwrap();
886 let before_reg2 =
887 get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
888 .await
889 .unwrap();
890 assert_eq!(before_reg1.len(), 2);
891 assert_eq!(before_reg2.len(), 2);
892
893 let deleted_count = cleanup_duplicates_by_registrar_id_and_completion_ids(
895 tx.as_mut(),
896 registrar_id,
897 &[completion1.id],
898 )
899 .await
900 .unwrap();
901 assert_eq!(deleted_count, 1); let after_reg1 =
905 get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
906 .await
907 .unwrap();
908 let after_reg2 =
909 get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
910 .await
911 .unwrap();
912 assert_eq!(after_reg1.len(), 1);
913 assert_eq!(after_reg2.len(), 2);
914
915 assert!(
917 after_reg1[0].real_student_number == "12345-1"
918 || after_reg1[0].real_student_number == "67890-1"
919 );
920 }
921
922 #[tokio::test]
923 async fn cleanup_duplicates_is_scoped_to_registrar() {
924 insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
925
926 let registrar_a = crate::study_registry_registrars::insert(
927 tx.as_mut(),
928 PKeyPolicy::Generate,
929 "Registrar A",
930 "registrar_a_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
931 )
932 .await
933 .unwrap();
934 let registrar_b = crate::study_registry_registrars::insert(
935 tx.as_mut(),
936 PKeyPolicy::Generate,
937 "Registrar B",
938 "registrar_b_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
939 )
940 .await
941 .unwrap();
942
943 let completion1 = crate::course_module_completions::insert(
944 tx.as_mut(),
945 PKeyPolicy::Generate,
946 &crate::course_module_completions::NewCourseModuleCompletion {
947 course_id: course,
948 course_module_id: course_module.id,
949 user_id: user,
950 completion_date: Utc::now(),
951 completion_registration_attempt_date: None,
952 completion_language: "en-US".to_string(),
953 eligible_for_ects: true,
954 email: "registrar-scope@example.com".to_string(),
955 grade: Some(5),
956 passed: true,
957 },
958 CourseModuleCompletionGranter::User(user),
959 )
960 .await
961 .unwrap();
962
963 insert_bulk(
964 tx.as_mut(),
965 vec![
966 NewCourseModuleCompletionRegisteredToStudyRegistry {
967 course_id: course,
968 course_module_completion_id: completion1.id,
969 course_module_id: course_module.id,
970 study_registry_registrar_id: registrar_a,
971 user_id: user,
972 real_student_number: "a-1".to_string(),
973 },
974 NewCourseModuleCompletionRegisteredToStudyRegistry {
975 course_id: course,
976 course_module_completion_id: completion1.id,
977 course_module_id: course_module.id,
978 study_registry_registrar_id: registrar_a,
979 user_id: user,
980 real_student_number: "a-2".to_string(),
981 },
982 NewCourseModuleCompletionRegisteredToStudyRegistry {
983 course_id: course,
984 course_module_completion_id: completion1.id,
985 course_module_id: course_module.id,
986 study_registry_registrar_id: registrar_b,
987 user_id: user,
988 real_student_number: "b-1".to_string(),
989 },
990 NewCourseModuleCompletionRegisteredToStudyRegistry {
991 course_id: course,
992 course_module_completion_id: completion1.id,
993 course_module_id: course_module.id,
994 study_registry_registrar_id: registrar_b,
995 user_id: user,
996 real_student_number: "b-2".to_string(),
997 },
998 ],
999 )
1000 .await
1001 .unwrap();
1002
1003 let deleted_count = cleanup_duplicates_by_registrar_id_and_completion_ids(
1004 tx.as_mut(),
1005 registrar_a,
1006 &[completion1.id],
1007 )
1008 .await
1009 .unwrap();
1010 assert_eq!(deleted_count, 1);
1011
1012 let remaining_a =
1013 get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_a)
1014 .await
1015 .unwrap();
1016 let remaining_b =
1017 get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_b)
1018 .await
1019 .unwrap();
1020
1021 assert_eq!(remaining_a.len(), 1);
1022 assert_eq!(remaining_b.len(), 2);
1023 }
1024}