Skip to main content

headless_lms_models/library/credit_registration/
student_progress.rs

1//! The step list a student sees once their registration has found an enrolment. Classified here so
2//! a new ledger state has to be placed on it before it compiles.
3
4use utoipa::ToSchema;
5
6use crate::credit_registrations::CreditRegistrationState;
7use crate::prelude::*;
8
9/// One row of the step list, in the order a registration passes them.
10#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, ToSchema)]
11#[serde(rename_all = "snake_case")]
12pub enum StudentRegistrationProgressStep {
13    EnrolmentFound,
14    SentToSisu,
15    AcceptedBySisu,
16    /// Sisu holds the assessment item attainment but not yet the course unit attainment.
17    PartiallyRegistered,
18}
19
20impl StudentRegistrationProgressStep {
21    const IN_ORDER: [Self; 4] = [
22        Self::EnrolmentFound,
23        Self::SentToSisu,
24        Self::AcceptedBySisu,
25        Self::PartiallyRegistered,
26    ];
27}
28
29#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
30pub struct CompletedProgressStep {
31    pub step: StudentRegistrationProgressStep,
32    /// When the registration reached the step; `None` for a step it skipped.
33    pub reached_at: Option<DateTime<Utc>>,
34}
35
36#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
37pub struct StudentRegistrationProgress {
38    /// `None` once every step is done.
39    pub current_step: Option<StudentRegistrationProgressStep>,
40    /// The steps before `current_step`, in order.
41    pub completed_steps: Vec<CompletedProgressStep>,
42    /// A send is out and we do not know yet whether it reached Sisu. Only with `current_step`
43    /// `sent_to_sisu`.
44    pub submission_uncertain: bool,
45}
46
47/// The state a row enters once it has found an enrolment; its latest entry dates
48/// [`StudentRegistrationProgressStep::EnrolmentFound`].
49pub const ENROLMENT_FOUND_STATE: CreditRegistrationState =
50    CreditRegistrationState::CheckingEnrolment;
51
52/// What the step list is drawn from.
53#[derive(Debug, Clone, Copy)]
54pub struct ProgressFacts {
55    pub state: CreditRegistrationState,
56    /// See `StudentCreditRegistration::has_usable_enrolment`.
57    pub has_usable_enrolment: bool,
58    /// The row's latest entry into [`ENROLMENT_FOUND_STATE`].
59    pub enrolment_found_at: Option<DateTime<Utc>>,
60    /// Whether Sisu answered the latest send with an attainment.
61    pub has_submitted_attainment: bool,
62    pub submitted_at: Option<DateTime<Utc>>,
63    pub partially_registered_at: Option<DateTime<Utc>>,
64    pub registered_at: Option<DateTime<Utc>>,
65}
66
67enum Position {
68    At(StudentRegistrationProgressStep),
69    Finished,
70    /// Not on its way to Sisu, so the step list does not describe it.
71    Off,
72}
73
74fn position(facts: &ProgressFacts) -> Position {
75    use CreditRegistrationState as S;
76    use StudentRegistrationProgressStep as Step;
77    match facts.state {
78        // With a usable enrolment, `pending` is a precondition lost after the enrolment was found.
79        S::Pending
80        | S::ReadyToSubmit
81        | S::ResolvingEnrolment
82        | S::CheckingEnrolment
83        | S::Submitting
84        | S::SubmissionUncertain => Position::At(Step::SentToSisu),
85        // A failed verify poll leaves a landed submission here until it resumes verifying.
86        S::FailedRetryable if facts.partially_registered_at.is_some() => {
87            Position::At(Step::PartiallyRegistered)
88        }
89        S::FailedRetryable if facts.has_submitted_attainment => Position::At(Step::AcceptedBySisu),
90        S::FailedRetryable => Position::At(Step::SentToSisu),
91        S::AwaitingVerification => Position::At(Step::AcceptedBySisu),
92        S::PartiallyRegistered => Position::At(Step::PartiallyRegistered),
93        S::Registered | S::Duplicate | S::NotImproved => Position::Finished,
94        S::NoUsableEnrolment
95        | S::Misregistered
96        | S::FailedPermanent
97        | S::Blocked
98        | S::Cancelled => Position::Off,
99    }
100}
101
102impl StudentRegistrationProgress {
103    /// `None` until the row has a usable enrolment, and for a row that is not on its way to Sisu.
104    pub fn of(facts: ProgressFacts) -> Option<Self> {
105        use StudentRegistrationProgressStep as Step;
106        if !facts.has_usable_enrolment {
107            return None;
108        }
109        let current_step = match position(&facts) {
110            Position::At(step) => Some(step),
111            Position::Finished => None,
112            Position::Off => return None,
113        };
114        let completed_steps = Step::IN_ORDER
115            .into_iter()
116            .take_while(|step| Some(*step) != current_step)
117            .map(|step| CompletedProgressStep {
118                step,
119                reached_at: match step {
120                    Step::EnrolmentFound => facts.enrolment_found_at,
121                    Step::SentToSisu => facts.submitted_at,
122                    Step::AcceptedBySisu => facts.partially_registered_at.or(facts.registered_at),
123                    Step::PartiallyRegistered => facts.partially_registered_at,
124                },
125            })
126            .collect();
127        Some(Self {
128            current_step,
129            completed_steps,
130            submission_uncertain: facts.state == CreditRegistrationState::SubmissionUncertain,
131        })
132    }
133}