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::{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#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
27pub struct EnrolmentCheckRosterCode {
28 pub course_code: String,
29 pub waiting_count: i32,
32 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 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 pub rate_limits: Vec<SuotarEndpointRateLimit>,
55}
56
57#[derive(Debug, Deserialize)]
58pub struct EnrolmentCheckDashboardQuery {
59 window_secs: Option<i64>,
60}
61
62#[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}