headless_lms_credit_registration/use_cases/enrolment_discovery/
listing.rs1use 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
18const NO_REALISATION_CODE: CreditRegistrationErrorCode =
21 CreditRegistrationErrorCode::CourseCodeNotFound;
22
23pub(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 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
93async 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}