Skip to main content

headless_lms_credit_registration/use_cases/resolve_enrolments/enrolments/
request.rs

1//! Whether a row can be asked about at all, and what to ask the study registry about its
2//! enrolment.
3
4use headless_lms_models::credit_registrations::CreditRegistrationErrorCode;
5use headless_lms_models::library::credit_registration::enrolment_selection::EnrolmentCriteria;
6use headless_lms_models::library::credit_registration::outcomes::{
7    UnaskedMove, module_not_configured, no_verified_student_number,
8};
9use headless_lms_models::library::credit_registration::submission_context::SubmissionContext;
10
11use crate::registry::{CourseCode, EnrolmentLookup, StudentNumber};
12
13/// A row that cannot even be asked about: each of these is the student's or a teacher's to fix, and
14/// none of them is worth a call.
15pub(super) enum Unaskable {
16    NoStudentNumber,
17    Config(CreditRegistrationErrorCode),
18}
19
20impl Unaskable {
21    /// Where the row goes instead of being asked about.
22    pub(super) fn unasked_move(&self) -> UnaskedMove {
23        match self {
24            Self::NoStudentNumber => no_verified_student_number(),
25            Self::Config(code) => module_not_configured(*code),
26        }
27    }
28}
29
30/// A row that can be asked about: what to ask the registry, and what the enrolment it lists must fit
31/// for the row's attainment to be registered against it.
32pub(super) struct Askable {
33    pub request: EnrolmentLookup,
34    pub criteria: EnrolmentCriteria,
35}
36
37/// What to ask the registry about the row's enrolment, or why it cannot be asked about.
38pub(super) fn enrolment_lookup(context: &SubmissionContext) -> Result<Askable, Unaskable> {
39    let student_number = context
40        .student_number
41        .clone()
42        .ok_or(Unaskable::NoStudentNumber)?;
43    let course_code = context
44        .uh_course_code
45        .as_deref()
46        .and_then(CourseCode::parse)
47        .ok_or(Unaskable::Config(
48            CreditRegistrationErrorCode::MissingUhCourseCode,
49        ))?;
50    let credits = context.ects_credits.ok_or(Unaskable::Config(
51        CreditRegistrationErrorCode::MissingEctsCredits,
52    ))?;
53    Ok(Askable {
54        request: EnrolmentLookup {
55            student_number: StudentNumber::new(student_number),
56            course_code,
57        },
58        criteria: EnrolmentCriteria {
59            attainment_date: context.completion.completion_date.date_naive(),
60            credits,
61        },
62    })
63}
64
65#[cfg(test)]
66mod tests {
67    use headless_lms_models::credit_registrations::AdminAttention;
68    use headless_lms_models::credit_registrations::CreditRegistrationErrorCode as Code;
69    use headless_lms_models::credit_registrations::CreditRegistrationState as State;
70
71    use super::super::fixtures::context;
72    use super::*;
73
74    #[test]
75    fn a_row_without_a_verified_number_waits_in_pending_without_a_call() {
76        let unlinked = SubmissionContext {
77            student_number: None,
78            ..context(Some(4))
79        };
80        let Err(unaskable) = enrolment_lookup(&unlinked) else {
81            panic!("askable");
82        };
83        assert!(matches!(unaskable, Unaskable::NoStudentNumber));
84        assert_eq!(unaskable.unasked_move().outcome.to_state, State::Pending);
85    }
86
87    #[test]
88    fn a_module_missing_its_code_or_credits_needs_an_admin_without_a_call() {
89        let blank_code = SubmissionContext {
90            uh_course_code: Some("  ".to_string()),
91            ..context(Some(4))
92        };
93        let no_credits = SubmissionContext {
94            ects_credits: None,
95            ..context(Some(4))
96        };
97        for (context, expected) in [
98            (blank_code, Code::MissingUhCourseCode),
99            (no_credits, Code::MissingEctsCredits),
100        ] {
101            let Err(unaskable) = enrolment_lookup(&context) else {
102                panic!("askable");
103            };
104            let outcome = unaskable.unasked_move().outcome;
105            assert_eq!(outcome.to_state, State::FailedPermanent);
106            assert_eq!(outcome.error_code, Some(expected));
107            assert_eq!(outcome.needs_admin_attention, Some(AdminAttention::Raise));
108        }
109    }
110
111    #[test]
112    fn the_lookup_asks_about_the_trimmed_course_code() {
113        let Ok(askable) = enrolment_lookup(&context(Some(4))) else {
114            panic!("unaskable");
115        };
116        assert_eq!(askable.request.course_code.as_str(), "TKT10002");
117        assert_eq!(askable.request.student_number.expose(), "012345678");
118        assert_eq!(askable.criteria.credits, 5.0);
119    }
120}