headless_lms_models/
course_module_completion_registered_to_study_registries.rs

1use crate::{course_module_completions, prelude::*};
2use rand::Rng;
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 id
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    // Create separate vectors for each column
77    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 id
126        "#,
127        &ids[..],
128        &course_ids[..],
129        &completion_ids[..],
130        &module_ids[..],
131        &registrar_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)]
142/// An object representing that a completion has been registered to a study registry.
143pub struct RegisteredCompletion {
144    /// Id of the completion that was registered to the study registry.
145    pub completion_id: Uuid,
146    /// The student number the completion was registed to.
147    pub student_number: String,
148    /// The registration date that is visible in the study registry for the user.
149    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    // Validate all completions exist before proceeding
165    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: Vec<NewCourseModuleCompletionRegisteredToStudyRegistry> = completions
179        .into_iter()
180        .map(|completion| {
181            let module_completion = completions_by_id.get(&completion.completion_id).unwrap();
182            NewCourseModuleCompletionRegisteredToStudyRegistry {
183                course_id: module_completion.course_id,
184                course_module_completion_id: completion.completion_id,
185                course_module_id: module_completion.course_module_id,
186                study_registry_registrar_id,
187                user_id: module_completion.user_id,
188                real_student_number: completion.student_number,
189            }
190        })
191        .collect();
192
193    let mut tx = conn.begin().await?;
194
195    insert_bulk(&mut tx, new_registrations).await?;
196
197    // We delete duplicates straight away, this way we can still see if the study registry keeps pushing the same completion multiple times
198    // Using a random chance to optimize the performance of the operation
199    let mut rng = rand::rng();
200    let delete_all = rng.random_range(0..50) == 0; // 1 in 50 chance
201
202    if delete_all {
203        delete_all_duplicates(&mut tx).await?;
204    } else {
205        delete_duplicates_for_specific_completions(&mut tx, &ids).await?;
206    }
207
208    tx.commit().await?;
209
210    Ok(())
211}
212
213pub async fn get_by_id(
214    conn: &mut PgConnection,
215    id: Uuid,
216) -> ModelResult<CourseModuleCompletionRegisteredToStudyRegistry> {
217    let res = sqlx::query_as!(
218        CourseModuleCompletionRegisteredToStudyRegistry,
219        "
220SELECT *
221FROM course_module_completion_registered_to_study_registries
222WHERE id = $1
223  AND deleted_at IS NULL
224        ",
225        id,
226    )
227    .fetch_one(conn)
228    .await?;
229    Ok(res)
230}
231
232pub async fn delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
233    sqlx::query!(
234        "
235UPDATE course_module_completion_registered_to_study_registries
236SET deleted_at = now()
237WHERE id = $1
238        ",
239        id
240    )
241    .execute(conn)
242    .await?;
243    Ok(())
244}
245
246/// Get the number of students that have completed the course
247pub async fn get_count_of_distinct_users_with_registrations_by_course_id(
248    conn: &mut PgConnection,
249    course_id: Uuid,
250) -> ModelResult<i64> {
251    let res = sqlx::query!(
252        "
253SELECT COUNT(DISTINCT user_id) as count
254FROM course_module_completion_registered_to_study_registries
255WHERE course_id = $1
256  AND deleted_at IS NULL
257",
258        course_id,
259    )
260    .fetch_one(conn)
261    .await?;
262    Ok(res.count.unwrap_or(0))
263}
264
265pub async fn get_by_completion_id_and_registrar_id(
266    conn: &mut PgConnection,
267    completion_id: Uuid,
268    study_registry_registrar_id: Uuid,
269) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
270    let registrations = sqlx::query_as!(
271        CourseModuleCompletionRegisteredToStudyRegistry,
272        r#"
273        SELECT *
274        FROM course_module_completion_registered_to_study_registries
275        WHERE course_module_completion_id = $1 AND study_registry_registrar_id = $2
276        AND deleted_at IS NULL
277        "#,
278        completion_id,
279        study_registry_registrar_id
280    )
281    .fetch_all(conn)
282    .await?;
283
284    Ok(registrations)
285}
286
287async fn delete_duplicates_for_specific_completions(
288    conn: &mut PgConnection,
289    completion_ids: &[Uuid],
290) -> ModelResult<i64> {
291    let res = sqlx::query!(
292        r#"
293WITH duplicate_rows AS (
294  SELECT id,
295    ROW_NUMBER() OVER (
296      PARTITION BY course_module_completion_id
297      ORDER BY created_at ASC -- Keep the oldest, delete the rest
298    ) AS rn
299  FROM course_module_completion_registered_to_study_registries
300  WHERE deleted_at IS NULL
301    AND course_module_completion_id = ANY($1)
302)
303UPDATE course_module_completion_registered_to_study_registries
304SET deleted_at = NOW()
305WHERE id IN (
306    SELECT id
307    FROM duplicate_rows
308    WHERE rn > 1
309  )
310RETURNING id
311        "#,
312        completion_ids,
313    )
314    .fetch_all(conn)
315    .await?;
316
317    Ok(res.len() as i64)
318}
319
320async fn delete_all_duplicates(conn: &mut PgConnection) -> ModelResult<i64> {
321    let res = sqlx::query!(
322        r#"
323WITH duplicate_rows AS (
324  SELECT id,
325    ROW_NUMBER() OVER (
326      PARTITION BY course_module_completion_id
327      ORDER BY created_at ASC -- Keep the oldest, delete the rest
328    ) AS rn
329  FROM course_module_completion_registered_to_study_registries
330  WHERE deleted_at IS NULL
331)
332UPDATE course_module_completion_registered_to_study_registries
333SET deleted_at = NOW()
334WHERE id IN (
335    SELECT id
336    FROM duplicate_rows
337    WHERE rn > 1
338  )
339RETURNING id
340        "#
341    )
342    .fetch_all(conn)
343    .await?;
344
345    Ok(res.len() as i64)
346}
347
348#[cfg(test)]
349mod test {
350    use super::*;
351    use crate::{course_module_completions::CourseModuleCompletionGranter, test_helper::*};
352
353    #[tokio::test]
354    async fn bulk_insert_works() {
355        insert_data!(:tx, :user, :org, :course, :instance, :course_module);
356
357        let registrar_id = crate::study_registry_registrars::insert(
358            tx.as_mut(),
359            PKeyPolicy::Generate,
360            "Test Registrar",
361            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
362        )
363        .await
364        .unwrap();
365
366        let new_completion_id = crate::course_module_completions::insert(
367            tx.as_mut(),
368            PKeyPolicy::Generate,
369            &crate::course_module_completions::NewCourseModuleCompletion {
370                course_id: course,
371                course_module_id: course_module.id,
372                user_id: user,
373                course_instance_id: instance.id,
374                completion_date: Utc::now(),
375                completion_registration_attempt_date: None,
376                completion_language: "en-US".to_string(),
377                eligible_for_ects: true,
378                email: "test@example.com".to_string(),
379                grade: Some(10),
380                passed: true,
381            },
382            CourseModuleCompletionGranter::User(user),
383        )
384        .await
385        .unwrap();
386
387        let registrations = vec![
388            NewCourseModuleCompletionRegisteredToStudyRegistry {
389                course_id: course,
390                course_module_completion_id: new_completion_id.id,
391                course_module_id: course_module.id,
392                study_registry_registrar_id: registrar_id,
393                user_id: user,
394                real_student_number: "12345".to_string(),
395            },
396            NewCourseModuleCompletionRegisteredToStudyRegistry {
397                course_id: course,
398                course_module_completion_id: new_completion_id.id,
399                course_module_id: course_module.id,
400                study_registry_registrar_id: registrar_id,
401                user_id: user,
402                real_student_number: "67890".to_string(),
403            },
404        ];
405
406        let inserted_ids = insert_bulk(tx.as_mut(), registrations).await.unwrap();
407        assert_eq!(inserted_ids.len(), 2);
408
409        // Verify both records were inserted correctly
410        for id in inserted_ids {
411            let registration = get_by_id(tx.as_mut(), id).await.unwrap();
412            assert_eq!(registration.course_id, course);
413            assert_eq!(
414                registration.course_module_completion_id,
415                new_completion_id.id
416            );
417            assert_eq!(registration.course_module_id, course_module.id);
418            assert_eq!(registration.study_registry_registrar_id, registrar_id);
419            assert_eq!(registration.user_id, user);
420        }
421    }
422
423    #[tokio::test]
424    async fn bulk_insert_empty_vec_works() {
425        insert_data!(:tx);
426
427        let empty_vec = vec![];
428        let result = insert_bulk(tx.as_mut(), empty_vec).await.unwrap();
429        assert!(result.is_empty());
430    }
431
432    #[tokio::test]
433    async fn insert_completions_works() {
434        insert_data!(:tx, :user, :org, :course, :instance, :course_module);
435
436        let registrar_id = crate::study_registry_registrars::insert(
437            tx.as_mut(),
438            PKeyPolicy::Generate,
439            "Test Registrar",
440            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
441        )
442        .await
443        .unwrap();
444
445        let completion = crate::course_module_completions::insert(
446            tx.as_mut(),
447            PKeyPolicy::Generate,
448            &crate::course_module_completions::NewCourseModuleCompletion {
449                course_id: course,
450                course_module_id: course_module.id,
451                user_id: user,
452                course_instance_id: instance.id,
453                completion_date: Utc::now(),
454                completion_registration_attempt_date: None,
455                completion_language: "en-US".to_string(),
456                eligible_for_ects: true,
457                email: "test@example.com".to_string(),
458                grade: Some(5),
459                passed: true,
460            },
461            CourseModuleCompletionGranter::User(user),
462        )
463        .await
464        .unwrap();
465
466        let registered_completions = vec![RegisteredCompletion {
467            completion_id: completion.id,
468            student_number: "12345".to_string(),
469            registration_date: Utc::now(),
470        }];
471
472        mark_completions_as_registered_to_study_registry(
473            tx.as_mut(),
474            registered_completions,
475            registrar_id,
476        )
477        .await
478        .unwrap();
479
480        let registrations =
481            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
482                .await
483                .unwrap();
484
485        assert_eq!(registrations.len(), 1);
486        assert_eq!(registrations[0].course_id, course);
487        assert_eq!(registrations[0].course_module_id, course_module.id);
488        assert_eq!(registrations[0].user_id, user);
489        assert_eq!(registrations[0].real_student_number, "12345");
490    }
491
492    #[tokio::test]
493    async fn insert_completions_with_invalid_completion_id_fails() {
494        insert_data!(:tx);
495
496        let registrar_id = crate::study_registry_registrars::insert(
497            tx.as_mut(),
498            PKeyPolicy::Generate,
499            "Test Registrar",
500            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
501        )
502        .await
503        .unwrap();
504
505        let invalid_uuid = Uuid::new_v4(); // This UUID doesn't correspond to any completion
506        let registered_completions = vec![RegisteredCompletion {
507            completion_id: invalid_uuid,
508            student_number: "12345".to_string(),
509            registration_date: Utc::now(),
510        }];
511
512        // Attempt to insert the completions should fail
513        let result = mark_completions_as_registered_to_study_registry(
514            tx.as_mut(),
515            registered_completions,
516            registrar_id,
517        )
518        .await;
519
520        assert!(result.is_err());
521        let error = result.unwrap_err();
522        assert_eq!(*error.error_type(), ModelErrorType::PreconditionFailed);
523        assert!(error.message().contains("Cannot find completion with id"));
524        assert!(error.message().contains(&invalid_uuid.to_string()));
525    }
526
527    #[tokio::test]
528    async fn delete_duplicate_registrations_works() {
529        insert_data!(:tx, :user, :org, :course, :instance, :course_module);
530
531        let registrar_id = crate::study_registry_registrars::insert(
532            tx.as_mut(),
533            PKeyPolicy::Generate,
534            "Test Registrar",
535            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
536        )
537        .await
538        .unwrap();
539
540        let completion = crate::course_module_completions::insert(
541            tx.as_mut(),
542            PKeyPolicy::Generate,
543            &crate::course_module_completions::NewCourseModuleCompletion {
544                course_id: course,
545                course_module_id: course_module.id,
546                user_id: user,
547                course_instance_id: instance.id,
548                completion_date: Utc::now(),
549                completion_registration_attempt_date: None,
550                completion_language: "en-US".to_string(),
551                eligible_for_ects: true,
552                email: "test@example.com".to_string(),
553                grade: Some(5),
554                passed: true,
555            },
556            CourseModuleCompletionGranter::User(user),
557        )
558        .await
559        .unwrap();
560
561        // Create first registration
562        let first_registration = NewCourseModuleCompletionRegisteredToStudyRegistry {
563            course_id: course,
564            course_module_completion_id: completion.id,
565            course_module_id: course_module.id,
566            study_registry_registrar_id: registrar_id,
567            user_id: user,
568            real_student_number: "12345".to_string(),
569        };
570        let first_id = insert(tx.as_mut(), PKeyPolicy::Generate, &first_registration)
571            .await
572            .unwrap();
573
574        // Create additional registrations in a separate bulk insert so that we get a different created_at timestamp
575        let later_registrations = vec![
576            NewCourseModuleCompletionRegisteredToStudyRegistry {
577                course_id: course,
578                course_module_completion_id: completion.id,
579                course_module_id: course_module.id,
580                study_registry_registrar_id: registrar_id,
581                user_id: user,
582                real_student_number: "67890".to_string(),
583            },
584            NewCourseModuleCompletionRegisteredToStudyRegistry {
585                course_id: course,
586                course_module_completion_id: completion.id,
587                course_module_id: course_module.id,
588                study_registry_registrar_id: registrar_id,
589                user_id: user,
590                real_student_number: "54321".to_string(),
591            },
592        ];
593        insert_bulk(tx.as_mut(), later_registrations).await.unwrap();
594
595        let before_registrations =
596            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
597                .await
598                .unwrap();
599        assert_eq!(before_registrations.len(), 3);
600
601        let deleted_count =
602            delete_duplicates_for_specific_completions(tx.as_mut(), &[completion.id])
603                .await
604                .unwrap();
605        assert_eq!(deleted_count, 2); // Should delete 2 out of 3 registrations
606
607        let after_registrations =
608            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
609                .await
610                .unwrap();
611        assert_eq!(after_registrations.len(), 1);
612
613        // The remaining registration should be the first one we created
614        assert_eq!(after_registrations[0].id, first_id);
615        assert_eq!(after_registrations[0].real_student_number, "12345");
616    }
617
618    #[tokio::test]
619    async fn delete_duplicate_registrations_with_no_duplicates() {
620        insert_data!(:tx, :user, :org, :course, :instance, :course_module);
621
622        let registrar_id = crate::study_registry_registrars::insert(
623            tx.as_mut(),
624            PKeyPolicy::Generate,
625            "Test Registrar",
626            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
627        )
628        .await
629        .unwrap();
630
631        let completion1 = crate::course_module_completions::insert(
632            tx.as_mut(),
633            PKeyPolicy::Generate,
634            &crate::course_module_completions::NewCourseModuleCompletion {
635                course_id: course,
636                course_module_id: course_module.id,
637                user_id: user,
638                course_instance_id: instance.id,
639                completion_date: Utc::now(),
640                completion_registration_attempt_date: None,
641                completion_language: "en-US".to_string(),
642                eligible_for_ects: true,
643                email: "test1@example.com".to_string(),
644                grade: Some(5),
645                passed: true,
646            },
647            CourseModuleCompletionGranter::User(user),
648        )
649        .await
650        .unwrap();
651
652        let completion2 = crate::course_module_completions::insert(
653            tx.as_mut(),
654            PKeyPolicy::Generate,
655            &crate::course_module_completions::NewCourseModuleCompletion {
656                course_id: course,
657                course_module_id: course_module.id,
658                user_id: user,
659                course_instance_id: instance.id,
660                completion_date: Utc::now(),
661                completion_registration_attempt_date: None,
662                completion_language: "en-US".to_string(),
663                eligible_for_ects: true,
664                email: "test2@example.com".to_string(),
665                grade: Some(4),
666                passed: true,
667            },
668            CourseModuleCompletionGranter::User(user),
669        )
670        .await
671        .unwrap();
672
673        // Create one registration for each completion (no duplicates)
674        let registrations = vec![
675            NewCourseModuleCompletionRegisteredToStudyRegistry {
676                course_id: course,
677                course_module_completion_id: completion1.id,
678                course_module_id: course_module.id,
679                study_registry_registrar_id: registrar_id,
680                user_id: user,
681                real_student_number: "12345".to_string(),
682            },
683            NewCourseModuleCompletionRegisteredToStudyRegistry {
684                course_id: course,
685                course_module_completion_id: completion2.id,
686                course_module_id: course_module.id,
687                study_registry_registrar_id: registrar_id,
688                user_id: user,
689                real_student_number: "67890".to_string(),
690            },
691        ];
692        insert_bulk(tx.as_mut(), registrations).await.unwrap();
693
694        // Verify we have 1 registration for each completion
695        let before_reg1 =
696            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
697                .await
698                .unwrap();
699        let before_reg2 =
700            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
701                .await
702                .unwrap();
703        assert_eq!(before_reg1.len(), 1);
704        assert_eq!(before_reg2.len(), 1);
705
706        // Delete duplicate registrations
707        let deleted_count = delete_all_duplicates(tx.as_mut()).await.unwrap();
708        assert_eq!(deleted_count, 0); // Should delete 0 registrations as there are no duplicates
709
710        // Verify both registrations still exist
711        let after_reg1 =
712            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
713                .await
714                .unwrap();
715        let after_reg2 =
716            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
717                .await
718                .unwrap();
719        assert_eq!(after_reg1.len(), 1);
720        assert_eq!(after_reg2.len(), 1);
721    }
722
723    #[tokio::test]
724    async fn delete_duplicate_registrations_filters_by_completion_id() {
725        insert_data!(:tx, :user, :org, :course, :instance, :course_module);
726
727        let registrar_id = crate::study_registry_registrars::insert(
728            tx.as_mut(),
729            PKeyPolicy::Generate,
730            "Test Registrar",
731            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
732        )
733        .await
734        .unwrap();
735
736        // Create two completions
737        let completion1 = crate::course_module_completions::insert(
738            tx.as_mut(),
739            PKeyPolicy::Generate,
740            &crate::course_module_completions::NewCourseModuleCompletion {
741                course_id: course,
742                course_module_id: course_module.id,
743                user_id: user,
744                course_instance_id: instance.id,
745                completion_date: Utc::now(),
746                completion_registration_attempt_date: None,
747                completion_language: "en-US".to_string(),
748                eligible_for_ects: true,
749                email: "test1@example.com".to_string(),
750                grade: Some(5),
751                passed: true,
752            },
753            CourseModuleCompletionGranter::User(user),
754        )
755        .await
756        .unwrap();
757
758        let completion2 = crate::course_module_completions::insert(
759            tx.as_mut(),
760            PKeyPolicy::Generate,
761            &crate::course_module_completions::NewCourseModuleCompletion {
762                course_id: course,
763                course_module_id: course_module.id,
764                user_id: user,
765                course_instance_id: instance.id,
766                completion_date: Utc::now(),
767                completion_registration_attempt_date: None,
768                completion_language: "en-US".to_string(),
769                eligible_for_ects: true,
770                email: "test2@example.com".to_string(),
771                grade: Some(5),
772                passed: true,
773            },
774            CourseModuleCompletionGranter::User(user),
775        )
776        .await
777        .unwrap();
778
779        // Create first registrations (these should be kept)
780        let first_registrations = vec![
781            NewCourseModuleCompletionRegisteredToStudyRegistry {
782                course_id: course,
783                course_module_completion_id: completion1.id,
784                course_module_id: course_module.id,
785                study_registry_registrar_id: registrar_id,
786                user_id: user,
787                real_student_number: "12345-1".to_string(),
788            },
789            NewCourseModuleCompletionRegisteredToStudyRegistry {
790                course_id: course,
791                course_module_completion_id: completion2.id,
792                course_module_id: course_module.id,
793                study_registry_registrar_id: registrar_id,
794                user_id: user,
795                real_student_number: "12345-2".to_string(),
796            },
797        ];
798        insert_bulk(tx.as_mut(), first_registrations).await.unwrap();
799
800        // Create second registrations (these should be deleted as duplicates)
801        let second_registrations = vec![
802            NewCourseModuleCompletionRegisteredToStudyRegistry {
803                course_id: course,
804                course_module_completion_id: completion1.id,
805                course_module_id: course_module.id,
806                study_registry_registrar_id: registrar_id,
807                user_id: user,
808                real_student_number: "67890-1".to_string(),
809            },
810            NewCourseModuleCompletionRegisteredToStudyRegistry {
811                course_id: course,
812                course_module_completion_id: completion2.id,
813                course_module_id: course_module.id,
814                study_registry_registrar_id: registrar_id,
815                user_id: user,
816                real_student_number: "67890-2".to_string(),
817            },
818        ];
819        insert_bulk(tx.as_mut(), second_registrations)
820            .await
821            .unwrap();
822
823        // Verify we have 2 registrations for each completion
824        let before_reg1 =
825            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
826                .await
827                .unwrap();
828        let before_reg2 =
829            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
830                .await
831                .unwrap();
832        assert_eq!(before_reg1.len(), 2);
833        assert_eq!(before_reg2.len(), 2);
834
835        // Delete duplicate registrations only for completion1
836        let deleted_count =
837            delete_duplicates_for_specific_completions(tx.as_mut(), &[completion1.id])
838                .await
839                .unwrap();
840        assert_eq!(deleted_count, 1); // Should delete 1 duplicate for completion1
841
842        // Verify completion1 now has 1 registration and completion2 still has 2
843        let after_reg1 =
844            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
845                .await
846                .unwrap();
847        let after_reg2 =
848            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
849                .await
850                .unwrap();
851        assert_eq!(after_reg1.len(), 1);
852        assert_eq!(after_reg2.len(), 2);
853
854        // The registration for completion1 should be the first one we created
855        assert_eq!(after_reg1[0].real_student_number, "12345-1");
856    }
857}