Skip to main content

headless_lms_server/controllers/main_frontend/credit_registration_admin/
enrolment_checks.rs

1//! The Enrolment checks tab: how the per-student enrolment checks and the roster listings are
2//! paced, what they cost Suotar and what they find.
3//!
4//! Lateness, findings and detection times come from the enrolment check log, the cost from the call
5//! log, and the limiter and breaker from the state the worker last reported.
6
7use chrono::Duration;
8use headless_lms_models::credit_registration_enrolment_check_outcomes::{
9    self, EnrolmentCheckFindings, EnrolmentCheckLateness, EnrolmentCheckPopulation, VERY_LATE_SECS,
10};
11use headless_lms_models::credit_registration_roster_schedules::{
12    self, RosterTier, ScheduleSelection, roster_tier,
13};
14use headless_lms_models::credit_registrations::CreditRegistrationErrorCode;
15use headless_lms_models::suotar_api_calls::{self, SuotarEndpointDailyCost};
16use headless_lms_models::suotar_endpoint_rate_limits::{self, SuotarEndpointRateLimit};
17use utoipa::ToSchema;
18
19use crate::prelude::*;
20
21use super::authorize_credit_registration_admin;
22
23const DEFAULT_WINDOW_SECS: i64 = 7 * 24 * 60 * 60;
24const MIN_WINDOW_SECS: i64 = 60 * 60;
25const MAX_WINDOW_SECS: i64 = 90 * 24 * 60 * 60;
26
27/// One course code's roster schedule as it stands.
28#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
29pub struct EnrolmentCheckRosterCode {
30    pub course_code: String,
31    pub tier: RosterTier,
32    /// When a trigger or the tier next makes it due; `None` when neither will.
33    pub next_fetch_at: Option<DateTime<Utc>>,
34    pub last_fetched_at: Option<DateTime<Utc>>,
35    pub last_listed_person_count: Option<i32>,
36    pub last_fetch_duration_ms: Option<i32>,
37    pub triggered_fetch_count_today: i32,
38    pub is_fetched_alone: bool,
39    pub consecutive_failures: i32,
40    pub retry_not_before: Option<DateTime<Utc>>,
41    pub last_error: Option<CreditRegistrationErrorCode>,
42    pub module_count: i32,
43}
44
45#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
46pub struct EnrolmentCheckDashboard {
47    /// Lateness past this counts as very late.
48    pub very_late_after_secs: i64,
49    pub lateness: Vec<EnrolmentCheckLateness>,
50    pub findings: Vec<EnrolmentCheckFindings>,
51    pub population: Vec<EnrolmentCheckPopulation>,
52    pub daily_costs: Vec<SuotarEndpointDailyCost>,
53    pub roster_codes: Vec<EnrolmentCheckRosterCode>,
54    /// What the worker last reported; empty until it has run.
55    pub rate_limits: Vec<SuotarEndpointRateLimit>,
56}
57
58#[derive(Debug, Deserialize)]
59pub struct EnrolmentCheckDashboardQuery {
60    window_secs: Option<i64>,
61}
62
63/**
64GET `/api/v0/main-frontend/credit-registration-admin/enrolment-checks` - Lateness, cost, population
65and findings of the enrolment checks, and the roster schedule per course code.
66*/
67#[instrument(skip(pool, app_conf))]
68#[utoipa::path(
69    get,
70    path = "/enrolment-checks",
71    operation_id = "getCreditRegistrationEnrolmentChecks",
72    tag = "credit-registration-admin",
73    params(("window_secs" = Option<i64>, Query, description = "How far back to read checks and calls, in seconds")),
74    responses(
75        (status = 200, description = "The enrolment check dashboard", body = EnrolmentCheckDashboard)
76    )
77)]
78pub async fn get_credit_registration_enrolment_checks(
79    user: AuthUser,
80    pool: web::Data<PgPool>,
81    app_conf: web::Data<ApplicationConfiguration>,
82    query: web::Query<EnrolmentCheckDashboardQuery>,
83) -> ControllerResult<web::Json<EnrolmentCheckDashboard>> {
84    let mut conn = pool.acquire().await?;
85    let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
86
87    let now = Utc::now();
88    let window_secs = query
89        .window_secs
90        .unwrap_or(DEFAULT_WINDOW_SECS)
91        .clamp(MIN_WINDOW_SECS, MAX_WINDOW_SECS);
92    let since = now - Duration::seconds(window_secs);
93    let lateness =
94        credit_registration_enrolment_check_outcomes::get_lateness_since(&mut conn, since).await?;
95    let findings =
96        credit_registration_enrolment_check_outcomes::get_findings_since(&mut conn, since).await?;
97    let population =
98        credit_registration_enrolment_check_outcomes::get_population(&mut conn).await?;
99    let daily_costs = suotar_api_calls::get_daily_costs_since(&mut conn, since).await?;
100    let is_account_linking_enabled = app_conf.suotar_configuration.account_linking_enabled;
101    let today = now.date_naive();
102    let roster_codes = credit_registration_roster_schedules::get_schedules(
103        &mut conn,
104        None,
105        ScheduleSelection::Every,
106    )
107    .await?
108    .into_iter()
109    .map(|schedule| EnrolmentCheckRosterCode {
110        tier: roster_tier(&schedule.tier_facts, is_account_linking_enabled, now),
111        next_fetch_at: schedule.next_fetch_at(is_account_linking_enabled, now),
112        triggered_fetch_count_today: if schedule.triggered_fetch_day == Some(today) {
113            schedule.triggered_fetch_count
114        } else {
115            0
116        },
117        module_count: i32::try_from(schedule.module_count).unwrap_or(i32::MAX),
118        course_code: schedule.course_code,
119        last_fetched_at: schedule.last_fetched_at,
120        last_listed_person_count: schedule.last_listed_person_count,
121        last_fetch_duration_ms: schedule.last_fetch_duration_ms,
122        is_fetched_alone: schedule.is_fetched_alone,
123        consecutive_failures: schedule.consecutive_failures,
124        retry_not_before: schedule.retry_not_before,
125        last_error: schedule.last_error,
126    })
127    .collect();
128    let rate_limits = suotar_endpoint_rate_limits::get_all(&mut conn).await?;
129
130    token.authorized_ok(web::Json(EnrolmentCheckDashboard {
131        very_late_after_secs: VERY_LATE_SECS,
132        lateness,
133        findings,
134        population,
135        daily_costs,
136        roster_codes,
137        rate_limits,
138    }))
139}
140
141pub fn _add_routes(cfg: &mut ServiceConfig) {
142    cfg.route(
143        "/enrolment-checks",
144        web::get().to(get_credit_registration_enrolment_checks),
145    );
146}