headless_lms_credit_registration/use_cases/verify/
recovery.rs1use chrono::{DateTime, Utc};
4use headless_lms_models::credit_registrations::VerifyFlow;
5use headless_lms_models::library::credit_registration::outcomes::{
6 Outcome, uncertain_recheck_outcome,
7};
8use headless_lms_models::library::credit_registration::submission_context::get_submission_contexts;
9use sqlx::PgConnection;
10
11use super::decide::decide_recovery;
12use super::lease::{Leased, VerifyAttempt, attempt_facts, claim_and_lease};
13use crate::error::CreditRegistrationResult;
14use crate::registry::{CourseCode, EnrolmentAnswer, EnrolmentLookup, ExchangeAudit, StudentNumber};
15use crate::use_cases::batch_flow::{BatchFlowContext, Prepared, RegistryBatchFlow};
16use crate::workflow::{Applied, RefusalPolicy, write_decision};
17
18pub(super) struct UncertainRecovery;
21
22impl RegistryBatchFlow for UncertainRecovery {
23 type Extra = VerifyAttempt;
24 type Request = EnrolmentLookup;
25
26 const ALL_UNAVAILABLE_ERROR: &'static str = "Every recovery lookup came back unavailable.";
27 const REFUSAL: RefusalPolicy<VerifyAttempt> = RefusalPolicy::KeepWaiting {
30 outcome: still_uncertain,
31 message: "Could not check Sisu for the credits this time.",
32 };
33
34 async fn claim(
37 ctx: &BatchFlowContext<'_>,
38 conn: &mut PgConnection,
39 limit: usize,
40 ) -> CreditRegistrationResult<Prepared<VerifyAttempt, EnrolmentLookup>> {
41 let recoveries = claim_and_lease(ctx, conn, VerifyFlow::UncertainRecovery, limit).await?;
42 let contexts = get_submission_contexts(
43 conn,
44 &recoveries
45 .iter()
46 .map(|recovery| recovery.claim.id())
47 .collect::<Vec<_>>(),
48 )
49 .await?;
50 let mut prepared = Prepared::new();
51 for recovery in recoveries {
52 let row = recovery.claim.registration();
53 let Some(context) = contexts.get(&row.id) else {
54 debug!(
55 credit_registration_id = %row.id,
56 "No completion or module to recover with; leaving row uncertain"
57 );
58 continue;
59 };
60 let (Some(student_number), Some(course_code)) = (
61 row.student_number
62 .clone()
63 .or_else(|| context.student_number.clone()),
64 row.uh_course_code
65 .as_deref()
66 .and_then(CourseCode::parse)
67 .or_else(|| {
68 context
69 .uh_course_code
70 .as_deref()
71 .and_then(CourseCode::parse)
72 }),
73 ) else {
74 debug!(
75 credit_registration_id = %row.id,
76 "No student number or course code to recover with; leaving row uncertain"
77 );
78 continue;
79 };
80 let lookup = EnrolmentLookup {
81 student_number: StudentNumber::new(student_number),
82 course_code,
83 };
84 prepared.send(recovery, lookup);
85 }
86 Ok(prepared)
87 }
88
89 async fn apply_answer(
90 conn: &mut PgConnection,
91 recovery: &Leased,
92 answer: Option<&EnrolmentAnswer>,
93 audit: &ExchangeAudit,
94 ) -> CreditRegistrationResult<Applied> {
95 let facts = attempt_facts(recovery, Utc::now());
96 let decision = decide_recovery(recovery.claim.registration(), &facts, answer);
97 write_decision(conn, &recovery.claim, decision, audit).await
98 }
99}
100
101fn still_uncertain(recovery: &Leased, now: DateTime<Utc>) -> Outcome {
102 uncertain_recheck_outcome(&attempt_facts(recovery, now))
103}