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: _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                completion_date: Utc::now(),
374                completion_registration_attempt_date: None,
375                completion_language: "en-US".to_string(),
376                eligible_for_ects: true,
377                email: "test@example.com".to_string(),
378                grade: Some(10),
379                passed: true,
380            },
381            CourseModuleCompletionGranter::User(user),
382        )
383        .await
384        .unwrap();
385
386        let registrations = vec![
387            NewCourseModuleCompletionRegisteredToStudyRegistry {
388                course_id: course,
389                course_module_completion_id: new_completion_id.id,
390                course_module_id: course_module.id,
391                study_registry_registrar_id: registrar_id,
392                user_id: user,
393                real_student_number: "12345".to_string(),
394            },
395            NewCourseModuleCompletionRegisteredToStudyRegistry {
396                course_id: course,
397                course_module_completion_id: new_completion_id.id,
398                course_module_id: course_module.id,
399                study_registry_registrar_id: registrar_id,
400                user_id: user,
401                real_student_number: "67890".to_string(),
402            },
403        ];
404
405        let inserted_ids = insert_bulk(tx.as_mut(), registrations).await.unwrap();
406        assert_eq!(inserted_ids.len(), 2);
407
408        // Verify both records were inserted correctly
409        for id in inserted_ids {
410            let registration = get_by_id(tx.as_mut(), id).await.unwrap();
411            assert_eq!(registration.course_id, course);
412            assert_eq!(
413                registration.course_module_completion_id,
414                new_completion_id.id
415            );
416            assert_eq!(registration.course_module_id, course_module.id);
417            assert_eq!(registration.study_registry_registrar_id, registrar_id);
418            assert_eq!(registration.user_id, user);
419        }
420    }
421
422    #[tokio::test]
423    async fn bulk_insert_empty_vec_works() {
424        insert_data!(:tx);
425
426        let empty_vec = vec![];
427        let result = insert_bulk(tx.as_mut(), empty_vec).await.unwrap();
428        assert!(result.is_empty());
429    }
430
431    #[tokio::test]
432    async fn insert_completions_works() {
433        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
434
435        let registrar_id = crate::study_registry_registrars::insert(
436            tx.as_mut(),
437            PKeyPolicy::Generate,
438            "Test Registrar",
439            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
440        )
441        .await
442        .unwrap();
443
444        let completion = crate::course_module_completions::insert(
445            tx.as_mut(),
446            PKeyPolicy::Generate,
447            &crate::course_module_completions::NewCourseModuleCompletion {
448                course_id: course,
449                course_module_id: course_module.id,
450                user_id: user,
451                completion_date: Utc::now(),
452                completion_registration_attempt_date: None,
453                completion_language: "en-US".to_string(),
454                eligible_for_ects: true,
455                email: "test@example.com".to_string(),
456                grade: Some(5),
457                passed: true,
458            },
459            CourseModuleCompletionGranter::User(user),
460        )
461        .await
462        .unwrap();
463
464        let registered_completions = vec![RegisteredCompletion {
465            completion_id: completion.id,
466            student_number: "12345".to_string(),
467            registration_date: Utc::now(),
468        }];
469
470        mark_completions_as_registered_to_study_registry(
471            tx.as_mut(),
472            registered_completions,
473            registrar_id,
474        )
475        .await
476        .unwrap();
477
478        let registrations =
479            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
480                .await
481                .unwrap();
482
483        assert_eq!(registrations.len(), 1);
484        assert_eq!(registrations[0].course_id, course);
485        assert_eq!(registrations[0].course_module_id, course_module.id);
486        assert_eq!(registrations[0].user_id, user);
487        assert_eq!(registrations[0].real_student_number, "12345");
488    }
489
490    #[tokio::test]
491    async fn insert_completions_with_invalid_completion_id_fails() {
492        insert_data!(:tx);
493
494        let registrar_id = crate::study_registry_registrars::insert(
495            tx.as_mut(),
496            PKeyPolicy::Generate,
497            "Test Registrar",
498            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
499        )
500        .await
501        .unwrap();
502
503        let invalid_uuid = Uuid::new_v4(); // This UUID doesn't correspond to any completion
504        let registered_completions = vec![RegisteredCompletion {
505            completion_id: invalid_uuid,
506            student_number: "12345".to_string(),
507            registration_date: Utc::now(),
508        }];
509
510        // Attempt to insert the completions should fail
511        let result = mark_completions_as_registered_to_study_registry(
512            tx.as_mut(),
513            registered_completions,
514            registrar_id,
515        )
516        .await;
517
518        assert!(result.is_err());
519        let error = result.unwrap_err();
520        assert_eq!(*error.error_type(), ModelErrorType::PreconditionFailed);
521        assert!(error.message().contains("Cannot find completion with id"));
522        assert!(error.message().contains(&invalid_uuid.to_string()));
523    }
524
525    #[tokio::test]
526    async fn delete_duplicate_registrations_works() {
527        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
528
529        let registrar_id = crate::study_registry_registrars::insert(
530            tx.as_mut(),
531            PKeyPolicy::Generate,
532            "Test Registrar",
533            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
534        )
535        .await
536        .unwrap();
537
538        let completion = crate::course_module_completions::insert(
539            tx.as_mut(),
540            PKeyPolicy::Generate,
541            &crate::course_module_completions::NewCourseModuleCompletion {
542                course_id: course,
543                course_module_id: course_module.id,
544                user_id: user,
545                completion_date: Utc::now(),
546                completion_registration_attempt_date: None,
547                completion_language: "en-US".to_string(),
548                eligible_for_ects: true,
549                email: "test@example.com".to_string(),
550                grade: Some(5),
551                passed: true,
552            },
553            CourseModuleCompletionGranter::User(user),
554        )
555        .await
556        .unwrap();
557
558        // Create first registration
559        let first_registration = NewCourseModuleCompletionRegisteredToStudyRegistry {
560            course_id: course,
561            course_module_completion_id: completion.id,
562            course_module_id: course_module.id,
563            study_registry_registrar_id: registrar_id,
564            user_id: user,
565            real_student_number: "12345".to_string(),
566        };
567        let first_id = insert(tx.as_mut(), PKeyPolicy::Generate, &first_registration)
568            .await
569            .unwrap();
570
571        // Create additional registrations in a separate bulk insert so that we get a different created_at timestamp
572        let later_registrations = vec![
573            NewCourseModuleCompletionRegisteredToStudyRegistry {
574                course_id: course,
575                course_module_completion_id: completion.id,
576                course_module_id: course_module.id,
577                study_registry_registrar_id: registrar_id,
578                user_id: user,
579                real_student_number: "67890".to_string(),
580            },
581            NewCourseModuleCompletionRegisteredToStudyRegistry {
582                course_id: course,
583                course_module_completion_id: completion.id,
584                course_module_id: course_module.id,
585                study_registry_registrar_id: registrar_id,
586                user_id: user,
587                real_student_number: "54321".to_string(),
588            },
589        ];
590        insert_bulk(tx.as_mut(), later_registrations).await.unwrap();
591
592        let before_registrations =
593            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
594                .await
595                .unwrap();
596        assert_eq!(before_registrations.len(), 3);
597
598        let deleted_count =
599            delete_duplicates_for_specific_completions(tx.as_mut(), &[completion.id])
600                .await
601                .unwrap();
602        assert_eq!(deleted_count, 2); // Should delete 2 out of 3 registrations
603
604        let after_registrations =
605            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
606                .await
607                .unwrap();
608        assert_eq!(after_registrations.len(), 1);
609
610        // The remaining registration should be the first one we created
611        assert_eq!(after_registrations[0].id, first_id);
612        assert_eq!(after_registrations[0].real_student_number, "12345");
613    }
614
615    #[tokio::test]
616    async fn delete_duplicate_registrations_with_no_duplicates() {
617        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
618
619        let registrar_id = crate::study_registry_registrars::insert(
620            tx.as_mut(),
621            PKeyPolicy::Generate,
622            "Test Registrar",
623            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
624        )
625        .await
626        .unwrap();
627
628        let completion1 = crate::course_module_completions::insert(
629            tx.as_mut(),
630            PKeyPolicy::Generate,
631            &crate::course_module_completions::NewCourseModuleCompletion {
632                course_id: course,
633                course_module_id: course_module.id,
634                user_id: user,
635                completion_date: Utc::now(),
636                completion_registration_attempt_date: None,
637                completion_language: "en-US".to_string(),
638                eligible_for_ects: true,
639                email: "test1@example.com".to_string(),
640                grade: Some(5),
641                passed: true,
642            },
643            CourseModuleCompletionGranter::User(user),
644        )
645        .await
646        .unwrap();
647
648        let completion2 = crate::course_module_completions::insert(
649            tx.as_mut(),
650            PKeyPolicy::Generate,
651            &crate::course_module_completions::NewCourseModuleCompletion {
652                course_id: course,
653                course_module_id: course_module.id,
654                user_id: user,
655                completion_date: Utc::now(),
656                completion_registration_attempt_date: None,
657                completion_language: "en-US".to_string(),
658                eligible_for_ects: true,
659                email: "test2@example.com".to_string(),
660                grade: Some(4),
661                passed: true,
662            },
663            CourseModuleCompletionGranter::User(user),
664        )
665        .await
666        .unwrap();
667
668        // Create one registration for each completion (no duplicates)
669        let registrations = vec![
670            NewCourseModuleCompletionRegisteredToStudyRegistry {
671                course_id: course,
672                course_module_completion_id: completion1.id,
673                course_module_id: course_module.id,
674                study_registry_registrar_id: registrar_id,
675                user_id: user,
676                real_student_number: "12345".to_string(),
677            },
678            NewCourseModuleCompletionRegisteredToStudyRegistry {
679                course_id: course,
680                course_module_completion_id: completion2.id,
681                course_module_id: course_module.id,
682                study_registry_registrar_id: registrar_id,
683                user_id: user,
684                real_student_number: "67890".to_string(),
685            },
686        ];
687        insert_bulk(tx.as_mut(), registrations).await.unwrap();
688
689        // Verify we have 1 registration for each completion
690        let before_reg1 =
691            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
692                .await
693                .unwrap();
694        let before_reg2 =
695            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
696                .await
697                .unwrap();
698        assert_eq!(before_reg1.len(), 1);
699        assert_eq!(before_reg2.len(), 1);
700
701        // Delete duplicate registrations
702        let deleted_count = delete_all_duplicates(tx.as_mut()).await.unwrap();
703        assert_eq!(deleted_count, 0); // Should delete 0 registrations as there are no duplicates
704
705        // Verify both registrations still exist
706        let after_reg1 =
707            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
708                .await
709                .unwrap();
710        let after_reg2 =
711            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
712                .await
713                .unwrap();
714        assert_eq!(after_reg1.len(), 1);
715        assert_eq!(after_reg2.len(), 1);
716    }
717
718    #[tokio::test]
719    async fn delete_duplicate_registrations_filters_by_completion_id() {
720        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
721
722        let registrar_id = crate::study_registry_registrars::insert(
723            tx.as_mut(),
724            PKeyPolicy::Generate,
725            "Test Registrar",
726            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
727        )
728        .await
729        .unwrap();
730
731        // Create two completions
732        let completion1 = crate::course_module_completions::insert(
733            tx.as_mut(),
734            PKeyPolicy::Generate,
735            &crate::course_module_completions::NewCourseModuleCompletion {
736                course_id: course,
737                course_module_id: course_module.id,
738                user_id: user,
739                completion_date: Utc::now(),
740                completion_registration_attempt_date: None,
741                completion_language: "en-US".to_string(),
742                eligible_for_ects: true,
743                email: "test1@example.com".to_string(),
744                grade: Some(5),
745                passed: true,
746            },
747            CourseModuleCompletionGranter::User(user),
748        )
749        .await
750        .unwrap();
751
752        let completion2 = crate::course_module_completions::insert(
753            tx.as_mut(),
754            PKeyPolicy::Generate,
755            &crate::course_module_completions::NewCourseModuleCompletion {
756                course_id: course,
757                course_module_id: course_module.id,
758                user_id: user,
759                completion_date: Utc::now(),
760                completion_registration_attempt_date: None,
761                completion_language: "en-US".to_string(),
762                eligible_for_ects: true,
763                email: "test2@example.com".to_string(),
764                grade: Some(5),
765                passed: true,
766            },
767            CourseModuleCompletionGranter::User(user),
768        )
769        .await
770        .unwrap();
771
772        // Create first registrations (these should be kept)
773        let first_registrations = vec![
774            NewCourseModuleCompletionRegisteredToStudyRegistry {
775                course_id: course,
776                course_module_completion_id: completion1.id,
777                course_module_id: course_module.id,
778                study_registry_registrar_id: registrar_id,
779                user_id: user,
780                real_student_number: "12345-1".to_string(),
781            },
782            NewCourseModuleCompletionRegisteredToStudyRegistry {
783                course_id: course,
784                course_module_completion_id: completion2.id,
785                course_module_id: course_module.id,
786                study_registry_registrar_id: registrar_id,
787                user_id: user,
788                real_student_number: "12345-2".to_string(),
789            },
790        ];
791        insert_bulk(tx.as_mut(), first_registrations).await.unwrap();
792
793        // Create second registrations (these should be deleted as duplicates)
794        let second_registrations = vec![
795            NewCourseModuleCompletionRegisteredToStudyRegistry {
796                course_id: course,
797                course_module_completion_id: completion1.id,
798                course_module_id: course_module.id,
799                study_registry_registrar_id: registrar_id,
800                user_id: user,
801                real_student_number: "67890-1".to_string(),
802            },
803            NewCourseModuleCompletionRegisteredToStudyRegistry {
804                course_id: course,
805                course_module_completion_id: completion2.id,
806                course_module_id: course_module.id,
807                study_registry_registrar_id: registrar_id,
808                user_id: user,
809                real_student_number: "67890-2".to_string(),
810            },
811        ];
812        insert_bulk(tx.as_mut(), second_registrations)
813            .await
814            .unwrap();
815
816        // Verify we have 2 registrations for each completion
817        let before_reg1 =
818            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
819                .await
820                .unwrap();
821        let before_reg2 =
822            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
823                .await
824                .unwrap();
825        assert_eq!(before_reg1.len(), 2);
826        assert_eq!(before_reg2.len(), 2);
827
828        // Delete duplicate registrations only for completion1
829        let deleted_count =
830            delete_duplicates_for_specific_completions(tx.as_mut(), &[completion1.id])
831                .await
832                .unwrap();
833        assert_eq!(deleted_count, 1); // Should delete 1 duplicate for completion1
834
835        // Verify completion1 now has 1 registration and completion2 still has 2
836        let after_reg1 =
837            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
838                .await
839                .unwrap();
840        let after_reg2 =
841            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
842                .await
843                .unwrap();
844        assert_eq!(after_reg1.len(), 1);
845        assert_eq!(after_reg2.len(), 2);
846
847        // The registration for completion1 should be the first one we created
848        assert_eq!(after_reg1[0].real_student_number, "12345-1");
849    }
850}