1use utoipa::ToSchema;
6
7use crate::credit_registrations::CreditRegistrationState;
8use crate::prelude::*;
9
10use super::pending_reason::{CreditRegistrationPendingReason, PendingPreconditions};
11
12#[derive(
15 Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, ToSchema,
16)]
17#[serde(rename_all = "snake_case")]
18pub enum TimelinePhase {
19 Course,
20 StudentNumber,
21 Registering,
22 Confirmation,
23 Ended,
26}
27
28impl TimelinePhase {
29 pub fn entered_with(state: CreditRegistrationState) -> Option<Self> {
32 (state != CreditRegistrationState::Pending)
33 .then(|| TimelineStep::of(state, PendingPreconditions::ALL_MET, false).phase())
34 }
35}
36
37#[derive(
40 Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Type, ToSchema,
41)]
42#[sqlx(
43 type_name = "credit_registration_timeline_step",
44 rename_all = "snake_case"
45)]
46#[serde(rename_all = "snake_case")]
47pub enum TimelineStep {
48 CourseNotRegistrableYet,
50 WaitingForStudentNumber,
51 HeldForCourseCode,
54 LookingForEnrolment,
55 WaitingForEnrolment,
56 Sending,
57 AnswerUnclear,
59 WaitingForAssessmentItem,
60 WaitingForCourseUnit,
61 Registered,
62 AlreadyInSisu,
63 BetterGradeInSisu,
64 RecordedWrongly,
65 NeedsAPerson,
67 NotRegistering,
68 NoLongerRegistrable,
69}
70
71impl TimelineStep {
72 pub const ALL: [Self; 16] = [
74 Self::CourseNotRegistrableYet,
75 Self::WaitingForStudentNumber,
76 Self::HeldForCourseCode,
77 Self::LookingForEnrolment,
78 Self::WaitingForEnrolment,
79 Self::Sending,
80 Self::AnswerUnclear,
81 Self::WaitingForAssessmentItem,
82 Self::WaitingForCourseUnit,
83 Self::Registered,
84 Self::AlreadyInSisu,
85 Self::BetterGradeInSisu,
86 Self::RecordedWrongly,
87 Self::NeedsAPerson,
88 Self::NotRegistering,
89 Self::NoLongerRegistrable,
90 ];
91
92 pub const ENGAGEMENT_STEPS: [Self; 2] =
94 [Self::WaitingForStudentNumber, Self::WaitingForEnrolment];
95
96 pub fn of(
104 state: CreditRegistrationState,
105 preconditions: PendingPreconditions,
106 enrolment_resolved: bool,
107 ) -> Self {
108 use CreditRegistrationPendingReason as Reason;
109 use CreditRegistrationState as State;
110 match state {
111 State::Pending => match preconditions.reason() {
112 Some(Reason::Completion) => Self::CourseNotRegistrableYet,
113 Some(Reason::StudentNumber) => Self::WaitingForStudentNumber,
114 Some(Reason::CourseCode) => Self::HeldForCourseCode,
115 None => Self::LookingForEnrolment,
117 },
118 State::ReadyToSubmit | State::ResolvingEnrolment => Self::LookingForEnrolment,
119 State::FailedRetryable if !enrolment_resolved => Self::LookingForEnrolment,
120 State::NoUsableEnrolment => Self::WaitingForEnrolment,
121 State::CheckingEnrolment | State::Submitting | State::FailedRetryable => Self::Sending,
122 State::SubmissionUncertain => Self::AnswerUnclear,
123 State::AwaitingVerification => Self::WaitingForAssessmentItem,
124 State::PartiallyRegistered => Self::WaitingForCourseUnit,
125 State::Registered => Self::Registered,
126 State::Duplicate => Self::AlreadyInSisu,
127 State::NotImproved => Self::BetterGradeInSisu,
128 State::Misregistered => Self::RecordedWrongly,
129 State::FailedPermanent => Self::NeedsAPerson,
130 State::Cancelled => Self::NotRegistering,
131 State::Blocked => Self::NoLongerRegistrable,
132 }
133 }
134
135 pub fn phase(self) -> TimelinePhase {
137 match self {
138 Self::CourseNotRegistrableYet => TimelinePhase::Course,
139 Self::WaitingForStudentNumber => TimelinePhase::StudentNumber,
140 Self::HeldForCourseCode
141 | Self::LookingForEnrolment
142 | Self::WaitingForEnrolment
143 | Self::Sending
144 | Self::AnswerUnclear
145 | Self::NeedsAPerson => TimelinePhase::Registering,
146 Self::WaitingForAssessmentItem
147 | Self::WaitingForCourseUnit
148 | Self::Registered
149 | Self::RecordedWrongly => TimelinePhase::Confirmation,
150 Self::AlreadyInSisu
151 | Self::BetterGradeInSisu
152 | Self::NotRegistering
153 | Self::NoLongerRegistrable => TimelinePhase::Ended,
154 }
155 }
156
157 pub fn has_engagement(self) -> bool {
159 Self::ENGAGEMENT_STEPS.contains(&self)
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub struct TimelinePosition {
166 pub step: TimelineStep,
167 pub engagement: Option<Engagement>,
168}
169
170impl TimelinePosition {
171 pub fn of(
173 state: CreditRegistrationState,
174 preconditions: PendingPreconditions,
175 enrolment_resolved: bool,
176 engagement: Engagement,
177 ) -> Self {
178 let step = TimelineStep::of(state, preconditions, enrolment_resolved);
179 Self {
180 step,
181 engagement: step.has_engagement().then_some(engagement),
182 }
183 }
184}
185
186#[derive(Debug, Clone, Default, PartialEq)]
193pub struct StepMatch {
194 pub states: Vec<CreditRegistrationState>,
195 pub completion_eligible: Vec<bool>,
196 pub has_verified_student_number: Vec<bool>,
197 pub course_code_allowed: Vec<bool>,
198 pub enrolment_resolved: Vec<bool>,
199 pub steps: Vec<TimelineStep>,
200}
201
202impl StepMatch {
203 pub fn of(steps: &[TimelineStep]) -> Self {
205 let mut matched = Self::default();
206 let flags = [false, true];
207 for (
208 state,
209 completion_eligible,
210 has_verified_student_number,
211 course_code_allowed,
212 enrolment_resolved,
213 ) in itertools::iproduct!(CreditRegistrationState::ALL, flags, flags, flags, flags)
214 {
215 let preconditions = PendingPreconditions {
216 completion_eligible,
217 has_verified_student_number,
218 course_code_allowed,
219 };
220 let step = TimelineStep::of(state, preconditions, enrolment_resolved);
221 if steps.contains(&step) {
222 matched.states.push(state);
223 matched.completion_eligible.push(completion_eligible);
224 matched
225 .has_verified_student_number
226 .push(has_verified_student_number);
227 matched.course_code_allowed.push(course_code_allowed);
228 matched.enrolment_resolved.push(enrolment_resolved);
229 matched.steps.push(step);
230 }
231 }
232 matched
233 }
234
235 pub fn all() -> Self {
237 Self::of(&TimelineStep::ALL)
238 }
239}
240
241#[derive(
244 Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Type, ToSchema,
245)]
246#[sqlx(
247 type_name = "credit_registration_engagement",
248 rename_all = "snake_case"
249)]
250#[serde(rename_all = "snake_case")]
251pub enum Engagement {
252 Pressed,
255 Visited,
256 NotStarted,
257}
258
259impl Engagement {
260 pub fn of(pressed_at: Option<DateTime<Utc>>, last_visited_at: Option<DateTime<Utc>>) -> Self {
263 if pressed_at.is_some() {
264 Self::Pressed
265 } else if last_visited_at.is_some() {
266 Self::Visited
267 } else {
268 Self::NotStarted
269 }
270 }
271}
272
273#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Hash, ToSchema)]
275#[serde(rename_all = "snake_case")]
276pub enum WaitsOn {
277 Student,
278 Sisu,
279 Support,
280 CourseSetup,
281 Nobody,
283}
284
285impl WaitsOn {
286 pub fn of(
290 step: TimelineStep,
291 engagement: Option<Engagement>,
292 needs_attention: bool,
293 needs_course_setup: bool,
294 ) -> Self {
295 if needs_course_setup {
296 return Self::CourseSetup;
297 }
298 if needs_attention {
299 return Self::Support;
300 }
301 match step {
302 TimelineStep::CourseNotRegistrableYet | TimelineStep::WaitingForEnrolment => {
303 Self::Student
304 }
305 TimelineStep::WaitingForStudentNumber => match engagement {
306 Some(Engagement::Pressed) => Self::Sisu,
308 Some(Engagement::Visited | Engagement::NotStarted) | None => Self::Student,
309 },
310 TimelineStep::HeldForCourseCode => Self::CourseSetup,
311 TimelineStep::LookingForEnrolment
312 | TimelineStep::Sending
313 | TimelineStep::WaitingForAssessmentItem
314 | TimelineStep::WaitingForCourseUnit => Self::Sisu,
315 TimelineStep::AnswerUnclear
316 | TimelineStep::RecordedWrongly
317 | TimelineStep::NeedsAPerson => Self::Support,
318 TimelineStep::Registered
319 | TimelineStep::AlreadyInSisu
320 | TimelineStep::BetterGradeInSisu
321 | TimelineStep::NotRegistering
322 | TimelineStep::NoLongerRegistrable => Self::Nobody,
323 }
324 }
325}