headless_lms_server/controllers/main_frontend/credit_registration_admin/
enrolment_checks.rs1use 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#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
29pub struct EnrolmentCheckRosterCode {
30 pub course_code: String,
31 pub tier: RosterTier,
32 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 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 pub rate_limits: Vec<SuotarEndpointRateLimit>,
56}
57
58#[derive(Debug, Deserialize)]
59pub struct EnrolmentCheckDashboardQuery {
60 window_secs: Option<i64>,
61}
62
63#[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}