1use std::collections::BTreeMap;
7
8use chrono::NaiveDate;
9
10use crate::prelude::*;
11
12use super::ids;
13use super::wire::{ASSESSMENT_ITEM_ATTAINMENT, COURSE_UNIT_ATTAINMENT, Endpoint};
14pub use super::wire::{CreditRange, DatePeriod, LocalizedName};
15
16pub type StudentNumber = String;
17pub type CourseCode = String;
18
19pub const REFUSED_COURSE_CODES: [&str; 9] = [
21 "TKT21018",
22 "AYTKT21018",
23 "AYTKT21018fi",
24 "AYTKT21018sv",
25 "TKT210281",
26 "TKT210282",
27 "AYTKT21028en",
28 "AYTKT210281en",
29 "AYTKT210282en",
30];
31
32pub const PENDING_WINDOW_HOURS: i64 = 24;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
37pub enum EnrolmentState {
38 Enrolled,
39 Processing,
40 Rejected,
41 Aborted,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
48pub enum AttainmentState {
49 Attained,
50 Misregistered,
51 Failed,
52}
53
54impl AttainmentState {
55 pub fn wire_state(self) -> &'static str {
56 match self {
57 Self::Attained | Self::Misregistered => "ATTAINED",
58 Self::Failed => "FAILED",
59 }
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(rename_all = "camelCase")]
67pub enum RealisationKind {
68 Degree,
69 OpenUniversity,
70}
71
72impl RealisationKind {
73 pub fn as_str(self) -> &'static str {
74 match self {
75 Self::Degree => "degree",
76 Self::OpenUniversity => "openUniversity",
77 }
78 }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
83#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
84pub enum SendState {
85 NotSent,
86 Attempted,
87 Accepted,
88 Rejected,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase", tag = "kind")]
94pub enum ImporterVisibility {
95 None,
96 #[serde(rename_all = "camelCase")]
98 Partial {
99 attainment_id: String,
100 },
101 #[serde(rename_all = "camelCase")]
102 Final {
103 attainment_id: String,
104 },
105 #[serde(rename_all = "camelCase")]
106 Misregistered {
107 attainment_id: String,
108 },
109}
110
111#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
112#[serde(rename_all = "camelCase", default)]
113pub struct PersonBehaviour {
114 pub study_right_unresolvable: bool,
117}
118
119#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
120#[serde(rename_all = "camelCase", default)]
121pub struct CourseBehaviour {
122 pub no_acceptors: bool,
124 pub acceptor_lookup_fails: bool,
126}
127
128#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
129#[serde(rename_all = "camelCase")]
130pub struct MockPerson {
131 pub student_number: StudentNumber,
132 pub person_id: String,
133 pub first_names: Option<String>,
134 pub last_name: Option<String>,
135 pub primary_email: Option<String>,
136 pub secondary_email: Option<String>,
137 #[serde(default)]
138 pub behaviour: PersonBehaviour,
139 pub owner_user_email: Option<String>,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct MockRealisation {
147 pub id: String,
148 pub name: Option<LocalizedName>,
149 pub assessment_item_id: String,
150 pub kind: RealisationKind,
151 pub activity_period: Option<DatePeriod>,
153 pub grade_scale_id: Option<String>,
155}
156
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct SuotarCourse {
160 pub name: String,
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(rename_all = "camelCase")]
165pub struct MockCourseUnit {
166 pub course_code: CourseCode,
167 pub course_unit_id: String,
168 pub name: LocalizedName,
169 pub credits: Option<CreditRange>,
170 pub grade_scale_id: Option<String>,
171 pub realisations: Vec<MockRealisation>,
172 pub suotar_course: Option<SuotarCourse>,
174 #[serde(default)]
175 pub behaviour: CourseBehaviour,
176 pub owner_course_slug: Option<String>,
179}
180
181impl MockCourseUnit {
182 pub fn realisation(&self, id: &str) -> Option<&MockRealisation> {
183 self.realisations.iter().find(|r| r.id == id)
184 }
185
186 pub fn grade_scale_for(&self, realisation: &MockRealisation) -> Option<String> {
188 realisation
189 .grade_scale_id
190 .clone()
191 .or_else(|| self.grade_scale_id.clone())
192 }
193}
194
195#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
196#[serde(rename_all = "camelCase")]
197pub struct MockStudyRight {
198 pub validity: DatePeriod,
199 pub grant_date: Option<NaiveDate>,
200}
201
202#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
203#[serde(rename_all = "camelCase")]
204pub struct MockEnrolment {
205 pub id: String,
206 pub student_number: StudentNumber,
207 pub course_code: CourseCode,
208 pub realisation_id: String,
209 pub state: EnrolmentState,
210 pub study_right_id: Option<String>,
211 pub study_right: Option<MockStudyRight>,
213 pub enrolment_date_time: Option<DateTime<Utc>>,
215}
216
217impl MockEnrolment {
218 pub fn enrolled_now(
220 student_number: &str,
221 course_code: &str,
222 realisation: &MockRealisation,
223 now: DateTime<Utc>,
224 ) -> Self {
225 Self {
226 id: ids::enrolment_id(student_number, course_code, realisation.kind),
227 student_number: student_number.to_string(),
228 course_code: course_code.to_string(),
229 realisation_id: realisation.id.clone(),
230 state: EnrolmentState::Enrolled,
231 study_right_id: Some(ids::study_right_id(student_number, realisation.kind)),
232 study_right: Some(MockStudyRight {
233 validity: DatePeriod {
234 start_date: (now - chrono::Duration::days(365)).date_naive(),
235 end_date: Some((now + chrono::Duration::days(365)).date_naive()),
236 },
237 grant_date: None,
238 }),
239 enrolment_date_time: Some(now),
240 }
241 }
242
243 pub fn kind(&self) -> &'static str {
245 if self
246 .study_right_id
247 .as_deref()
248 .is_some_and(|id| id.contains("avoin"))
249 {
250 RealisationKind::OpenUniversity.as_str()
251 } else {
252 RealisationKind::Degree.as_str()
253 }
254 }
255}
256
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum AttainmentLevel {
260 Partial,
262 Final,
264}
265
266#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
267#[serde(rename_all = "camelCase")]
268pub struct MockAttainment {
269 pub id: String,
270 #[serde(rename = "type")]
271 pub attainment_type: String,
272 pub state: AttainmentState,
273 pub person_id: String,
274 pub student_number: StudentNumber,
275 pub course_code: CourseCode,
276 pub course_unit_id: String,
277 pub assessment_item_id: Option<String>,
278 pub course_unit_realisation_id: Option<String>,
279 pub attainment_date: NaiveDate,
280 pub registration_date: NaiveDate,
281 pub grade_scale_id: String,
282 pub grade_id: String,
283 pub passed: Option<bool>,
284 pub credits: Option<f64>,
287 pub from_submission: Option<String>,
289}
290
291impl MockAttainment {
292 pub fn from_submission(
295 submission: &MockSubmission,
296 attainment_id: &str,
297 level: AttainmentLevel,
298 state: AttainmentState,
299 defaults: &WorldDefaults,
300 now: DateTime<Utc>,
301 ) -> Self {
302 let (attainment_type, assessment_item_id, course_unit_realisation_id) = match level {
303 AttainmentLevel::Final => (COURSE_UNIT_ATTAINMENT, None, None),
304 AttainmentLevel::Partial => (
305 ASSESSMENT_ITEM_ATTAINMENT,
306 Some(submission.assessment_item_id.clone()),
307 Some(submission.realisation_id.clone()),
308 ),
309 };
310 Self {
311 id: attainment_id.to_string(),
312 attainment_type: attainment_type.to_string(),
313 state,
314 person_id: submission.person_id.clone(),
315 student_number: submission.student_number.clone(),
316 course_code: submission.course_code.clone(),
317 course_unit_id: submission.course_unit_id.clone(),
318 assessment_item_id,
319 course_unit_realisation_id,
320 attainment_date: submission.adjusted_attainment_date,
321 registration_date: now.date_naive(),
322 grade_scale_id: submission.grade_scale_id.clone(),
323 grade_id: submission.grade_id.clone(),
324 passed: defaults
325 .scale(&submission.grade_scale_id)
326 .and_then(|scale| scale.grade(&submission.grade_id))
327 .map(|grade| grade.passed),
328 credits: Some(submission.credits),
329 from_submission: Some(submission.submitted_attainment_id.clone()),
330 }
331 }
332}
333
334#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337#[serde(rename_all = "camelCase")]
338pub struct MockSubmission {
339 pub submitted_attainment_id: String,
341 pub request_item_id: String,
342 pub student_number: StudentNumber,
343 pub course_code: CourseCode,
344 pub enrolment_id: String,
345 pub realisation_id: String,
346 pub person_id: String,
347 pub course_unit_id: String,
348 pub assessment_item_id: String,
349 pub attainment_date: NaiveDate,
350 pub adjusted_attainment_date: NaiveDate,
352 pub attainment_language: String,
353 pub grade_scale_id: String,
354 pub grade_id: String,
355 pub credits: f64,
356 pub send_state: SendState,
357 pub violations: Vec<String>,
358 pub importer: ImporterVisibility,
359 pub created_at: DateTime<Utc>,
360}
361
362#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
363#[serde(rename_all = "camelCase")]
364pub struct Grade {
365 pub id: String,
366 pub passed: bool,
367}
368
369#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
370#[serde(rename_all = "camelCase")]
371pub struct GradeScale {
372 pub id: String,
373 pub grades: Vec<Grade>,
374}
375
376impl GradeScale {
377 pub fn grade(&self, grade_id: &str) -> Option<&Grade> {
378 self.grades.iter().find(|grade| grade.id == grade_id)
379 }
380}
381
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
385#[serde(rename_all = "camelCase", default)]
386pub struct WorldDefaults {
387 pub accepted_token: String,
388 pub grade_scales: Vec<GradeScale>,
390 pub call_log_capacity: usize,
391}
392
393impl Default for WorldDefaults {
394 fn default() -> Self {
395 Self {
396 accepted_token: headless_lms_base::config::MOCK_SUOTAR_TOKEN.to_string(),
397 grade_scales: default_grade_scales(),
398 call_log_capacity: 2000,
399 }
400 }
401}
402
403impl WorldDefaults {
404 pub fn scale(&self, id: &str) -> Option<&GradeScale> {
405 self.grade_scales.iter().find(|scale| scale.id == id)
406 }
407}
408
409pub fn default_grade_scales() -> Vec<GradeScale> {
410 vec![
411 GradeScale {
412 id: "sis-hyl-hyv".to_string(),
413 grades: vec![
414 Grade {
415 id: "0".to_string(),
416 passed: false,
417 },
418 Grade {
419 id: "1".to_string(),
420 passed: true,
421 },
422 ],
423 },
424 GradeScale {
425 id: "sis-0-5".to_string(),
426 grades: (0..=5)
427 .map(|value| Grade {
428 id: value.to_string(),
429 passed: value >= 1,
430 })
431 .collect(),
432 },
433 ]
434}
435
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437#[serde(rename_all = "camelCase")]
438pub struct MissedFault {
439 pub fault_id: String,
440 pub predicate: String,
442}
443
444#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
445#[serde(rename_all = "camelCase")]
446pub struct RecordedFaults {
447 pub applied: Vec<String>,
448 pub shadowed: Vec<String>,
450 pub missed: Vec<MissedFault>,
451}
452
453#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
454#[serde(rename_all = "camelCase")]
455pub struct RecordedItem {
456 pub request_item_id: String,
457 pub student_number: Option<String>,
458 pub course_code: Option<String>,
459 pub submitted_attainment_id: Option<String>,
460 pub status: String,
463 pub code: String,
464}
465
466#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
469#[serde(rename_all = "camelCase")]
470pub struct RecordedCall {
471 pub seq: u64,
472 pub received_at: DateTime<Utc>,
473 pub endpoint: Endpoint,
474 pub correlation_id: Option<String>,
475 pub authorized: bool,
476 pub http_status: u16,
477 pub request_level_code: Option<String>,
478 pub effect: Option<String>,
479 pub raw_body_truncated: String,
480 pub faults: RecordedFaults,
481 pub items: Vec<RecordedItem>,
482}
483
484#[derive(Debug, Clone, PartialEq)]
486pub enum WorldWrite {
487 UpsertSubmission(String),
488 IndexSubmission {
489 student_number: StudentNumber,
490 course_code: CourseCode,
491 },
492}
493
494#[derive(Debug, Clone, Default)]
496pub struct WorkingSet {
497 pub defaults: WorldDefaults,
498 pub persons: BTreeMap<StudentNumber, MockPerson>,
499 pub course_units: BTreeMap<CourseCode, MockCourseUnit>,
500 pub enrolments: BTreeMap<String, MockEnrolment>,
501 pub attainments: BTreeMap<String, MockAttainment>,
502 pub submissions: BTreeMap<String, MockSubmission>,
503 pub sisu_violations: BTreeMap<String, Vec<String>>,
505 pub attainments_by_person_course: BTreeMap<String, Vec<String>>,
507 pub submissions_by_person_course: BTreeMap<String, Vec<String>>,
509 pub enrolments_by_person: BTreeMap<StudentNumber, Vec<String>>,
510 pub enrolments_by_realisation: BTreeMap<String, Vec<String>>,
511 pub writes: Vec<WorldWrite>,
512}
513
514pub fn person_course_key(student_number: &str, course_code: &str) -> String {
515 format!("{student_number}|{course_code}")
516}