Skip to main content

headless_lms_credit_registration/
attention.rs

1//! The blocking problems the Needs attention queue attributes rows to, read the way the alert
2//! rules read them.
3
4use chrono::{DateTime, Utc};
5use headless_lms_models::credit_registrations::{
6    AttentionRules, BlockingProblems, StoppedPhase, StuckThresholds,
7};
8use headless_lms_models::{
9    ModelResult, course_module_suotar_configurations, credit_registration_phase_state,
10    credit_registration_roster_schedules,
11};
12use sqlx::PgConnection;
13
14use crate::CreditRegistrationPhase;
15
16/// A phase is late once this many of its own intervals have passed without a heartbeat.
17pub const PHASE_HEARTBEAT_INTERVAL_MULTIPLIER: i32 = 2;
18
19/// Whether a phase's last heartbeat is older than [`PHASE_HEARTBEAT_INTERVAL_MULTIPLIER`] of its
20/// intervals. A paused phase is never late.
21pub fn is_heartbeat_late(
22    last_heartbeat_at: Option<DateTime<Utc>>,
23    expected_interval_secs: i32,
24    paused_at: Option<DateTime<Utc>>,
25    now: DateTime<Utc>,
26) -> bool {
27    paused_at.is_none()
28        && last_heartbeat_at.is_some_and(|at| {
29            (now - at).num_seconds()
30                > i64::from(expected_interval_secs) * i64::from(PHASE_HEARTBEAT_INTERVAL_MULTIPLIER)
31        })
32}
33
34/// The rules every Needs attention count is taken under: the shared thresholds, and the blocking
35/// problems as they stand now.
36pub async fn attention_rules(
37    conn: &mut PgConnection,
38    account_linking_since: Option<DateTime<Utc>>,
39) -> ModelResult<AttentionRules> {
40    let now = Utc::now();
41    let stopped_phases = credit_registration_phase_state::get_all(conn)
42        .await?
43        .into_iter()
44        .filter(|row| {
45            row.paused_at.is_some()
46                || is_heartbeat_late(
47                    row.last_heartbeat_at,
48                    row.expected_interval_secs,
49                    row.paused_at,
50                    now,
51                )
52        })
53        .filter_map(|row| {
54            let phase = CreditRegistrationPhase::from_phase_name(&row.phase)?;
55            Some(StoppedPhase {
56                phase: row.phase,
57                owned_states: phase.owned_states().to_vec(),
58                sends_linking_emails: matches!(
59                    phase,
60                    CreditRegistrationPhase::EnrolmentDiscovery
61                        | CreditRegistrationPhase::LinkEmails
62                ),
63            })
64        })
65        .collect();
66    let failing_course_codes = credit_registration_roster_schedules::get_failing_codes(conn)
67        .await?
68        .into_iter()
69        .map(|code| code.course_code)
70        .collect();
71    let misconfigured_module_ids =
72        course_module_suotar_configurations::get_module_ids_failing_config_check(conn).await?;
73    Ok(AttentionRules {
74        thresholds: StuckThresholds::CURRENT,
75        account_linking_since,
76        blocking: BlockingProblems {
77            stopped_phases,
78            failing_course_codes,
79            misconfigured_module_ids,
80        },
81    })
82}