1use chrono::{DateTime, Duration, Utc};
25use uuid::Uuid;
26
27use super::commands::{
28 CourseUnitUpsert, EnrolmentUpsert, PersonUpsert, ProductAccessTokenUpsert, RealisationUpsert,
29 WorldPush,
30};
31use super::ids as mock_ids;
32use super::world::{
33 CourseBehaviour, CreditRange, DatePeriod, EnrolmentState, LocalizedName, PersonBehaviour,
34 RealisationKind, Ripeness, WorldDefaults,
35};
36
37pub const SUOTAR_COURSE_ID: Uuid = Uuid::from_u128(0xc5ed17ea_0001_4a5e_9e6e_c0de00000001);
39pub const OLD_FLOW_COURSE_ID: Uuid = Uuid::from_u128(0xc5ed17ea_0002_4a5e_9e6e_c0de00000002);
41pub const IMPORT_OUTCOMES_COURSE_ID: Uuid = Uuid::from_u128(0xc5ed17ea_0004_4a5e_9e6e_c0de00000004);
43pub const GRADE_IMPROVEMENT_COURSE_ID: Uuid =
45 Uuid::from_u128(0xc5ed17ea_0005_4a5e_9e6e_c0de00000005);
46pub const BACKFILL_COURSE_ID: Uuid = Uuid::from_u128(0xc5ed17ea_0003_4a5e_9e6e_c0de00000003);
48pub const ADMIN_COURSE_ID: Uuid = Uuid::from_u128(0xc5ed17ea_0006_4a5e_9e6e_c0de00000006);
51pub const STATES_COURSE_ID: Uuid = Uuid::from_u128(0xc5ed17ea_0007_4a5e_9e6e_c0de00000007);
54pub const RETRY_COURSE_ID: Uuid = Uuid::from_u128(0xc5ed17ea_0009_4a5e_9e6e_c0de00000009);
56
57pub const CRS_101: &str = "CRS-101";
58pub const CRS_102: &str = "CRS-102";
59pub const CRS_OLD_101: &str = "CRS-OLD-101";
60pub const CRS_OLD_102: &str = "CRS-OLD-102";
63pub const CRS_BACKFILL_101: &str = "CRS-BACKFILL-101";
64pub const CRS_ADMIN_101: &str = "CRS-ADMIN-101";
65pub const CRS_STATES_101: &str = "CRS-STATES-101";
66pub const CRS_RETRY_101: &str = "CRS-RETRY-101";
67
68pub const CRS_GRADED_101: &str = "CRS-GRADED-101";
69pub const CRS_IMPORT_101: &str = "CRS-IMPORT-101";
71pub const CRS_IMPORT_102: &str = "CRS-IMPORT-102";
72pub const CRS_IMPORT_103: &str = "CRS-IMPORT-103";
73pub const CRS_IMPORT_104: &str = "CRS-IMPORT-104";
74pub const IMPORT_OUTCOME_COURSE_CODES: [&str; 4] = [
75 CRS_IMPORT_101,
76 CRS_IMPORT_102,
77 CRS_IMPORT_103,
78 CRS_IMPORT_104,
79];
80
81pub const SUOTAR_COURSE_SLUG: &str = "credit-registration-via-suotar";
82pub const OLD_FLOW_COURSE_SLUG: &str = "credit-registration-old-flow";
83pub const BACKFILL_COURSE_SLUG: &str = "credit-registration-backfill";
84pub const IMPORT_OUTCOMES_COURSE_SLUG: &str = "credit-registration-import-outcomes";
85pub const GRADE_IMPROVEMENT_COURSE_SLUG: &str = "credit-registration-grade-improvement";
86pub const ADMIN_COURSE_SLUG: &str = "credit-registration-admin";
87pub const STATES_COURSE_SLUG: &str = "credit-registration-states";
88pub const RETRY_COURSE_SLUG: &str = "credit-registration-retry";
89
90pub struct MockPersonFixture {
93 pub student_number: &'static str,
94 pub first_names: &'static str,
95 pub last_name: &'static str,
96 pub sisu_email: &'static str,
98 pub account_email: Option<&'static str>,
99}
100
101impl MockPersonFixture {
102 pub fn sisu_person_id(&self) -> String {
103 format!("hy-hlo-{}", self.student_number)
104 }
105}
106
107pub const LINKED_STUDENT: MockPersonFixture = MockPersonFixture {
110 student_number: "900000101",
111 first_names: "Zzyzx",
112 last_name: "Numberlinked",
113 sisu_email: "zzyzx.numberlinked@helsinki.example",
114 account_email: Some("credit-registration-linked-student@example.com"),
115};
116pub const UNLINKED_STUDENT: MockPersonFixture = MockPersonFixture {
118 student_number: "900000102",
119 first_names: "Zzyzx",
120 last_name: "Linkpending",
121 sisu_email: "zzyzx.linkpending@helsinki.example",
122 account_email: Some("credit-registration-unlinked-student@example.com"),
123};
124pub const IMPORT_TIMEOUT: MockPersonFixture = MockPersonFixture {
133 student_number: "900000402",
134 first_names: "Zzyzx",
135 last_name: "Timedout",
136 sisu_email: "zzyzx.timedout@helsinki.example",
137 account_email: Some("credit-registration-import-timeout@example.com"),
138};
139pub const SISU_OUTAGE: MockPersonFixture = MockPersonFixture {
144 student_number: "900000601",
145 first_names: "Zzyzx",
146 last_name: "Outaged",
147 sisu_email: "zzyzx.outaged@helsinki.example",
148 account_email: Some("credit-registration-sisu-outage@example.com"),
149};
150pub const NO_ENROLMENT: MockPersonFixture = MockPersonFixture {
153 student_number: "900000301",
154 first_names: "Zzyzx",
155 last_name: "Unenrolled",
158 sisu_email: "zzyzx.unenrolled@helsinki.example",
159 account_email: Some("credit-registration-no-enrolment@example.com"),
160};
161pub const TWO_ENROLMENTS: MockPersonFixture = MockPersonFixture {
164 student_number: "900000302",
165 first_names: "Zzyzx",
166 last_name: "Twicenrolled",
167 sisu_email: "zzyzx.twicenrolled@helsinki.example",
168 account_email: Some("credit-registration-two-enrolments@example.com"),
169};
170pub const VERIFY_POLLING: MockPersonFixture = MockPersonFixture {
171 student_number: "900000501",
172 first_names: "Zzyzx",
173 last_name: "Polling",
174 sisu_email: "zzyzx.polling@helsinki.example",
175 account_email: Some("credit-registration-verify-polling@example.com"),
176};
177pub const VERIFY_MISREGISTERED: MockPersonFixture = MockPersonFixture {
178 student_number: "900000502",
179 first_names: "Zzyzx",
180 last_name: "Reversed",
181 sisu_email: "zzyzx.reversed@helsinki.example",
182 account_email: Some("credit-registration-verify-misregistered@example.com"),
183};
184pub const ADMIN_UNLINKED: MockPersonFixture = MockPersonFixture {
185 student_number: "900000902",
186 first_names: "Zzyzx",
187 last_name: "Unlinked",
188 sisu_email: "zzyzx.unlinked@helsinki.example",
189 account_email: Some("credit-registration-admin-unlinked@example.com"),
190};
191pub const ADMIN_STALE: MockPersonFixture = MockPersonFixture {
194 student_number: "900000903",
195 first_names: "Zzyzx",
196 last_name: "Deadaddress",
197 sisu_email: "zzyzx.deadaddress@helsinki.example",
198 account_email: None,
199};
200pub const TEACHER_RESEND_CAPPED: MockPersonFixture = MockPersonFixture {
203 student_number: "900000804",
204 first_names: "Zzyzx",
205 last_name: "Cappedmail",
206 sisu_email: "zzyzx.cappedmail@helsinki.example",
207 account_email: None,
208};
209
210pub const ADMIN_LINKED: MockPersonFixture = MockPersonFixture {
211 student_number: "900000904",
212 first_names: "Zzyzx",
213 last_name: "Alreadylinked",
214 sisu_email: "zzyzx.alreadylinked@helsinki.example",
215 account_email: Some("credit-registration-admin-linked@example.com"),
216};
217
218pub const MAILED_ADDRESS_SUFFIXES: [&str; 3] = ["", "old.", "older."];
221
222pub const PROFILE_EMPTY_EMAIL: &str = "credit-registration-profile-empty@example.com";
224
225pub const LINK_CLAIMER_EMAIL: &str = "credit-registration-link-claimer@example.com";
228pub const LINK_VALID: MockPersonFixture = MockPersonFixture {
229 student_number: "900000201",
230 first_names: "Zzyzx",
231 last_name: "Linkvalid",
232 sisu_email: "zzyzx.linkvalid@helsinki.example",
233 account_email: None,
234};
235pub const LINK_EXPIRED: MockPersonFixture = MockPersonFixture {
236 student_number: "900000202",
237 first_names: "Zzyzx",
238 last_name: "Linkexpired",
239 sisu_email: "zzyzx.linkexpired@helsinki.example",
240 account_email: None,
241};
242pub const LINK_USED: MockPersonFixture = MockPersonFixture {
243 student_number: "900000203",
244 first_names: "Zzyzx",
245 last_name: "Linkused",
246 sisu_email: "zzyzx.linkused@helsinki.example",
247 account_email: None,
248};
249pub const SUPERSEDED: MockPersonFixture = MockPersonFixture {
250 student_number: "900000901",
251 first_names: "Zzyzx",
252 last_name: "Regraded",
253 sisu_email: "zzyzx.regraded@helsinki.example",
254 account_email: Some("credit-registration-superseded@example.com"),
255};
256pub const FAST_TRACK_VERIFIED: MockPersonFixture = MockPersonFixture {
259 student_number: "900001401",
260 first_names: "Zzyzx",
261 last_name: "Fasttrack",
262 sisu_email: "credit-registration-verified-email@example.com",
263 account_email: Some("credit-registration-verified-email@example.com"),
264};
265pub const FAST_TRACK_TWIN: MockPersonFixture = MockPersonFixture {
266 student_number: "900001402",
267 first_names: "Zzyzx",
268 last_name: "Nearmiss",
269 sisu_email: "credit-registration-unverified-twin@example.com",
270 account_email: Some("credit-registration-unverified-twin@example.com"),
271};
272pub const FAST_TRACK_STALE: MockPersonFixture = MockPersonFixture {
275 student_number: "900001403",
276 first_names: "Zzyzx",
277 last_name: "Staleproof",
278 sisu_email: "credit-registration-stale-proof@example.com",
279 account_email: Some("credit-registration-stale-proof@example.com"),
280};
281pub const FAST_TRACK_NAME_MISMATCH: MockPersonFixture = MockPersonFixture {
284 student_number: "900001404",
285 first_names: "Zzyzx",
286 last_name: "Registryname",
287 sisu_email: "credit-registration-name-mismatch@example.com",
288 account_email: Some("credit-registration-name-mismatch@example.com"),
289};
290pub const FAST_TRACK_HAS_NUMBER: MockPersonFixture = MockPersonFixture {
293 student_number: "900001405",
294 first_names: "Zzyzx",
295 last_name: "Alreadynumbered",
296 sisu_email: "credit-registration-has-number@example.com",
297 account_email: Some("credit-registration-has-number@example.com"),
298};
299pub const FAST_TRACK_OTHER_NUMBER: &str = "900001495";
301pub const FAST_TRACK_SECONDARY_ONLY: MockPersonFixture = MockPersonFixture {
304 student_number: "900001406",
305 first_names: "Zzyzx",
306 last_name: "Secondonly",
307 sisu_email: "zzyzx.secondonly@helsinki.example",
308 account_email: Some("credit-registration-secondary-only@example.com"),
309};
310pub const FAST_TRACK_NO_MATCH: MockPersonFixture = MockPersonFixture {
313 student_number: "900001407",
314 first_names: "Zzyzx",
315 last_name: "Othermailbox",
316 sisu_email: "zzyzx.othermailbox@helsinki.example",
317 account_email: Some("credit-registration-fast-track-no-match@example.com"),
318};
319
320pub const BANNER_STUCK: MockPersonFixture = MockPersonFixture {
323 student_number: "900001501",
324 first_names: "Zzyzx",
325 last_name: "Bannerstuck",
326 sisu_email: "zzyzx.bannerstuck@helsinki.example",
327 account_email: Some("credit-registration-banner-stuck@example.com"),
328};
329pub const BANNER_REENROLS: MockPersonFixture = MockPersonFixture {
332 student_number: "900001502",
333 first_names: "Zzyzx",
334 last_name: "Bannerreenrols",
335 sisu_email: "zzyzx.bannerreenrols@helsinki.example",
336 account_email: Some("credit-registration-banner-reenrols@example.com"),
337};
338
339pub const EMAILS_REGISTERED: MockPersonFixture = MockPersonFixture {
341 student_number: "900001301",
342 first_names: "Zzyzx",
343 last_name: "Mailedsuccess",
344 sisu_email: "zzyzx.mailedsuccess@helsinki.example",
345 account_email: Some("credit-registration-emails-registered@example.com"),
346};
347pub const EMAILS_NO_ENROLMENT: MockPersonFixture = MockPersonFixture {
350 student_number: "900001302",
351 first_names: "Zzyzx",
352 last_name: "Mailedaction",
353 sisu_email: "zzyzx.mailedaction@helsinki.example",
354 account_email: Some("credit-registration-emails-no-enrolment@example.com"),
355};
356
357pub const IMPORT_OUTCOMES: MockPersonFixture = MockPersonFixture {
358 student_number: "900000401",
359 first_names: "Zzyzx",
360 last_name: "Importoutcomes",
361 sisu_email: "zzyzx.importoutcomes@helsinki.example",
362 account_email: Some("credit-registration-import-outcomes@example.com"),
363};
364pub const GRADE_IMPROVEMENT: MockPersonFixture = MockPersonFixture {
365 student_number: "900001201",
366 first_names: "Zzyzx",
367 last_name: "Improved",
368 sisu_email: "zzyzx.improved@helsinki.example",
369 account_email: Some("credit-registration-grade-improvement@example.com"),
370};
371
372pub const BACKFILL_STUDENTS: [MockPersonFixture; 4] = [
373 MockPersonFixture {
374 student_number: "900001101",
375 first_names: "Zzyzx",
376 last_name: "Backfill1",
377 sisu_email: "zzyzx.backfill1@helsinki.example",
378 account_email: Some("credit-registration-backfill-1@example.com"),
379 },
380 MockPersonFixture {
381 student_number: "900001102",
382 first_names: "Zzyzx",
383 last_name: "Backfill2",
384 sisu_email: "zzyzx.backfill2@helsinki.example",
385 account_email: Some("credit-registration-backfill-2@example.com"),
386 },
387 MockPersonFixture {
388 student_number: "900001103",
389 first_names: "Zzyzx",
390 last_name: "Backfill3",
391 sisu_email: "zzyzx.backfill3@helsinki.example",
392 account_email: Some("credit-registration-backfill-3@example.com"),
393 },
394 MockPersonFixture {
395 student_number: "900001104",
396 first_names: "Zzyzx",
397 last_name: "Backfill4",
398 sisu_email: "zzyzx.backfill4@helsinki.example",
399 account_email: Some("credit-registration-backfill-4@example.com"),
400 },
401];
402
403pub fn mock_suotar_world() -> WorldPush {
408 let now = Utc::now();
409 let wide = DatePeriod {
410 start_date: (now - Duration::days(730)).date_naive(),
411 end_date: (now + Duration::days(730)).date_naive(),
412 };
413 let past = DatePeriod {
414 start_date: (now - Duration::days(900)).date_naive(),
415 end_date: (now - Duration::days(800)).date_naive(),
416 };
417
418 let on_crs_101 = [
419 &LINKED_STUDENT,
420 &UNLINKED_STUDENT,
421 &LINK_VALID,
422 &LINK_EXPIRED,
423 &LINK_USED,
424 &SUPERSEDED,
425 &FAST_TRACK_VERIFIED,
426 &FAST_TRACK_TWIN,
427 &FAST_TRACK_STALE,
428 &FAST_TRACK_NAME_MISMATCH,
429 &FAST_TRACK_HAS_NUMBER,
430 &FAST_TRACK_SECONDARY_ONLY,
431 &FAST_TRACK_NO_MATCH,
432 &IMPORT_TIMEOUT,
433 &SISU_OUTAGE,
434 &TWO_ENROLMENTS,
435 &VERIFY_POLLING,
436 &VERIFY_MISREGISTERED,
437 &EMAILS_REGISTERED,
438 ];
439 let on_crs_admin_101 = [
440 &ADMIN_UNLINKED,
441 &ADMIN_STALE,
442 &ADMIN_LINKED,
443 &TEACHER_RESEND_CAPPED,
444 ];
445
446 let mut persons: Vec<PersonUpsert> = on_crs_101.iter().map(|f| person(f)).collect();
447 persons.extend(on_crs_admin_101.iter().map(|f| {
448 let mut upsert = person(f);
449 if f.student_number == ADMIN_STALE.student_number
453 || f.student_number == TEACHER_RESEND_CAPPED.student_number
454 {
455 upsert.primary_email = format!("current.{}", f.sisu_email);
456 }
457 upsert
458 }));
459 for upsert in &mut persons {
460 if upsert.student_number == FAST_TRACK_SECONDARY_ONLY.student_number {
463 upsert.secondary_email = FAST_TRACK_SECONDARY_ONLY.account_email.map(str::to_string);
464 }
465 }
466 persons.push(person(&NO_ENROLMENT));
467 persons.push(person(&EMAILS_NO_ENROLMENT));
468 persons.push(person(&BANNER_STUCK));
469 persons.push(person(&BANNER_REENROLS));
470 persons.push(person(&IMPORT_OUTCOMES));
471 persons.push(person(&GRADE_IMPROVEMENT));
472 persons.extend(BACKFILL_STUDENTS.iter().map(person));
473
474 let mut enrolments: Vec<EnrolmentUpsert> = on_crs_101
475 .iter()
476 .filter(|fixture| {
479 fixture.student_number != IMPORT_TIMEOUT.student_number
480 && fixture.student_number != SISU_OUTAGE.student_number
481 })
482 .map(|fixture| enrolment(fixture, CRS_101, RealisationKind::Degree, wide.clone(), now))
483 .collect();
484 enrolments.extend(BACKFILL_STUDENTS.iter().map(|fixture| {
485 enrolment(
486 fixture,
487 CRS_BACKFILL_101,
488 RealisationKind::Degree,
489 wide.clone(),
490 now,
491 )
492 }));
493 enrolments.extend(IMPORT_OUTCOME_COURSE_CODES.iter().map(|course_code| {
494 EnrolmentUpsert {
495 id: Some(mock_ids::enrolment_id_for_course(
498 IMPORT_OUTCOMES.student_number,
499 course_code,
500 RealisationKind::Degree,
501 )),
502 ..enrolment(
503 &IMPORT_OUTCOMES,
504 course_code,
505 RealisationKind::Degree,
506 wide.clone(),
507 now,
508 )
509 }
510 }));
511 enrolments.push(enrolment(
512 &GRADE_IMPROVEMENT,
513 CRS_GRADED_101,
514 RealisationKind::Degree,
515 wide.clone(),
516 now,
517 ));
518 enrolments.extend(on_crs_admin_101.iter().map(|fixture| {
519 enrolment(
520 fixture,
521 CRS_ADMIN_101,
522 RealisationKind::Degree,
523 wide.clone(),
524 now,
525 )
526 }));
527 enrolments.push(enrolment(
528 &TWO_ENROLMENTS,
529 CRS_101,
530 RealisationKind::OpenUniversity,
531 wide.clone(),
532 now,
533 ));
534
535 let import_outcomes = [
536 CourseUnitShape {
537 import_allowed: false,
538 ..CourseUnitShape::new(CRS_IMPORT_101, IMPORT_OUTCOMES_COURSE_SLUG, 5.0)
539 },
540 CourseUnitShape {
541 credits: Some(CreditRange { min: 1.0, max: 1.0 }),
542 ..CourseUnitShape::new(CRS_IMPORT_102, IMPORT_OUTCOMES_COURSE_SLUG, 5.0)
543 },
544 CourseUnitShape {
545 activity_period: Some(past),
546 ..CourseUnitShape::new(CRS_IMPORT_103, IMPORT_OUTCOMES_COURSE_SLUG, 5.0)
547 },
548 CourseUnitShape {
549 acceptor: false,
550 ..CourseUnitShape::new(CRS_IMPORT_104, IMPORT_OUTCOMES_COURSE_SLUG, 5.0)
551 },
552 ];
553
554 let mut course_units = vec![
555 CourseUnitShape {
556 kinds: &[RealisationKind::Degree, RealisationKind::OpenUniversity],
557 ..CourseUnitShape::new(CRS_101, SUOTAR_COURSE_SLUG, 5.0)
558 }
559 .build(&wide),
560 CourseUnitShape::new(CRS_102, SUOTAR_COURSE_SLUG, 3.0).build(&wide),
561 CourseUnitShape {
562 kinds: &[RealisationKind::OpenUniversity],
563 ..CourseUnitShape::new(CRS_OLD_101, OLD_FLOW_COURSE_SLUG, 5.0)
564 }
565 .build(&wide),
566 CourseUnitShape::new(CRS_BACKFILL_101, BACKFILL_COURSE_SLUG, 5.0).build(&wide),
567 CourseUnitShape {
568 kinds: &[RealisationKind::Degree, RealisationKind::OpenUniversity],
569 ..CourseUnitShape::new(CRS_ADMIN_101, ADMIN_COURSE_SLUG, 5.0)
570 }
571 .build(&wide),
572 CourseUnitShape::new(CRS_STATES_101, STATES_COURSE_SLUG, 5.0).build(&wide),
573 CourseUnitShape::new(CRS_RETRY_101, RETRY_COURSE_SLUG, 5.0).build(&wide),
574 CourseUnitShape {
576 grade_scale_id: "sis-0-5",
577 ..CourseUnitShape::new(CRS_GRADED_101, GRADE_IMPROVEMENT_COURSE_SLUG, 5.0)
578 }
579 .build(&wide),
580 ];
581 course_units.extend(import_outcomes.into_iter().map(|shape| shape.build(&wide)));
582
583 WorldPush {
584 defaults: Some(WorldDefaults {
585 ripeness: Ripeness::Manual,
586 ..WorldDefaults::default()
587 }),
588 persons,
589 course_units,
590 enrolments,
591 attainments: Vec::new(),
592 submissions: Vec::new(),
593 product_tokens: vec![
594 product_token(CRS_101),
595 product_token(CRS_OLD_101),
596 product_token(CRS_ADMIN_101),
597 ],
598 }
599}
600
601fn person(fixture: &MockPersonFixture) -> PersonUpsert {
602 PersonUpsert {
603 student_number: fixture.student_number.to_string(),
604 person_id: Some(fixture.sisu_person_id()),
605 first_names: fixture.first_names.to_string(),
606 last_name: fixture.last_name.to_string(),
607 primary_email: fixture.sisu_email.to_string(),
608 secondary_email: None,
609 behaviour: PersonBehaviour::default(),
610 owner_user_email: fixture.account_email.map(str::to_string),
611 }
612}
613
614fn enrolment(
615 fixture: &MockPersonFixture,
616 course_code: &str,
617 kind: RealisationKind,
618 validity: DatePeriod,
619 enrolled_at: DateTime<Utc>,
620) -> EnrolmentUpsert {
621 EnrolmentUpsert {
622 id: None,
623 student_number: fixture.student_number.to_string(),
624 course_code: course_code.to_string(),
625 realisation_id: None,
626 kind,
627 state: EnrolmentState::Enrolled,
628 study_right_id: None,
629 study_right_validity_period: validity,
630 enrolment_date_time: Some(enrolled_at),
631 }
632}
633
634struct CourseUnitShape<'a> {
637 course_code: &'a str,
638 course_slug: &'a str,
639 ects: f64,
640 kinds: &'a [RealisationKind],
641 grade_scale_id: &'a str,
642 credits: Option<CreditRange>,
643 activity_period: Option<DatePeriod>,
644 acceptor: bool,
645 import_allowed: bool,
646}
647
648impl<'a> CourseUnitShape<'a> {
649 fn new(course_code: &'a str, course_slug: &'a str, ects: f64) -> Self {
650 Self {
651 course_code,
652 course_slug,
653 ects,
654 kinds: &[RealisationKind::Degree],
655 grade_scale_id: "sis-hyl-hyv",
656 credits: None,
657 activity_period: None,
658 acceptor: true,
659 import_allowed: true,
660 }
661 }
662
663 fn build(self, activity_period: &DatePeriod) -> CourseUnitUpsert {
664 let name = LocalizedName {
665 fi: self.course_code.to_string(),
666 sv: self.course_code.to_string(),
667 en: self.course_code.to_string(),
668 };
669 CourseUnitUpsert {
670 course_code: self.course_code.to_string(),
671 course_unit_id: None,
672 name: Some(name),
673 realisations: self
674 .kinds
675 .iter()
676 .map(|kind| RealisationUpsert {
677 id: None,
678 name: None,
679 assessment_item_id: None,
680 kind: *kind,
681 activity_period: self
682 .activity_period
683 .clone()
684 .unwrap_or_else(|| activity_period.clone()),
685 grade_scale_id: self.grade_scale_id.to_string(),
686 credits: self.credits.clone().unwrap_or(CreditRange {
687 min: self.ects,
688 max: self.ects,
689 }),
690 acceptor_person_id: self.acceptor.then(|| "hy-hlo-acceptor".to_string()),
691 open_university_product_id: match kind {
692 RealisationKind::OpenUniversity => Some(product_id(self.course_code)),
693 RealisationKind::Degree => None,
694 },
695 })
696 .collect(),
697 behaviour: CourseBehaviour {
698 import_allowed: self.import_allowed,
699 },
700 owner_course_slug: Some(self.course_slug.to_string()),
701 }
702 }
703}
704
705fn product_token(course_code: &str) -> ProductAccessTokenUpsert {
706 ProductAccessTokenUpsert {
707 open_university_product_id: product_id(course_code),
708 id: None,
709 access_token: None,
710 state: None,
711 document_state: None,
712 }
713}
714
715pub fn product_id(course_code: &str) -> String {
716 format!("otm-product-{}", course_code.to_lowercase())
717}
718
719#[cfg(test)]
720mod tests {
721 use super::*;
722
723 #[test]
725 fn the_pushed_world_is_internally_consistent() {
726 let world = mock_suotar_world();
727 let scales = world
728 .defaults
729 .as_ref()
730 .map(|defaults| defaults.grade_scales.clone())
731 .unwrap_or_default();
732 let product_ids: Vec<&String> = world
733 .product_tokens
734 .iter()
735 .map(|token| &token.open_university_product_id)
736 .collect();
737
738 for unit in &world.course_units {
739 for realisation in &unit.realisations {
740 assert!(
741 scales
742 .iter()
743 .any(|scale| scale.answers_to(&realisation.grade_scale_id)),
744 "{} names an unknown grade scale {}",
745 unit.course_code,
746 realisation.grade_scale_id
747 );
748 if let Some(product_id) = &realisation.open_university_product_id {
749 assert!(
750 product_ids.contains(&product_id),
751 "{} references a product token that was not pushed: {product_id}",
752 unit.course_code
753 );
754 }
755 }
756 }
757
758 for enrolment in &world.enrolments {
759 assert!(
760 world
761 .persons
762 .iter()
763 .any(|person| person.student_number == enrolment.student_number),
764 "enrolment for an unpushed person {}",
765 enrolment.student_number
766 );
767 let unit = world
768 .course_units
769 .iter()
770 .find(|unit| unit.course_code == enrolment.course_code)
771 .unwrap_or_else(|| {
772 panic!("enrolment on an unpushed course {}", enrolment.course_code)
773 });
774 assert!(
775 unit.realisations
776 .iter()
777 .any(|realisation| realisation.kind == enrolment.kind),
778 "{} has no {:?} realisation for {} to enrol on",
779 unit.course_code,
780 enrolment.kind,
781 enrolment.student_number
782 );
783 }
784 }
785}