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
238AND deleted_at IS NULL
239        ",
240        id
241    )
242    .execute(conn)
243    .await?;
244    Ok(())
245}
246
247/// Get the number of students that have completed the course
248pub async fn get_count_of_distinct_users_with_registrations_by_course_id(
249    conn: &mut PgConnection,
250    course_id: Uuid,
251) -> ModelResult<i64> {
252    let res = sqlx::query!(
253        "
254SELECT COUNT(DISTINCT user_id) as count
255FROM course_module_completion_registered_to_study_registries
256WHERE course_id = $1
257  AND deleted_at IS NULL
258",
259        course_id,
260    )
261    .fetch_one(conn)
262    .await?;
263    Ok(res.count.unwrap_or(0))
264}
265
266pub async fn get_by_completion_id_and_registrar_id(
267    conn: &mut PgConnection,
268    completion_id: Uuid,
269    study_registry_registrar_id: Uuid,
270) -> ModelResult<Vec<CourseModuleCompletionRegisteredToStudyRegistry>> {
271    let registrations = sqlx::query_as!(
272        CourseModuleCompletionRegisteredToStudyRegistry,
273        r#"
274        SELECT *
275        FROM course_module_completion_registered_to_study_registries
276        WHERE course_module_completion_id = $1 AND study_registry_registrar_id = $2
277        AND deleted_at IS NULL
278        "#,
279        completion_id,
280        study_registry_registrar_id
281    )
282    .fetch_all(conn)
283    .await?;
284
285    Ok(registrations)
286}
287
288async fn delete_duplicates_for_specific_completions(
289    conn: &mut PgConnection,
290    completion_ids: &[Uuid],
291) -> ModelResult<i64> {
292    let res = sqlx::query!(
293        r#"
294WITH duplicate_rows AS (
295  SELECT id,
296    ROW_NUMBER() OVER (
297      PARTITION BY course_module_completion_id
298      ORDER BY created_at ASC -- Keep the oldest, delete the rest
299    ) AS rn
300  FROM course_module_completion_registered_to_study_registries
301  WHERE deleted_at IS NULL
302    AND course_module_completion_id = ANY($1)
303)
304UPDATE course_module_completion_registered_to_study_registries
305SET deleted_at = NOW()
306WHERE id IN (
307    SELECT id
308    FROM duplicate_rows
309    WHERE rn > 1
310  )
311RETURNING id
312        "#,
313        completion_ids,
314    )
315    .fetch_all(conn)
316    .await?;
317
318    Ok(res.len() as i64)
319}
320
321async fn delete_all_duplicates(conn: &mut PgConnection) -> ModelResult<i64> {
322    let res = sqlx::query!(
323        r#"
324WITH duplicate_rows AS (
325  SELECT id,
326    ROW_NUMBER() OVER (
327      PARTITION BY course_module_completion_id
328      ORDER BY created_at ASC -- Keep the oldest, delete the rest
329    ) AS rn
330  FROM course_module_completion_registered_to_study_registries
331  WHERE deleted_at IS NULL
332)
333UPDATE course_module_completion_registered_to_study_registries
334SET deleted_at = NOW()
335WHERE id IN (
336    SELECT id
337    FROM duplicate_rows
338    WHERE rn > 1
339  )
340RETURNING id
341        "#
342    )
343    .fetch_all(conn)
344    .await?;
345
346    Ok(res.len() as i64)
347}
348
349#[cfg(test)]
350mod test {
351    use super::*;
352    use crate::{course_module_completions::CourseModuleCompletionGranter, test_helper::*};
353
354    #[tokio::test]
355    async fn bulk_insert_works() {
356        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
357
358        let registrar_id = crate::study_registry_registrars::insert(
359            tx.as_mut(),
360            PKeyPolicy::Generate,
361            "Test Registrar",
362            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
363        )
364        .await
365        .unwrap();
366
367        let new_completion_id = crate::course_module_completions::insert(
368            tx.as_mut(),
369            PKeyPolicy::Generate,
370            &crate::course_module_completions::NewCourseModuleCompletion {
371                course_id: course,
372                course_module_id: course_module.id,
373                user_id: user,
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: _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                completion_date: Utc::now(),
453                completion_registration_attempt_date: None,
454                completion_language: "en-US".to_string(),
455                eligible_for_ects: true,
456                email: "test@example.com".to_string(),
457                grade: Some(5),
458                passed: true,
459            },
460            CourseModuleCompletionGranter::User(user),
461        )
462        .await
463        .unwrap();
464
465        let registered_completions = vec![RegisteredCompletion {
466            completion_id: completion.id,
467            student_number: "12345".to_string(),
468            registration_date: Utc::now(),
469        }];
470
471        mark_completions_as_registered_to_study_registry(
472            tx.as_mut(),
473            registered_completions,
474            registrar_id,
475        )
476        .await
477        .unwrap();
478
479        let registrations =
480            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
481                .await
482                .unwrap();
483
484        assert_eq!(registrations.len(), 1);
485        assert_eq!(registrations[0].course_id, course);
486        assert_eq!(registrations[0].course_module_id, course_module.id);
487        assert_eq!(registrations[0].user_id, user);
488        assert_eq!(registrations[0].real_student_number, "12345");
489    }
490
491    #[tokio::test]
492    async fn insert_completions_with_invalid_completion_id_fails() {
493        insert_data!(:tx);
494
495        let registrar_id = crate::study_registry_registrars::insert(
496            tx.as_mut(),
497            PKeyPolicy::Generate,
498            "Test Registrar",
499            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
500        )
501        .await
502        .unwrap();
503
504        let invalid_uuid = Uuid::new_v4(); // This UUID doesn't correspond to any completion
505        let registered_completions = vec![RegisteredCompletion {
506            completion_id: invalid_uuid,
507            student_number: "12345".to_string(),
508            registration_date: Utc::now(),
509        }];
510
511        // Attempt to insert the completions should fail
512        let result = mark_completions_as_registered_to_study_registry(
513            tx.as_mut(),
514            registered_completions,
515            registrar_id,
516        )
517        .await;
518
519        assert!(result.is_err());
520        let error = result.unwrap_err();
521        assert_eq!(*error.error_type(), ModelErrorType::PreconditionFailed);
522        assert!(error.message().contains("Cannot find completion with id"));
523        assert!(error.message().contains(&invalid_uuid.to_string()));
524    }
525
526    #[tokio::test]
527    async fn delete_duplicate_registrations_works() {
528        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
529
530        let registrar_id = crate::study_registry_registrars::insert(
531            tx.as_mut(),
532            PKeyPolicy::Generate,
533            "Test Registrar",
534            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
535        )
536        .await
537        .unwrap();
538
539        let completion = crate::course_module_completions::insert(
540            tx.as_mut(),
541            PKeyPolicy::Generate,
542            &crate::course_module_completions::NewCourseModuleCompletion {
543                course_id: course,
544                course_module_id: course_module.id,
545                user_id: user,
546                completion_date: Utc::now(),
547                completion_registration_attempt_date: None,
548                completion_language: "en-US".to_string(),
549                eligible_for_ects: true,
550                email: "test@example.com".to_string(),
551                grade: Some(5),
552                passed: true,
553            },
554            CourseModuleCompletionGranter::User(user),
555        )
556        .await
557        .unwrap();
558
559        // Create first registration
560        let first_registration = NewCourseModuleCompletionRegisteredToStudyRegistry {
561            course_id: course,
562            course_module_completion_id: completion.id,
563            course_module_id: course_module.id,
564            study_registry_registrar_id: registrar_id,
565            user_id: user,
566            real_student_number: "12345".to_string(),
567        };
568        let first_id = insert(tx.as_mut(), PKeyPolicy::Generate, &first_registration)
569            .await
570            .unwrap();
571
572        // Create additional registrations in a separate bulk insert so that we get a different created_at timestamp
573        let later_registrations = vec![
574            NewCourseModuleCompletionRegisteredToStudyRegistry {
575                course_id: course,
576                course_module_completion_id: completion.id,
577                course_module_id: course_module.id,
578                study_registry_registrar_id: registrar_id,
579                user_id: user,
580                real_student_number: "67890".to_string(),
581            },
582            NewCourseModuleCompletionRegisteredToStudyRegistry {
583                course_id: course,
584                course_module_completion_id: completion.id,
585                course_module_id: course_module.id,
586                study_registry_registrar_id: registrar_id,
587                user_id: user,
588                real_student_number: "54321".to_string(),
589            },
590        ];
591        insert_bulk(tx.as_mut(), later_registrations).await.unwrap();
592
593        let before_registrations =
594            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
595                .await
596                .unwrap();
597        assert_eq!(before_registrations.len(), 3);
598
599        let deleted_count =
600            delete_duplicates_for_specific_completions(tx.as_mut(), &[completion.id])
601                .await
602                .unwrap();
603        assert_eq!(deleted_count, 2); // Should delete 2 out of 3 registrations
604
605        let after_registrations =
606            get_by_completion_id_and_registrar_id(tx.as_mut(), completion.id, registrar_id)
607                .await
608                .unwrap();
609        assert_eq!(after_registrations.len(), 1);
610
611        // The remaining registration should be the first one we created
612        assert_eq!(after_registrations[0].id, first_id);
613        assert_eq!(after_registrations[0].real_student_number, "12345");
614    }
615
616    #[tokio::test]
617    async fn delete_duplicate_registrations_with_no_duplicates() {
618        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
619
620        let registrar_id = crate::study_registry_registrars::insert(
621            tx.as_mut(),
622            PKeyPolicy::Generate,
623            "Test Registrar",
624            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
625        )
626        .await
627        .unwrap();
628
629        let completion1 = crate::course_module_completions::insert(
630            tx.as_mut(),
631            PKeyPolicy::Generate,
632            &crate::course_module_completions::NewCourseModuleCompletion {
633                course_id: course,
634                course_module_id: course_module.id,
635                user_id: user,
636                completion_date: Utc::now(),
637                completion_registration_attempt_date: None,
638                completion_language: "en-US".to_string(),
639                eligible_for_ects: true,
640                email: "test1@example.com".to_string(),
641                grade: Some(5),
642                passed: true,
643            },
644            CourseModuleCompletionGranter::User(user),
645        )
646        .await
647        .unwrap();
648
649        let completion2 = crate::course_module_completions::insert(
650            tx.as_mut(),
651            PKeyPolicy::Generate,
652            &crate::course_module_completions::NewCourseModuleCompletion {
653                course_id: course,
654                course_module_id: course_module.id,
655                user_id: user,
656                completion_date: Utc::now(),
657                completion_registration_attempt_date: None,
658                completion_language: "en-US".to_string(),
659                eligible_for_ects: true,
660                email: "test2@example.com".to_string(),
661                grade: Some(4),
662                passed: true,
663            },
664            CourseModuleCompletionGranter::User(user),
665        )
666        .await
667        .unwrap();
668
669        // Create one registration for each completion (no duplicates)
670        let registrations = vec![
671            NewCourseModuleCompletionRegisteredToStudyRegistry {
672                course_id: course,
673                course_module_completion_id: completion1.id,
674                course_module_id: course_module.id,
675                study_registry_registrar_id: registrar_id,
676                user_id: user,
677                real_student_number: "12345".to_string(),
678            },
679            NewCourseModuleCompletionRegisteredToStudyRegistry {
680                course_id: course,
681                course_module_completion_id: completion2.id,
682                course_module_id: course_module.id,
683                study_registry_registrar_id: registrar_id,
684                user_id: user,
685                real_student_number: "67890".to_string(),
686            },
687        ];
688        insert_bulk(tx.as_mut(), registrations).await.unwrap();
689
690        // Verify we have 1 registration for each completion
691        let before_reg1 =
692            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
693                .await
694                .unwrap();
695        let before_reg2 =
696            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
697                .await
698                .unwrap();
699        assert_eq!(before_reg1.len(), 1);
700        assert_eq!(before_reg2.len(), 1);
701
702        // Delete duplicate registrations
703        let deleted_count = delete_all_duplicates(tx.as_mut()).await.unwrap();
704        assert_eq!(deleted_count, 0); // Should delete 0 registrations as there are no duplicates
705
706        // Verify both registrations still exist
707        let after_reg1 =
708            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
709                .await
710                .unwrap();
711        let after_reg2 =
712            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
713                .await
714                .unwrap();
715        assert_eq!(after_reg1.len(), 1);
716        assert_eq!(after_reg2.len(), 1);
717    }
718
719    #[tokio::test]
720    async fn delete_duplicate_registrations_filters_by_completion_id() {
721        insert_data!(:tx, :user, :org, :course, instance: _instance, :course_module);
722
723        let registrar_id = crate::study_registry_registrars::insert(
724            tx.as_mut(),
725            PKeyPolicy::Generate,
726            "Test Registrar",
727            "test_123131231231231231231231231231238971283718927389172893718923712893129837189273891278317892378193971289",
728        )
729        .await
730        .unwrap();
731
732        // Create two completions
733        let completion1 = crate::course_module_completions::insert(
734            tx.as_mut(),
735            PKeyPolicy::Generate,
736            &crate::course_module_completions::NewCourseModuleCompletion {
737                course_id: course,
738                course_module_id: course_module.id,
739                user_id: user,
740                completion_date: Utc::now(),
741                completion_registration_attempt_date: None,
742                completion_language: "en-US".to_string(),
743                eligible_for_ects: true,
744                email: "test1@example.com".to_string(),
745                grade: Some(5),
746                passed: true,
747            },
748            CourseModuleCompletionGranter::User(user),
749        )
750        .await
751        .unwrap();
752
753        let completion2 = crate::course_module_completions::insert(
754            tx.as_mut(),
755            PKeyPolicy::Generate,
756            &crate::course_module_completions::NewCourseModuleCompletion {
757                course_id: course,
758                course_module_id: course_module.id,
759                user_id: user,
760                completion_date: Utc::now(),
761                completion_registration_attempt_date: None,
762                completion_language: "en-US".to_string(),
763                eligible_for_ects: true,
764                email: "test2@example.com".to_string(),
765                grade: Some(5),
766                passed: true,
767            },
768            CourseModuleCompletionGranter::User(user),
769        )
770        .await
771        .unwrap();
772
773        // Create first registrations (these should be kept)
774        let first_registrations = vec![
775            NewCourseModuleCompletionRegisteredToStudyRegistry {
776                course_id: course,
777                course_module_completion_id: completion1.id,
778                course_module_id: course_module.id,
779                study_registry_registrar_id: registrar_id,
780                user_id: user,
781                real_student_number: "12345-1".to_string(),
782            },
783            NewCourseModuleCompletionRegisteredToStudyRegistry {
784                course_id: course,
785                course_module_completion_id: completion2.id,
786                course_module_id: course_module.id,
787                study_registry_registrar_id: registrar_id,
788                user_id: user,
789                real_student_number: "12345-2".to_string(),
790            },
791        ];
792        insert_bulk(tx.as_mut(), first_registrations).await.unwrap();
793
794        // Create second registrations (these should be deleted as duplicates)
795        let second_registrations = vec![
796            NewCourseModuleCompletionRegisteredToStudyRegistry {
797                course_id: course,
798                course_module_completion_id: completion1.id,
799                course_module_id: course_module.id,
800                study_registry_registrar_id: registrar_id,
801                user_id: user,
802                real_student_number: "67890-1".to_string(),
803            },
804            NewCourseModuleCompletionRegisteredToStudyRegistry {
805                course_id: course,
806                course_module_completion_id: completion2.id,
807                course_module_id: course_module.id,
808                study_registry_registrar_id: registrar_id,
809                user_id: user,
810                real_student_number: "67890-2".to_string(),
811            },
812        ];
813        insert_bulk(tx.as_mut(), second_registrations)
814            .await
815            .unwrap();
816
817        // Verify we have 2 registrations for each completion
818        let before_reg1 =
819            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
820                .await
821                .unwrap();
822        let before_reg2 =
823            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
824                .await
825                .unwrap();
826        assert_eq!(before_reg1.len(), 2);
827        assert_eq!(before_reg2.len(), 2);
828
829        // Delete duplicate registrations only for completion1
830        let deleted_count =
831            delete_duplicates_for_specific_completions(tx.as_mut(), &[completion1.id])
832                .await
833                .unwrap();
834        assert_eq!(deleted_count, 1); // Should delete 1 duplicate for completion1
835
836        // Verify completion1 now has 1 registration and completion2 still has 2
837        let after_reg1 =
838            get_by_completion_id_and_registrar_id(tx.as_mut(), completion1.id, registrar_id)
839                .await
840                .unwrap();
841        let after_reg2 =
842            get_by_completion_id_and_registrar_id(tx.as_mut(), completion2.id, registrar_id)
843                .await
844                .unwrap();
845        assert_eq!(after_reg1.len(), 1);
846        assert_eq!(after_reg2.len(), 2);
847
848        // The registration for completion1 should be the first one we created
849        assert_eq!(after_reg1[0].real_student_number, "12345-1");
850    }
851}