1use std::collections::BTreeMap;
7
8use chrono::NaiveDate;
9use headless_lms_utils::services::suotar::SuotarEndpoint;
10
11use crate::prelude::*;
12
13pub type StudentNumber = String;
14pub type CourseCode = String;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
18pub enum EnrolmentState {
19 Enrolled,
20 Processing,
21 Rejected,
22 Aborted,
23}
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
27pub enum AttainmentState {
28 Attained,
29 Misregistered,
30 Failed,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub enum RealisationKind {
36 Degree,
37 OpenUniversity,
38}
39
40impl RealisationKind {
41 pub fn as_str(self) -> &'static str {
42 match self {
43 Self::Degree => "degree",
44 Self::OpenUniversity => "openUniversity",
45 }
46 }
47}
48
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
51pub enum ProductTokenState {
52 Enabled,
53 Disabled,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
58pub enum ProductDocumentState {
59 Active,
60 Draft,
61 Deleted,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65#[serde(rename_all = "camelCase")]
66pub enum DuplicateDetection {
67 Detect,
68 AllowDoubles,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub enum Ripeness {
76 AtImport,
78 Manual,
80 AutoAfterVerifyCalls { calls: u32 },
83}
84
85#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase")]
87pub enum SubmissionLifecycle {
88 Pending {
89 ripeness: Ripeness,
90 },
91 Registered {
92 attainment_id: String,
93 registered_at: DateTime<Utc>,
94 },
95 Misregistered {
96 attainment_id: String,
97 misregistered_at: DateTime<Utc>,
98 },
99 TimedOutNothingLanded,
100 TimedOutButLanded {
101 ripeness: Ripeness,
102 },
103}
104
105#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
106#[serde(rename_all = "camelCase")]
107pub struct PersonBehaviour {
108 pub ripeness: Option<Ripeness>,
109 pub duplicate_detection: Option<DuplicateDetection>,
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct CourseBehaviour {
117 pub import_allowed: bool,
118}
119
120impl Default for CourseBehaviour {
121 fn default() -> Self {
122 Self {
123 import_allowed: true,
124 }
125 }
126}
127
128pub use super::wire::{CreditRange, DatePeriod, LocalizedName};
130
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct MockPerson {
134 pub student_number: StudentNumber,
135 pub person_id: String,
136 pub first_names: String,
137 pub last_name: String,
138 pub primary_email: String,
139 pub secondary_email: Option<String>,
140 #[serde(default)]
141 pub behaviour: PersonBehaviour,
142 pub owner_user_email: Option<String>,
145}
146
147#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
148#[serde(rename_all = "camelCase")]
149pub struct MockRealisation {
150 pub id: String,
151 pub name: LocalizedName,
152 pub assessment_item_id: String,
153 pub kind: RealisationKind,
154 pub activity_period: DatePeriod,
155 pub grade_scale_id: String,
156 pub credits: CreditRange,
157 pub acceptor_person_id: Option<String>,
158 pub open_university_product_id: Option<String>,
159}
160
161#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162#[serde(rename_all = "camelCase")]
163pub struct MockCourseUnit {
164 pub course_code: CourseCode,
165 pub course_unit_id: String,
166 pub name: LocalizedName,
167 pub realisations: Vec<MockRealisation>,
168 #[serde(default)]
169 pub behaviour: CourseBehaviour,
170 pub owner_course_slug: Option<String>,
173}
174
175impl MockCourseUnit {
176 pub fn realisation(&self, id: &str) -> Option<&MockRealisation> {
177 self.realisations.iter().find(|r| r.id == id)
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(rename_all = "camelCase")]
183pub struct MockEnrolment {
184 pub id: String,
185 pub student_number: StudentNumber,
186 pub course_code: CourseCode,
187 pub realisation_id: String,
188 pub state: EnrolmentState,
189 pub study_right_id: String,
190 pub study_right_validity_period: DatePeriod,
191 pub enrolment_date_time: DateTime<Utc>,
192}
193
194#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct MockAttainment {
197 pub id: String,
198 #[serde(rename = "type")]
199 pub attainment_type: String,
200 pub state: AttainmentState,
201 pub person_id: String,
202 pub student_number: StudentNumber,
203 pub course_code: CourseCode,
204 pub course_unit_id: String,
205 pub assessment_item_id: String,
206 pub course_unit_realisation_id: String,
207 pub attainment_date: NaiveDate,
208 pub registration_date: NaiveDate,
209 pub grade_scale_id: String,
210 pub grade_id: String,
211 pub passed: bool,
212 pub from_submission: Option<String>,
214}
215
216impl MockAttainment {
217 pub fn from_submission(
220 submission: &MockSubmission,
221 attainment_id: &str,
222 state: AttainmentState,
223 defaults: &WorldDefaults,
224 now: DateTime<Utc>,
225 ) -> Self {
226 Self {
227 id: attainment_id.to_string(),
228 attainment_type: "CourseUnitAttainment".to_string(),
229 state,
230 person_id: submission.person_id.clone(),
231 student_number: submission.student_number.clone(),
232 course_code: submission.course_code.clone(),
233 course_unit_id: submission.course_unit_id.clone(),
234 assessment_item_id: submission.assessment_item_id.clone(),
235 course_unit_realisation_id: submission.realisation_id.clone(),
236 attainment_date: submission.attainment_date,
237 registration_date: now.date_naive(),
238 grade_scale_id: submission.grade_scale_id.clone(),
239 grade_id: submission.grade_id.clone(),
240 passed: defaults
241 .scale(&submission.grade_scale_id)
242 .and_then(|scale| scale.grade(&submission.grade_id))
243 .is_some_and(|grade| grade.passed),
244 from_submission: Some(submission.submitted_attainment_id.clone()),
245 }
246 }
247}
248
249#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
250#[serde(rename_all = "camelCase")]
251pub struct MockSubmission {
252 pub submitted_attainment_id: String,
253 pub submitted_attainment_type: String,
254 pub student_number: StudentNumber,
255 pub course_code: CourseCode,
256 pub enrolment_id: String,
257 pub realisation_id: String,
258 pub person_id: String,
260 pub course_unit_id: String,
261 pub assessment_item_id: String,
262 pub attainment_date: NaiveDate,
263 pub attainment_language: String,
264 pub grade_scale_id: String,
265 pub grade_id: String,
266 pub credits: f64,
267 pub lifecycle: SubmissionLifecycle,
268 pub verify_calls: u32,
269 pub id_disclosed_to_client: bool,
270 pub created_at: DateTime<Utc>,
271}
272
273#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
274#[serde(rename_all = "camelCase")]
275pub struct MockProductAccessToken {
276 pub open_university_product_id: String,
277 pub id: String,
278 pub access_token: String,
279 pub state: ProductTokenState,
280 pub document_state: ProductDocumentState,
281}
282
283#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285pub struct Grade {
286 pub id: String,
287 pub rank: i32,
288 pub passed: bool,
289}
290
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292#[serde(rename_all = "camelCase")]
293pub struct GradeScale {
294 pub id: String,
295 #[serde(default)]
297 pub aliases: Vec<String>,
298 pub grades: Vec<Grade>,
299}
300
301impl GradeScale {
302 pub fn answers_to(&self, id: &str) -> bool {
303 self.id == id || self.aliases.iter().any(|alias| alias == id)
304 }
305
306 pub fn grade(&self, grade_id: &str) -> Option<&Grade> {
307 self.grades.iter().find(|grade| grade.id == grade_id)
308 }
309}
310
311#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
314#[serde(rename_all = "camelCase", default)]
315pub struct WorldDefaults {
316 pub accepted_token: String,
317 pub ripeness: Ripeness,
318 pub duplicate_detection: DuplicateDetection,
319 pub grade_scales: Vec<GradeScale>,
320 pub call_log_capacity: usize,
321 pub include_non_enrolled_in_result: bool,
322 pub realisation_id_required: bool,
323 pub static_grade_error_code: Option<String>,
326}
327
328impl Default for WorldDefaults {
329 fn default() -> Self {
330 Self {
331 accepted_token: headless_lms_base::config::MOCK_SUOTAR_TOKEN.to_string(),
332 ripeness: Ripeness::Manual,
333 duplicate_detection: DuplicateDetection::Detect,
334 grade_scales: default_grade_scales(),
335 call_log_capacity: 2000,
336 include_non_enrolled_in_result: false,
337 realisation_id_required: false,
338 static_grade_error_code: None,
339 }
340 }
341}
342
343impl WorldDefaults {
344 pub fn scale(&self, id: &str) -> Option<&GradeScale> {
345 self.grade_scales.iter().find(|scale| scale.answers_to(id))
346 }
347
348 pub fn any_scale_has_grade(&self, grade_id: &str) -> bool {
350 self.grade_scales
351 .iter()
352 .any(|scale| scale.grade(grade_id).is_some())
353 }
354}
355
356pub fn default_grade_scales() -> Vec<GradeScale> {
359 vec![
360 GradeScale {
361 id: "sis-hyl-hyv".to_string(),
362 aliases: vec!["sis-hyv-hyl".to_string()],
363 grades: vec![
364 Grade {
365 id: "0".to_string(),
366 rank: 0,
367 passed: false,
368 },
369 Grade {
370 id: "1".to_string(),
371 rank: 1,
372 passed: true,
373 },
374 ],
375 },
376 GradeScale {
377 id: "sis-0-5".to_string(),
378 aliases: Vec::new(),
379 grades: (0..=5)
380 .map(|value| Grade {
381 id: value.to_string(),
382 rank: value,
383 passed: value >= 1,
384 })
385 .collect(),
386 },
387 ]
388}
389
390#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase")]
392pub struct MissedFault {
393 pub fault_id: String,
394 pub predicate: String,
396}
397
398#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct RecordedFaults {
401 pub applied: Vec<String>,
402 pub shadowed: Vec<String>,
404 pub missed: Vec<MissedFault>,
405}
406
407#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
408#[serde(rename_all = "camelCase")]
409pub struct RecordedItem {
410 pub request_item_id: String,
411 pub student_number: Option<String>,
412 pub course_code: Option<String>,
413 pub submitted_attainment_id: Option<String>,
414 pub product_id: Option<String>,
415 pub status: String,
416 pub code: String,
417}
418
419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
422#[serde(rename_all = "camelCase")]
423pub struct RecordedCall {
424 pub seq: u64,
425 pub received_at: DateTime<Utc>,
426 pub endpoint: SuotarEndpoint,
427 pub correlation_id: Option<String>,
428 pub authorized: bool,
429 pub http_status: u16,
430 pub request_level_code: Option<String>,
431 pub effect: Option<String>,
432 pub raw_body_truncated: String,
433 pub faults: RecordedFaults,
434 pub items: Vec<RecordedItem>,
435}
436
437#[derive(Debug, Clone, PartialEq)]
439pub enum WorldWrite {
440 UpsertSubmission(String),
441 UpsertAttainment(String),
442 IndexSubmission {
443 student_number: StudentNumber,
444 course_code: CourseCode,
445 id: String,
446 },
447 IndexAttainment {
448 student_number: StudentNumber,
449 course_code: CourseCode,
450 id: String,
451 },
452}
453
454#[derive(Debug, Clone, Default)]
456pub struct WorkingSet {
457 pub defaults: WorldDefaults,
458 pub persons: BTreeMap<StudentNumber, MockPerson>,
459 pub course_units: BTreeMap<CourseCode, MockCourseUnit>,
460 pub enrolments: BTreeMap<String, MockEnrolment>,
461 pub attainments: BTreeMap<String, MockAttainment>,
462 pub submissions: BTreeMap<String, MockSubmission>,
463 pub product_tokens: BTreeMap<String, MockProductAccessToken>,
464 pub attainments_by_person_course: BTreeMap<String, Vec<String>>,
466 pub submissions_by_person_course: BTreeMap<String, Vec<String>>,
468 pub enrolments_by_person: BTreeMap<StudentNumber, Vec<String>>,
469 pub enrolments_by_realisation: BTreeMap<String, Vec<String>>,
470 pub writes: Vec<WorldWrite>,
471}
472
473pub fn person_course_key(student_number: &str, course_code: &str) -> String {
474 format!("{student_number}|{course_code}")
475}
476
477impl WorkingSet {
478 pub fn ripeness_for(&self, student_number: &str) -> Ripeness {
479 self.persons
480 .get(student_number)
481 .and_then(|person| person.behaviour.ripeness)
482 .unwrap_or(self.defaults.ripeness)
483 }
484
485 pub fn duplicate_detection_for(&self, student_number: &str) -> DuplicateDetection {
486 self.persons
487 .get(student_number)
488 .and_then(|person| person.behaviour.duplicate_detection)
489 .unwrap_or(self.defaults.duplicate_detection)
490 }
491}