Skip to main content

headless_lms_credit_registration/runtime/
manual.rs

1//! The account-linking actions a person sets off by hand, each over a study registry built for
2//! someone waiting on the answer.
3
4use headless_lms_models::library::credit_registration::study_registry::RosterPerson;
5use headless_lms_utils::prelude::{DateTime, Utc};
6use headless_lms_utils::services::suotar::SuotarClient;
7use secrecy::SecretString;
8use sqlx::PgPool;
9use uuid::Uuid;
10
11use super::suotar::InteractiveSuotar;
12use crate::error::CreditRegistrationResult;
13use crate::registry::{InteractiveStudyRegistry, PersonLookupError, RegistryPerson, StudentNumber};
14use crate::use_cases::account_linking::{self, RateCapOverride, ResendAttempt};
15
16/// What a manual action needs from the controller that runs it.
17pub struct ManualActionContext<'a> {
18    pool: &'a PgPool,
19    suotar_client: &'a SuotarClient,
20    caller: &'a str,
21}
22
23impl<'a> ManualActionContext<'a> {
24    /// `caller` is the `worker_name` the action's study registry calls are logged under, one per
25    /// endpoint that sets the action off, e.g. `admin-resend`.
26    pub fn new(pool: &'a PgPool, suotar_client: &'a SuotarClient, caller: &'a str) -> Self {
27        Self {
28            pool,
29            suotar_client,
30            caller,
31        }
32    }
33}
34
35/// Shared by the teacher- and admin-facing resend endpoints: refuses a target that is already
36/// linked, otherwise reruns the send path exactly as the worker would, applying
37/// `rate_cap_override`, if any, only once the registry has named an address to mail.
38pub async fn resend_linking_mail_for_target(
39    ctx: &ManualActionContext<'_>,
40    course_id: Uuid,
41    student_number: &SecretString,
42    rate_cap_override: Option<RateCapOverride<'_>>,
43) -> CreditRegistrationResult<ResendAttempt> {
44    let registry = InteractiveSuotar::new(ctx.suotar_client, ctx.caller.to_string());
45    account_linking::resend_for_target(
46        ctx.pool,
47        &registry,
48        course_id,
49        student_number,
50        rate_cap_override,
51    )
52    .await
53}
54
55/// Looks one student number up in the study registry without changing anything: no ledger row, no
56/// claimed mail slot, just the call log row every study registry call writes.
57///
58/// `Ok(None)` means the registry answered `personNotFound`; `Err` means it gave no usable answer.
59pub async fn look_up_person(
60    ctx: &ManualActionContext<'_>,
61    student_number: &SecretString,
62) -> Result<Option<RegistryPerson>, PersonLookupError> {
63    InteractiveSuotar::new(ctx.suotar_client, ctx.caller.to_string())
64        .look_up_person(&StudentNumber::new(student_number.clone()))
65        .await
66}
67
68/// Lists the rosters of every course code on the course now and keeps the people linked to no
69/// account, whenever they enrolled, each once. Stores nothing.
70///
71/// `Ok(None)` means the registry gave no usable answer for some code.
72pub async fn list_unlinked_on_course_rosters(
73    ctx: &ManualActionContext<'_>,
74    course_id: Uuid,
75) -> CreditRegistrationResult<Option<Vec<RosterPerson>>> {
76    let registry = InteractiveSuotar::new(ctx.suotar_client, ctx.caller.to_string());
77    account_linking::unlinked_on_course_rosters(ctx.pool, &registry, course_id).await
78}
79
80/// Lists one course code's roster now and keeps the people linking mails skip: enrolled before
81/// `since` or at no known time, and linked to no account, each once. Stores nothing.
82///
83/// `Ok(None)` means the registry gave no usable answer.
84pub async fn list_unlinked_enrolled_before(
85    ctx: &ManualActionContext<'_>,
86    course_code: &str,
87    since: DateTime<Utc>,
88) -> CreditRegistrationResult<Option<Vec<RosterPerson>>> {
89    let registry = InteractiveSuotar::new(ctx.suotar_client, ctx.caller.to_string());
90    account_linking::unlinked_enrolled_before(ctx.pool, &registry, course_code, since).await
91}