Skip to main content

headless_lms_models/library/credit_registration/
student_facing_status.rs

1//! What a student is told about one credit registration. Computed here rather than in the frontend
2//! so a new ledger state has to be classified before it compiles.
3
4use utoipa::ToSchema;
5
6use crate::credit_registrations::CreditRegistrationState;
7use crate::prelude::*;
8
9use super::pending_reason::{CreditRegistrationPendingReason, PendingPreconditions};
10
11/// The stage a student sees.
12#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, ToSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum StudentFacingCreditRegistrationStatus {
15    WaitingForCompletion,
16    NeedsStudentNumber,
17    InProgress,
18    NeedsEnrolment,
19    WaitingForSisu,
20    Registered,
21    Failed,
22    /// Nothing is happening and nothing will, until the student changes something.
23    NotRegistering,
24}
25
26impl StudentFacingCreditRegistrationStatus {
27    /// `preconditions` is only read for `pending`, whose whole point is that the ledger does not
28    /// record which of them the row is waiting on. Pass [`PendingPreconditions::ALL_MET`] only where
29    /// the row is known not to be pending.
30    pub fn of(state: CreditRegistrationState, preconditions: PendingPreconditions) -> Self {
31        use CreditRegistrationPendingReason as Reason;
32        use CreditRegistrationState as State;
33        match state {
34            State::Pending => match preconditions.reason() {
35                Some(Reason::Completion) => Self::WaitingForCompletion,
36                Some(Reason::StudentNumber) => Self::NeedsStudentNumber,
37                // Nothing is outstanding, so the next precondition tick moves the row on.
38                None => Self::InProgress,
39            },
40            State::ReadyToSubmit
41            | State::ResolvingEnrolment
42            | State::CheckingEnrolment
43            | State::Submitting
44            | State::FailedRetryable => Self::InProgress,
45            State::NoUsableEnrolment => Self::NeedsEnrolment,
46            State::SubmissionUncertain | State::AwaitingVerification => Self::WaitingForSisu,
47            // not_improved means Sisu holds an equal or better attainment, so the credit exists.
48            State::Registered | State::Duplicate | State::NotImproved => Self::Registered,
49            State::Misregistered | State::FailedPermanent => Self::Failed,
50            State::Blocked | State::Cancelled => Self::NotRegistering,
51        }
52    }
53
54    /// Whether the pipeline still moves this row on its own; the status page polls while it does.
55    pub fn is_moving(self) -> bool {
56        matches!(self, Self::InProgress | Self::WaitingForSisu)
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use CreditRegistrationState as State;
64    use StudentFacingCreditRegistrationStatus as Status;
65
66    /// A row waiting on the student must not keep the page polling; nothing changes until they act.
67    #[test]
68    fn only_the_states_the_pipeline_still_owns_keep_the_page_polling() {
69        for reason in [
70            PendingPreconditions {
71                completion_eligible: false,
72                ..PendingPreconditions::ALL_MET
73            },
74            PendingPreconditions {
75                has_verified_student_number: false,
76                ..PendingPreconditions::ALL_MET
77            },
78        ] {
79            assert!(
80                !Status::of(State::Pending, reason).is_moving(),
81                "{reason:?}"
82            );
83        }
84        let moving = [
85            // Nothing outstanding, so the recompute moves it on without the student doing anything.
86            State::Pending,
87            State::ReadyToSubmit,
88            State::ResolvingEnrolment,
89            State::CheckingEnrolment,
90            State::Submitting,
91            State::FailedRetryable,
92            State::SubmissionUncertain,
93            State::AwaitingVerification,
94        ];
95        for state in CreditRegistrationState::ALL {
96            assert_eq!(
97                Status::of(state, PendingPreconditions::ALL_MET).is_moving(),
98                moving.contains(&state),
99                "{state:?}"
100            );
101        }
102    }
103
104    /// The student's question is "do I have the credits", so every success terminal answers yes.
105    #[test]
106    fn the_success_set_is_one_stage() {
107        for state in CreditRegistrationState::ALL {
108            if state.is_success() {
109                assert_eq!(
110                    Status::of(state, PendingPreconditions::ALL_MET),
111                    Status::Registered,
112                    "{state:?}"
113                );
114            }
115        }
116    }
117
118    #[test]
119    fn the_wire_spelling_is_snake_case() {
120        assert_eq!(
121            serde_json::to_value(Status::NeedsStudentNumber).unwrap(),
122            serde_json::json!("needs_student_number")
123        );
124    }
125}