headless_lms_credit_registration/
phase.rs1use headless_lms_models::credit_registrations::CreditRegistrationState;
4use headless_lms_models::credit_registrations::RegistrationScope;
5use headless_lms_models::library::credit_registration::study_registry::RegistryOperation;
6use headless_lms_models::suotar_circuit_breakers::BreakerTarget;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum CreditRegistrationPhase {
13 Materialize,
14 Preconditions,
15 ResolveEnrolments,
16 Import,
17 Verify,
18 LegacyMirror,
19 StudentNotifications,
20 EnrolmentDiscovery,
21 LinkEmails,
22 ConfigValidation,
23 RetentionSweep,
24 LedgerSnapshot,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum WorkerProcess {
31 CreditRegistrar,
33 SuotarSyncer,
35}
36
37impl WorkerProcess {
38 pub const ALL: [Self; 2] = [Self::CreditRegistrar, Self::SuotarSyncer];
39
40 pub fn as_str(self) -> &'static str {
42 match self {
43 Self::CreditRegistrar => "credit-registrar",
44 Self::SuotarSyncer => "suotar-syncer",
45 }
46 }
47}
48
49#[derive(Debug)]
51pub struct PhaseSpec {
52 pub name: &'static str,
54 pub process: WorkerProcess,
55 pub scope: ScopeSupport,
56 pub operations: &'static [RegistryOperation],
60 pub breakers: &'static [BreakerTarget],
62 pub owned_states: &'static [CreditRegistrationState],
65 pub is_account_linking_only: bool,
69 pub waits_out_sisu_day_gap: bool,
72}
73
74const DEFAULTS: PhaseSpec = PhaseSpec {
77 name: "",
78 process: WorkerProcess::CreditRegistrar,
79 scope: ScopeSupport::LEDGER,
80 operations: &[],
81 breakers: &[],
82 owned_states: &[],
83 is_account_linking_only: false,
84 waits_out_sisu_day_gap: false,
85};
86
87const STUDY_REGISTRY: &[BreakerTarget] = &[BreakerTarget::StudyRegistry];
88
89const COURSE_MODULES: ScopeSupport = ScopeSupport {
92 course: true,
93 user: false,
94 registration_ids: false,
95};
96
97const MATERIALIZE: PhaseSpec = PhaseSpec {
98 name: "materialize",
99 scope: ScopeSupport {
101 course: true,
102 user: true,
103 registration_ids: false,
104 },
105 ..DEFAULTS
106};
107const PRECONDITIONS: PhaseSpec = PhaseSpec {
108 name: "preconditions",
109 owned_states: &[
110 CreditRegistrationState::Pending,
111 CreditRegistrationState::FailedRetryable,
112 CreditRegistrationState::Blocked,
113 ],
114 ..DEFAULTS
115};
116const RESOLVE_ENROLMENTS: PhaseSpec = PhaseSpec {
117 name: "resolve-enrolments",
118 operations: &[
120 RegistryOperation::ResolvePersons,
121 RegistryOperation::ResolveEnrolments,
122 ],
123 breakers: STUDY_REGISTRY,
124 owned_states: &[
125 CreditRegistrationState::ReadyToSubmit,
126 CreditRegistrationState::ResolvingEnrolment,
127 CreditRegistrationState::NoUsableEnrolment,
128 ],
129 ..DEFAULTS
130};
131const IMPORT: PhaseSpec = PhaseSpec {
132 name: "import",
133 operations: &[RegistryOperation::ImportAttainments],
134 breakers: &[BreakerTarget::StudyRegistry, BreakerTarget::SisuSubmissions],
137 owned_states: &[
138 CreditRegistrationState::CheckingEnrolment,
139 CreditRegistrationState::Submitting,
140 ],
141 waits_out_sisu_day_gap: true,
142 ..DEFAULTS
143};
144const VERIFY: PhaseSpec = PhaseSpec {
145 name: "verify",
146 operations: &[
148 RegistryOperation::VerifyAttainments,
149 RegistryOperation::ResolveEnrolments,
150 ],
151 breakers: STUDY_REGISTRY,
152 owned_states: &[
153 CreditRegistrationState::AwaitingVerification,
154 CreditRegistrationState::PartiallyRegistered,
155 CreditRegistrationState::SubmissionUncertain,
156 ],
157 ..DEFAULTS
158};
159const LEGACY_MIRROR: PhaseSpec = PhaseSpec {
160 name: "legacy-mirror",
161 ..DEFAULTS
162};
163const STUDENT_NOTIFICATIONS: PhaseSpec = PhaseSpec {
164 name: "student-notifications",
165 ..DEFAULTS
166};
167const ENROLMENT_DISCOVERY: PhaseSpec = PhaseSpec {
168 name: "enrolment-discovery",
169 process: WorkerProcess::SuotarSyncer,
170 scope: COURSE_MODULES,
171 operations: &[RegistryOperation::ListCourseRoster],
172 breakers: STUDY_REGISTRY,
173 ..DEFAULTS
174};
175const LINK_EMAILS: PhaseSpec = PhaseSpec {
176 name: "link-emails",
177 process: WorkerProcess::SuotarSyncer,
178 scope: COURSE_MODULES,
179 is_account_linking_only: true,
180 ..DEFAULTS
181};
182const CONFIG_VALIDATION: PhaseSpec = PhaseSpec {
183 name: "config-validation",
184 process: WorkerProcess::SuotarSyncer,
185 scope: COURSE_MODULES,
186 operations: &[RegistryOperation::ValidateCourseCodes],
187 breakers: STUDY_REGISTRY,
188 ..DEFAULTS
189};
190const RETENTION_SWEEP: PhaseSpec = PhaseSpec {
191 name: "retention-sweep",
192 process: WorkerProcess::SuotarSyncer,
193 scope: ScopeSupport::NONE,
195 ..DEFAULTS
196};
197const LEDGER_SNAPSHOT: PhaseSpec = PhaseSpec {
198 name: "ledger-snapshot",
199 process: WorkerProcess::SuotarSyncer,
200 scope: ScopeSupport::NONE,
203 ..DEFAULTS
204};
205
206impl CreditRegistrationPhase {
207 pub const ALL: [Self; 12] = [
209 Self::Materialize,
210 Self::Preconditions,
211 Self::ResolveEnrolments,
212 Self::Import,
213 Self::Verify,
214 Self::LegacyMirror,
215 Self::StudentNotifications,
216 Self::EnrolmentDiscovery,
217 Self::LinkEmails,
218 Self::ConfigValidation,
219 Self::RetentionSweep,
220 Self::LedgerSnapshot,
221 ];
222
223 pub fn spec(self) -> &'static PhaseSpec {
225 match self {
226 Self::Materialize => &MATERIALIZE,
227 Self::Preconditions => &PRECONDITIONS,
228 Self::ResolveEnrolments => &RESOLVE_ENROLMENTS,
229 Self::Import => &IMPORT,
230 Self::Verify => &VERIFY,
231 Self::LegacyMirror => &LEGACY_MIRROR,
232 Self::StudentNotifications => &STUDENT_NOTIFICATIONS,
233 Self::EnrolmentDiscovery => &ENROLMENT_DISCOVERY,
234 Self::LinkEmails => &LINK_EMAILS,
235 Self::ConfigValidation => &CONFIG_VALIDATION,
236 Self::RetentionSweep => &RETENTION_SWEEP,
237 Self::LedgerSnapshot => &LEDGER_SNAPSHOT,
238 }
239 }
240
241 pub fn as_str(self) -> &'static str {
243 self.spec().name
244 }
245
246 pub fn from_phase_name(name: &str) -> Option<Self> {
248 Self::ALL.into_iter().find(|phase| phase.as_str() == name)
249 }
250
251 pub fn pipeline_index(self) -> usize {
253 Self::ALL
254 .iter()
255 .position(|phase| *phase == self)
256 .unwrap_or(Self::ALL.len())
257 }
258
259 pub fn owned_states(self) -> &'static [CreditRegistrationState] {
266 self.spec().owned_states
267 }
268
269 pub fn queue_depth(
275 self,
276 depth_of: impl Fn(CreditRegistrationState) -> i64,
277 due_enrolment_checks: i64,
278 ) -> i64 {
279 self.owned_states()
280 .iter()
281 .map(|&state| {
282 if state == CreditRegistrationState::NoUsableEnrolment {
283 due_enrolment_checks
284 } else {
285 depth_of(state)
286 }
287 })
288 .sum()
289 }
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub struct ScopeSupport {
296 pub course: bool,
297 pub user: bool,
298 pub registration_ids: bool,
299}
300
301impl ScopeSupport {
302 pub const NONE: Self = Self {
303 course: false,
304 user: false,
305 registration_ids: false,
306 };
307 pub const LEDGER: Self = Self {
309 course: true,
310 user: true,
311 registration_ids: true,
312 };
313
314 pub(crate) fn covers(self, scope: &RegistrationScope) -> bool {
315 let requested_unsupported = (scope.course_id.is_some() && !self.course)
316 || (scope.user_id.is_some() && !self.user)
317 || (!scope.credit_registration_ids.is_empty() && !self.registration_ids);
318 !requested_unsupported
319 }
320}
321
322#[cfg(test)]
323mod tests {
324 use headless_lms_models::credit_registration_phase_state::PHASES;
325 use uuid::Uuid;
326
327 use super::*;
328
329 #[test]
332 fn phase_names_match_the_seeded_rows() {
333 let from_enum: Vec<&str> = CreditRegistrationPhase::ALL
334 .iter()
335 .map(|phase| phase.as_str())
336 .collect();
337 assert_eq!(from_enum, PHASES);
338 assert_eq!(from_enum.len(), 12);
339 }
340
341 #[test]
344 fn a_phase_refuses_a_scope_it_cannot_apply() {
345 let ids = RegistrationScope {
346 credit_registration_ids: vec![Uuid::new_v4()],
347 ..RegistrationScope::default()
348 };
349 assert!(
350 !CreditRegistrationPhase::Materialize
351 .spec()
352 .scope
353 .covers(&ids)
354 );
355 assert!(CreditRegistrationPhase::Import.spec().scope.covers(&ids));
356 assert!(
357 !CreditRegistrationPhase::RetentionSweep
358 .spec()
359 .scope
360 .covers(&RegistrationScope::for_course(Uuid::new_v4()))
361 );
362 }
363}