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 enrolment list fetches
2//! are 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::{self, ScheduleSelection};
12use headless_lms_models::credit_registrations::CreditRegistrationErrorCode;
13use headless_lms_models::suotar_api_calls::{self, SuotarEndpointDailyCost};
14use headless_lms_models::suotar_endpoint_rate_limits::{self, SuotarEndpointRateLimit};
15use utoipa::ToSchema;
16
17use crate::prelude::*;
18
19use super::authorize_credit_registration_admin;
20
21const DEFAULT_WINDOW_SECS: i64 = 7 * 24 * 60 * 60;
22const MIN_WINDOW_SECS: i64 = 60 * 60;
23const MAX_WINDOW_SECS: i64 = 90 * 24 * 60 * 60;
24
25/// One course code's enrolment list schedule as it stands.
26#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
27pub struct EnrolmentCheckRosterCode {
28    pub course_code: String,
29    /// People on the code's modules waiting for a student number, who make it due sooner than
30    /// weekly.
31    pub waiting_count: i32,
32    /// When it is next due, ignoring the failure backoff.
33    pub next_fetch_at: 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 is_fetched_alone: bool,
38    pub consecutive_failures: i32,
39    pub retry_not_before: Option<DateTime<Utc>>,
40    pub last_error: Option<CreditRegistrationErrorCode>,
41    pub module_count: i32,
42}
43
44#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
45pub struct EnrolmentCheckDashboard {
46    /// Lateness past this counts as very late.
47    pub very_late_after_secs: i64,
48    pub lateness: Vec<EnrolmentCheckLateness>,
49    pub findings: Vec<EnrolmentCheckFindings>,
50    pub population: Vec<EnrolmentCheckPopulation>,
51    pub daily_costs: Vec<SuotarEndpointDailyCost>,
52    pub roster_codes: Vec<EnrolmentCheckRosterCode>,
53    /// What the worker last reported; empty until it has run.
54    pub rate_limits: Vec<SuotarEndpointRateLimit>,
55}
56
57#[derive(Debug, Deserialize)]
58pub struct EnrolmentCheckDashboardQuery {
59    window_secs: Option<i64>,
60}
61
62/**
63GET `/api/v0/main-frontend/credit-registration-admin/enrolment-checks` - Lateness, cost, population
64and findings of the enrolment checks, and the enrolment list schedule per course code.
65*/
66#[instrument(skip(pool, app_conf))]
67#[utoipa::path(
68    get,
69    path = "/enrolment-checks",
70    operation_id = "getCreditRegistrationEnrolmentChecks",
71    tag = "credit-registration-admin",
72    params(("window_secs" = Option<i64>, Query, description = "How far back to read checks and calls, in seconds")),
73    responses(
74        (status = 200, description = "The enrolment check dashboard", body = EnrolmentCheckDashboard)
75    )
76)]
77pub async fn get_credit_registration_enrolment_checks(
78    user: AuthUser,
79    pool: web::Data<PgPool>,
80    app_conf: web::Data<ApplicationConfiguration>,
81    query: web::Query<EnrolmentCheckDashboardQuery>,
82) -> ControllerResult<web::Json<EnrolmentCheckDashboard>> {
83    let mut conn = pool.acquire().await?;
84    let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
85
86    let now = Utc::now();
87    let window_secs = query
88        .window_secs
89        .unwrap_or(DEFAULT_WINDOW_SECS)
90        .clamp(MIN_WINDOW_SECS, MAX_WINDOW_SECS);
91    let since = now - Duration::seconds(window_secs);
92    let lateness =
93        credit_registration_enrolment_check_outcomes::get_lateness_since(&mut conn, since).await?;
94    let findings =
95        credit_registration_enrolment_check_outcomes::get_findings_since(&mut conn, since).await?;
96    let population =
97        credit_registration_enrolment_check_outcomes::get_population(&mut conn).await?;
98    let daily_costs = suotar_api_calls::get_daily_costs_since(&mut conn, since).await?;
99    let roster_codes = credit_registration_roster_schedules::get_schedules(
100        &mut conn,
101        None,
102        ScheduleSelection::Every,
103        app_conf.suotar_configuration.account_linking_since,
104    )
105    .await?
106    .into_iter()
107    .map(|schedule| EnrolmentCheckRosterCode {
108        next_fetch_at: schedule.next_fetch_at(now),
109        waiting_count: i32::try_from(schedule.waiting_count).unwrap_or(i32::MAX),
110        module_count: i32::try_from(schedule.module_count).unwrap_or(i32::MAX),
111        course_code: schedule.course_code,
112        last_fetched_at: schedule.last_fetched_at,
113        last_listed_person_count: schedule.last_listed_person_count,
114        last_fetch_duration_ms: schedule.last_fetch_duration_ms,
115        is_fetched_alone: schedule.is_fetched_alone,
116        consecutive_failures: schedule.consecutive_failures,
117        retry_not_before: schedule.retry_not_before,
118        last_error: schedule.last_error,
119    })
120    .collect();
121    let rate_limits = suotar_endpoint_rate_limits::get_all(&mut conn).await?;
122
123    token.authorized_ok(web::Json(EnrolmentCheckDashboard {
124        very_late_after_secs: VERY_LATE_SECS,
125        lateness,
126        findings,
127        population,
128        daily_costs,
129        roster_codes,
130        rate_limits,
131    }))
132}
133
134pub fn _add_routes(cfg: &mut ServiceConfig) {
135    cfg.route(
136        "/enrolment-checks",
137        web::get().to(get_credit_registration_enrolment_checks),
138    );
139}