headless_lms_models/library/credit_registration/
student_facing_status.rs1use utoipa::ToSchema;
5
6use crate::credit_registrations::CreditRegistrationState;
7use crate::prelude::*;
8
9use super::pending_reason::{CreditRegistrationPendingReason, PendingPreconditions};
10
11#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, ToSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum StudentFacingCreditRegistrationStatus {
15 WaitingForCompletion,
16 NeedsStudentNumber,
17 LookingForEnrolment,
20 NeedsEnrolment,
21 WaitingForCourseSetup,
24 Sending,
26 WaitingForSisu,
27 Registered,
28 Failed,
29 NotRegistering,
31}
32
33impl StudentFacingCreditRegistrationStatus {
34 pub fn of(
42 state: CreditRegistrationState,
43 preconditions: PendingPreconditions,
44 enrolment_resolved: bool,
45 ) -> Self {
46 use CreditRegistrationPendingReason as Reason;
47 use CreditRegistrationState as State;
48 match state {
49 State::Pending => match preconditions.reason() {
50 Some(Reason::Completion) => Self::WaitingForCompletion,
51 Some(Reason::StudentNumber) => Self::NeedsStudentNumber,
52 Some(Reason::CourseCode) => Self::WaitingForCourseSetup,
53 None => Self::LookingForEnrolment,
55 },
56 State::ReadyToSubmit | State::ResolvingEnrolment | State::CheckingEnrolment => {
57 Self::LookingForEnrolment
58 }
59 State::Submitting => Self::Sending,
60 State::FailedRetryable => {
61 if enrolment_resolved {
62 Self::Sending
63 } else {
64 Self::LookingForEnrolment
65 }
66 }
67 State::NoUsableEnrolment => Self::NeedsEnrolment,
68 State::SubmissionUncertain
69 | State::AwaitingVerification
70 | State::PartiallyRegistered => Self::WaitingForSisu,
71 State::Registered | State::Duplicate | State::NotImproved => Self::Registered,
73 State::Misregistered | State::FailedPermanent => Self::Failed,
74 State::Blocked | State::Cancelled => Self::NotRegistering,
75 }
76 }
77
78 pub fn is_moving(self) -> bool {
80 matches!(
81 self,
82 Self::LookingForEnrolment | Self::Sending | Self::WaitingForSisu
83 )
84 }
85}
86
87#[derive(Debug, Clone, Default, PartialEq)]
95pub struct StageMatch {
96 pub states: Vec<CreditRegistrationState>,
97 pub completion_eligible: Vec<bool>,
98 pub has_verified_student_number: Vec<bool>,
99 pub course_code_allowed: Vec<bool>,
100 pub enrolment_resolved: Vec<bool>,
101}
102
103impl StageMatch {
104 pub fn of(stages: &[StudentFacingCreditRegistrationStatus]) -> Self {
106 let mut matched = Self::default();
107 if stages.is_empty() {
108 return matched;
109 }
110 let flags = [false, true];
111 for (
112 state,
113 completion_eligible,
114 has_verified_student_number,
115 course_code_allowed,
116 enrolment_resolved,
117 ) in itertools::iproduct!(CreditRegistrationState::ALL, flags, flags, flags, flags)
118 {
119 let preconditions = PendingPreconditions {
120 completion_eligible,
121 has_verified_student_number,
122 course_code_allowed,
123 };
124 if stages.contains(&StudentFacingCreditRegistrationStatus::of(
125 state,
126 preconditions,
127 enrolment_resolved,
128 )) {
129 matched.states.push(state);
130 matched.completion_eligible.push(completion_eligible);
131 matched
132 .has_verified_student_number
133 .push(has_verified_student_number);
134 matched.course_code_allowed.push(course_code_allowed);
135 matched.enrolment_resolved.push(enrolment_resolved);
136 }
137 }
138 matched
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145 use CreditRegistrationState as State;
146 use StudentFacingCreditRegistrationStatus as Status;
147
148 #[test]
150 fn only_the_states_the_pipeline_still_owns_keep_the_page_polling() {
151 for reason in [
152 PendingPreconditions {
153 completion_eligible: false,
154 ..PendingPreconditions::ALL_MET
155 },
156 PendingPreconditions {
157 has_verified_student_number: false,
158 ..PendingPreconditions::ALL_MET
159 },
160 PendingPreconditions {
161 course_code_allowed: false,
162 ..PendingPreconditions::ALL_MET
163 },
164 ] {
165 assert!(
166 !Status::of(State::Pending, reason, false).is_moving(),
167 "{reason:?}"
168 );
169 }
170 let moving = [
171 State::Pending,
173 State::ReadyToSubmit,
174 State::ResolvingEnrolment,
175 State::CheckingEnrolment,
176 State::Submitting,
177 State::FailedRetryable,
178 State::SubmissionUncertain,
179 State::AwaitingVerification,
180 State::PartiallyRegistered,
181 ];
182 for state in CreditRegistrationState::ALL {
183 for enrolment_resolved in [false, true] {
184 assert_eq!(
185 Status::of(state, PendingPreconditions::ALL_MET, enrolment_resolved)
186 .is_moving(),
187 moving.contains(&state),
188 "{state:?} {enrolment_resolved}"
189 );
190 }
191 }
192 }
193
194 #[test]
196 fn the_success_set_is_one_stage() {
197 for state in CreditRegistrationState::ALL {
198 if state.is_success() {
199 assert_eq!(
200 Status::of(state, PendingPreconditions::ALL_MET, true),
201 Status::Registered,
202 "{state:?}"
203 );
204 }
205 }
206 }
207
208 #[test]
209 fn the_wire_spelling_is_snake_case() {
210 assert_eq!(
211 serde_json::to_value(Status::NeedsStudentNumber).unwrap(),
212 serde_json::json!("needs_student_number")
213 );
214 }
215}