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_utils::services::suotar::SuotarClient;
5use secrecy::SecretString;
6use sqlx::PgPool;
7use uuid::Uuid;
8
9use super::suotar::InteractiveSuotar;
10use crate::error::CreditRegistrationResult;
11use crate::registry::{InteractiveStudyRegistry, PersonLookupError, RegistryPerson, StudentNumber};
12use crate::use_cases::account_linking::{self, RateCapOverride, ResendAttempt};
13
14/// What a manual action needs from the controller that runs it.
15pub struct ManualActionContext<'a> {
16    pool: &'a PgPool,
17    suotar_client: &'a SuotarClient,
18    caller: &'a str,
19}
20
21impl<'a> ManualActionContext<'a> {
22    /// `caller` is the `worker_name` the action's study registry calls are logged under, one per
23    /// endpoint that sets the action off, e.g. `admin-resend`.
24    pub fn new(pool: &'a PgPool, suotar_client: &'a SuotarClient, caller: &'a str) -> Self {
25        Self {
26            pool,
27            suotar_client,
28            caller,
29        }
30    }
31}
32
33/// Shared by the teacher- and admin-facing resend endpoints: refuses a target that is already
34/// linked, otherwise reruns the send path exactly as the worker would, applying
35/// `rate_cap_override`, if any, only once the registry has named an address to mail.
36pub async fn resend_linking_mail_for_target(
37    ctx: &ManualActionContext<'_>,
38    course_id: Uuid,
39    student_number: &SecretString,
40    rate_cap_override: Option<RateCapOverride<'_>>,
41) -> CreditRegistrationResult<ResendAttempt> {
42    let registry = InteractiveSuotar::new(ctx.suotar_client, ctx.caller.to_string());
43    account_linking::resend_for_target(
44        ctx.pool,
45        &registry,
46        course_id,
47        student_number,
48        rate_cap_override,
49    )
50    .await
51}
52
53/// Looks one student number up in the study registry without changing anything: no ledger row, no
54/// claimed mail slot, just the call log row every study registry call writes.
55///
56/// `Ok(None)` means the registry answered `personNotFound`; `Err` means it gave no usable answer.
57pub async fn look_up_person(
58    ctx: &ManualActionContext<'_>,
59    student_number: &SecretString,
60) -> Result<Option<RegistryPerson>, PersonLookupError> {
61    InteractiveSuotar::new(ctx.suotar_client, ctx.caller.to_string())
62        .look_up_person(&StudentNumber::new(student_number.clone()))
63        .await
64}