Skip to main content

headless_lms_credit_registration/use_cases/verify/
recovery.rs

1//! Looking for what a submission we lost track of would have created.
2
3use 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
18/// Looks for the attainment a submission we lost track of would have produced. The row stays
19/// `submission_uncertain` unless it is found: never failed, never re-imported.
20pub(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    /// Not the shared request-level outcome either: these rows must stay uncertain whatever the
28    /// call did.
29    const REFUSAL: RefusalPolicy<VerifyAttempt> = RefusalPolicy::KeepWaiting {
30        outcome: still_uncertain,
31        message: "Could not check Sisu for the credits this time.",
32    };
33
34    /// A row with nothing to ask about is left where it is: it is uncertain, which no answer of ours
35    /// may turn into a failure, and its lease already schedules the next check.
36    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}