Skip to main content

headless_lms_credit_registration/use_cases/enrolment_discovery/
listing.rs

1//! Sending one listing request, and recording what its rosters, or its failure, say about each
2//! code.
3
4use headless_lms_models::course_module_suotar_configurations::mark_listing_failed;
5use headless_lms_models::credit_registration_roster_schedules::{
6    mark_alone_failed, mark_attempted, mark_batch_failed, mark_fetched, mark_window_closed,
7};
8use headless_lms_models::credit_registrations::CreditRegistrationErrorCode;
9use headless_lms_models::library::credit_registration::outcomes::request_level_code;
10use sqlx::{PgConnection, PgPool};
11
12use super::CodeListing;
13use super::reconcile::reconcile_roster;
14use crate::error::CreditRegistrationResult;
15use crate::registry::{RegistryError, RosterCode, StudyRegistry};
16use crate::workflow::Counts;
17
18/// The code Suotar answers for a code it holds no realisation of, which it stops holding two months
19/// after the last one ends.
20const NO_REALISATION_CODE: CreditRegistrationErrorCode =
21    CreditRegistrationErrorCode::CourseCodeNotFound;
22
23/// Sends one listing request and reconciles what came back.
24pub(super) async fn fetch_course_roster<R: StudyRegistry>(
25    pool: &PgPool,
26    registry: &mut R,
27    request: &[CodeListing],
28    is_account_linking_enabled: bool,
29) -> CreditRegistrationResult<Counts> {
30    let codes: Vec<String> = request
31        .iter()
32        .map(|listing| listing.code.course_code.as_str().to_string())
33        .collect();
34    let module_count: usize = request.iter().map(|listing| listing.modules.len()).sum();
35    let attempted = i32::try_from(module_count).unwrap_or(i32::MAX);
36    {
37        let mut conn = pool.acquire().await?;
38        mark_attempted(&mut conn, &codes).await?;
39    }
40    let roster_codes: Vec<RosterCode> =
41        request.iter().map(|listing| listing.code.clone()).collect();
42    let listed = registry.list_course_roster(&roster_codes).await;
43    let mut conn = pool.acquire().await?;
44    let roster_listing = match listed {
45        Ok(roster_listing) => roster_listing,
46        Err(error) => {
47            record_roster_failure(&mut conn, request, &codes, &error).await?;
48            return Ok(Counts::all_failed(attempted));
49        }
50    };
51
52    let duration_ms = i32::try_from(roster_listing.duration.as_millis()).unwrap_or(i32::MAX);
53    let mut items_failed = 0;
54    let mut enrolments = 0;
55    let mut new_mails = 0;
56    // The registry answers one roster per requested code, in request order.
57    for (listing, roster) in request.iter().zip(&roster_listing.rosters) {
58        let course_code = listing.code.course_code.as_str();
59        match roster {
60            Ok(people) => {
61                new_mails +=
62                    reconcile_roster(&mut conn, listing, people, is_account_linking_enabled)
63                        .await?;
64                let person_count = i32::try_from(people.len()).unwrap_or(i32::MAX);
65                enrolments += person_count;
66                mark_fetched(&mut conn, course_code, person_count, duration_ms).await?;
67            }
68            Err(error) => {
69                let error = *error;
70                items_failed += i32::try_from(listing.modules.len()).unwrap_or(i32::MAX);
71                for module in &listing.modules {
72                    mark_listing_failed(&mut conn, module.course_module_id, error).await?;
73                }
74                if error == NO_REALISATION_CODE {
75                    mark_window_closed(&mut conn, course_code).await?;
76                } else {
77                    mark_alone_failed(&mut conn, course_code, error).await?;
78                }
79            }
80        }
81    }
82    let codes = request.len();
83    info!(
84        codes,
85        enrolments,
86        new_mails,
87        duration_ms,
88        "fetched roster: {enrolments} enrolments for {codes} codes, {new_mails} new"
89    );
90    Ok(Counts::processed_with_failures(attempted, items_failed))
91}
92
93/// A request Suotar failed as a whole. Suotar fails every code of a request when one fails, so the
94/// codes of a failed batch are listed on their own from now on, and a code that fails alone backs
95/// off.
96async fn record_roster_failure(
97    conn: &mut PgConnection,
98    request: &[CodeListing],
99    codes: &[String],
100    error: &RegistryError,
101) -> CreditRegistrationResult<()> {
102    let code = request_level_code(error.kind);
103    for module in request.iter().flat_map(|listing| &listing.modules) {
104        mark_listing_failed(conn, module.course_module_id, code).await?;
105    }
106    if !error.blames_request_items() {
107        return Ok(());
108    }
109    match request {
110        [only] => mark_alone_failed(conn, only.code.course_code.as_str(), code).await?,
111        _ => mark_batch_failed(conn, codes, code).await?,
112    }
113    Ok(())
114}