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