headless_lms_credit_registration/use_cases/verify/
lease.rs1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use headless_lms_models::credit_registrations::{
7 VerifyFlow, claim_due_for_verify, increment_verify_attempt_counts, schedule_next_attempts,
8};
9use headless_lms_models::library::credit_registration::course_unit_poll_slots::next_course_unit_polls;
10use headless_lms_models::library::credit_registration::outcomes::{
11 RowFacts, verify_poll_lease_until,
12};
13use sqlx::PgConnection;
14use uuid::Uuid;
15
16use crate::error::CreditRegistrationResult;
17use crate::use_cases::batch_flow::BatchFlowContext;
18use crate::workflow::{Claimed, ClaimedRegistration};
19
20pub(super) type Leased = Claimed<VerifyAttempt>;
22
23pub(super) struct VerifyAttempt {
26 count: i32,
27 course_unit_poll_at: Option<DateTime<Utc>>,
28}
29
30pub(super) fn attempt_facts(leased: &Leased, now: DateTime<Utc>) -> RowFacts {
33 RowFacts {
34 verify_attempt_count: leased.extra.count,
35 course_unit_poll_at: leased.extra.course_unit_poll_at,
36 ..leased.claim.facts(now)
37 }
38}
39
40pub(super) async fn claim_and_lease(
44 ctx: &BatchFlowContext<'_>,
45 conn: &mut PgConnection,
46 flow: VerifyFlow,
47 limit: usize,
48) -> CreditRegistrationResult<Vec<Leased>> {
49 let claimed = claim_due_for_verify(
50 conn,
51 flow,
52 ctx.scope,
53 i64::try_from(limit).unwrap_or(i64::MAX),
54 )
55 .await?;
56 let attempts = increment_verify_attempt_counts(
57 conn,
58 &claimed.iter().map(|row| row.id).collect::<Vec<_>>(),
59 )
60 .await?;
61 let now = Utc::now();
62 let course_unit_polls: HashMap<Uuid, DateTime<Utc>> =
63 if flow == VerifyFlow::Poll && !claimed.is_empty() {
64 let first_seen_at: Vec<_> = claimed
65 .iter()
66 .map(|row| row.partially_registered_at.unwrap_or(now))
67 .collect();
68 claimed
69 .iter()
70 .map(|row| row.id)
71 .zip(next_course_unit_polls(conn, now, &first_seen_at).await?)
72 .collect()
73 } else {
74 HashMap::new()
75 };
76 let leased: Vec<Leased> = claimed
77 .into_iter()
78 .filter_map(|row| {
79 let Some(attempt) = attempts.get(&row.id).copied() else {
82 warn!(
83 credit_registration_id = %row.id,
84 "Verify attempt was not counted for a claimed credit registration; leaving it unpolled"
85 );
86 return None;
87 };
88 let course_unit_poll_at = course_unit_polls.get(&row.id).copied();
89 Some(Claimed {
90 claim: ClaimedRegistration::left_in_place(row),
91 extra: VerifyAttempt {
92 count: attempt,
93 course_unit_poll_at,
94 },
95 })
96 })
97 .collect();
98 let scheduled: Vec<_> = leased
99 .iter()
100 .map(|leased| {
101 let row = leased.claim.registration();
102 (
103 row.id,
104 verify_poll_lease_until(
105 row.state,
106 &attempt_facts(leased, now),
107 ctx.study_registry_wait,
108 ),
109 )
110 })
111 .collect();
112 schedule_next_attempts(conn, &scheduled).await?;
113 Ok(leased)
114}