headless_lms_credit_registration/use_cases/resolve_enrolments/enrolments/
request.rs1use 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
13pub(super) enum Unaskable {
16 NoStudentNumber,
17 Config(CreditRegistrationErrorCode),
18}
19
20impl Unaskable {
21 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
30pub(super) struct Askable {
33 pub request: EnrolmentLookup,
34 pub criteria: EnrolmentCriteria,
35}
36
37pub(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}