Skip to main content

headless_lms_server/controllers/main_frontend/credit_registration_admin/
history.rs

1//! The Pipeline tab's history: queue depth per state per day, and the flow through each.
2//!
3//! Reads the daily snapshots the `ledger-snapshot` phase writes. The ledger holds current state
4//! only, so a row that passed through a state in an hour leaves no depth trace there — history
5//! cannot be reconstructed from it, which is why the snapshots exist.
6
7use chrono::{Duration, NaiveDate};
8use headless_lms_models::credit_registration_daily_snapshots;
9use headless_lms_models::credit_registrations::CreditRegistrationState;
10use utoipa::ToSchema;
11
12use crate::prelude::*;
13
14use super::authorize_credit_registration_admin;
15
16const DEFAULT_DAYS: i64 = 30;
17const MAX_DAYS: i64 = 365;
18
19/// One state's depth and flow on one day.
20#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
21pub struct CreditRegistrationHistoryPoint {
22    pub state: CreditRegistrationState,
23    /// Rows in the state when the snapshot was taken, once that day.
24    pub count: i32,
25    /// Transitions into and out of the state during that UTC day. A self-transition is neither.
26    pub entered_count: i32,
27    pub left_count: i32,
28}
29
30#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
31pub struct CreditRegistrationHistoryDay {
32    /// The UTC day the snapshot describes.
33    pub snapshot_date: NaiveDate,
34    /// Every state, whether or not anything is in it: a missing state would read as a gap in the
35    /// chart rather than as an empty queue.
36    pub states: Vec<CreditRegistrationHistoryPoint>,
37}
38
39#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
40pub struct CreditRegistrationHistory {
41    /// Oldest first. Days with no snapshot are absent rather than zeroed: before the phase first
42    /// ran, and on a day it did not, there is no depth to report.
43    pub days: Vec<CreditRegistrationHistoryDay>,
44    pub from: NaiveDate,
45    pub to: NaiveDate,
46}
47
48#[derive(Debug, Deserialize)]
49pub struct HistoryQuery {
50    days: Option<i64>,
51}
52
53/**
54GET `/api/v0/main-frontend/credit-registration-admin/pipeline-history` - Daily queue depth per
55ledger state, with what entered and left each state that day.
56*/
57#[instrument(skip(pool))]
58#[utoipa::path(
59    get,
60    path = "/pipeline-history",
61    operation_id = "getCreditRegistrationPipelineHistory",
62    tag = "credit-registration-admin",
63    params(("days" = Option<i64>, Query, description = "How many days back to read, today included")),
64    responses(
65        (status = 200, description = "One entry per day that has a snapshot", body = CreditRegistrationHistory)
66    )
67)]
68pub async fn get_credit_registration_pipeline_history(
69    user: AuthUser,
70    pool: web::Data<PgPool>,
71    query: web::Query<HistoryQuery>,
72) -> ControllerResult<web::Json<CreditRegistrationHistory>> {
73    let mut conn = pool.acquire().await?;
74    let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
75
76    let days = query.days.unwrap_or(DEFAULT_DAYS).clamp(1, MAX_DAYS);
77    let to = Utc::now().date_naive();
78    let from = to - Duration::days(days - 1);
79
80    let mut history: Vec<CreditRegistrationHistoryDay> = Vec::new();
81    for row in credit_registration_daily_snapshots::get_between(&mut conn, from, to).await? {
82        // The model orders by date then state, so a day's rows arrive together.
83        if history
84            .last()
85            .is_none_or(|day| day.snapshot_date != row.snapshot_date)
86        {
87            history.push(CreditRegistrationHistoryDay {
88                snapshot_date: row.snapshot_date,
89                states: Vec::new(),
90            });
91        }
92        if let Some(day) = history.last_mut() {
93            day.states.push(CreditRegistrationHistoryPoint {
94                state: row.state,
95                count: row.count,
96                entered_count: row.entered_count,
97                left_count: row.left_count,
98            });
99        }
100    }
101
102    token.authorized_ok(web::Json(CreditRegistrationHistory {
103        days: history,
104        from,
105        to,
106    }))
107}
108
109pub fn _add_routes(cfg: &mut ServiceConfig) {
110    cfg.route(
111        "/pipeline-history",
112        web::get().to(get_credit_registration_pipeline_history),
113    );
114}