Skip to main content

headless_lms_credit_registration/registry/
answers.rs

1//! What the study registry answered for one item, read into the pipeline's terms.
2
3use std::collections::HashMap;
4use std::time::Duration;
5
6use chrono::{DateTime, Utc};
7use headless_lms_models::credit_registrations::{
8    CreditRegistrationErrorCode, CreditRegistrationState,
9};
10use headless_lms_models::library::credit_registration::config_validation::CourseCodeVerdict;
11use headless_lms_models::library::credit_registration::study_registry::{
12    RegistryAttainment, RegistryEnrolment, RosterPerson,
13};
14use secrecy::SecretString;
15
16use super::ids::{CourseCode, SubmittedAttainmentRef};
17
18/// A Sisu person a student number resolved to.
19pub(crate) struct FoundPerson {
20    pub person_id: SecretString,
21    pub first_names: Option<SecretString>,
22    pub last_name: Option<SecretString>,
23}
24
25/// A worker's person lookup, read into the codes its row is written with. Not [`RegistryPerson`],
26/// which keeps Suotar's own code for someone to read.
27pub(crate) struct PersonAnswer {
28    pub reading: PersonReading,
29    /// The item's own error, which may accompany any reading.
30    pub error_message: Option<String>,
31}
32
33pub(crate) enum PersonReading {
34    Found(FoundPerson),
35    /// No person to fill in: the item's code, or `UnexpectedResponse` for an ok item without one.
36    Refused {
37        code: CreditRegistrationErrorCode,
38    },
39}
40
41/// The Sisu person an interactive lookup found for a student number.
42pub struct RegistryPerson {
43    pub sisu_person_id: SecretString,
44    pub first_names: Option<SecretString>,
45    pub last_name: Option<SecretString>,
46    /// The registry's own per-item code, an identifier rather than prose.
47    pub code: String,
48}
49
50/// Why an interactive person lookup could not say whether the number exists. `personNotFound` is
51/// an answer, not one of these.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum PersonLookupError {
54    /// The request itself failed: network, auth, or a request-level error from the registry.
55    StudyRegistryUnavailable,
56    /// The registry answered but its response did not include this item.
57    ItemMissingFromResponse,
58    /// The registry answered for this item with something other than a person or
59    /// `personNotFound`: its own code, an identifier rather than prose.
60    UnexpectedAnswer { code: String },
61}
62
63/// An enrolment lookup. The lists are read whatever the reading: an enrolment error still lists
64/// the attainments.
65pub(crate) struct EnrolmentAnswer {
66    pub reading: EnrolmentReading,
67    pub enrolments: Vec<RegistryEnrolment>,
68    pub existing_attainments: Vec<RegistryAttainment>,
69}
70
71pub(crate) enum EnrolmentReading {
72    Listed,
73    Refused {
74        code: CreditRegistrationErrorCode,
75        error_message: Option<String>,
76    },
77}
78
79pub(crate) enum ImportAnswer {
80    /// Sisu took the submission, or an earlier item of the same batch already made it
81    /// (`is_repeat_in_batch`).
82    Submitted {
83        submission: Option<SubmittedAttainmentRef>,
84        is_repeat_in_batch: bool,
85    },
86    /// The registry already holds the credit, so the row settles without a submission.
87    Settled {
88        held: HeldCredit,
89        attainment: Option<RegistryAttainment>,
90    },
91    /// Anything the registry answered as an error, `sisuTimeout` included, which may still name a
92    /// submission it made.
93    Refused {
94        code: CreditRegistrationErrorCode,
95        submission: Option<SubmittedAttainmentRef>,
96        error_message: Option<String>,
97    },
98    /// A success code we do not know, which is no proof that nothing was created.
99    UnknownSuccessCode,
100}
101
102/// How the credit an import settled on is already held.
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub(crate) enum HeldCredit {
105    Duplicate,
106    NotImproved,
107}
108
109impl HeldCredit {
110    pub(crate) fn state(self) -> CreditRegistrationState {
111        match self {
112            Self::Duplicate => CreditRegistrationState::Duplicate,
113            Self::NotImproved => CreditRegistrationState::NotImproved,
114        }
115    }
116}
117
118/// A verify poll.
119pub(crate) struct VerificationAnswer {
120    pub reading: VerificationReading,
121    /// The item's own error, which may accompany any reading.
122    pub error_message: Option<String>,
123}
124
125pub(crate) enum VerificationReading {
126    /// The course unit attainment is there.
127    Registered {
128        attainment: RegistryAttainment,
129    },
130    /// Registered, but with no course unit attainment yet.
131    PartiallyRegistered,
132    /// Sisu has not finished; `resubmit_not_before` bounds when resending becomes safe.
133    Pending {
134        resubmit_not_before: Option<DateTime<Utc>>,
135    },
136    /// No trace of the submission.
137    NotRegistered,
138    Failed {
139        code: CreditRegistrationErrorCode,
140    },
141    /// An answer that settles nothing we act on.
142    Inconclusive,
143}
144
145/// One list-by-course request's answer.
146pub(crate) struct RosterListing {
147    pub duration: Duration,
148    /// One per requested code, in request order: its people, or why there is no roster for it.
149    pub rosters: Vec<Result<Vec<RosterPerson>, CreditRegistrationErrorCode>>,
150}
151
152/// An interactive search of a course's rosters for one student number.
153pub(crate) struct RosterSearch {
154    /// The first roster, in code order, that lists the number.
155    pub person: Option<RosterPerson>,
156    /// A code's request failed, so the person may still be on its roster.
157    pub has_unanswered_code: bool,
158}
159
160/// The verdicts the registry gave, by code; a code it gave none for is missing.
161pub(crate) type CourseCodeVerdicts = HashMap<CourseCode, CourseCodeVerdict>;