Skip to main content

headless_lms_server/domain/credit_registration_phases/
link_emails.rs

1//! The `link-emails` phase: turning a claimed mail slot into a queued message.
2//!
3//! It talks to no study registry, which is the point of it being its own phase: a wedged mail queue
4//! and an unreachable Sisu are different problems. The caps and the dedup guard applied when the
5//! slot was claimed, so this phase retries until the message is queued rather than deciding again.
6
7use headless_lms_models::credit_registration_account_linking_emails::{
8    LinkingMailToQueue, claim_unqueued, set_email_delivery_id,
9};
10use headless_lms_models::credit_registration_phase_state::PhaseRunOutcome;
11use headless_lms_models::email_deliveries::insert_email_delivery_to_address;
12use headless_lms_models::email_templates::EmailTemplateType;
13use headless_lms_models::library::credit_registration::account_linking::link_student_number_url;
14use secrecy::ExposeSecret;
15use serde_json::json;
16use sqlx::PgConnection;
17use uuid::Uuid;
18
19use super::{MailQueuePhase, PhaseContext, PhaseScope, run_mail_queue_phase, template_language};
20
21/// How many mails one iteration queues; the sender has its own rate, so this only bounds how much
22/// one transaction holds open.
23const QUEUE_LIMIT: i64 = 200;
24
25pub async fn run(ctx: &PhaseContext<'_>, scope: &PhaseScope) -> anyhow::Result<PhaseRunOutcome> {
26    run_mail_queue_phase::<LinkEmailsPhase>(ctx, scope).await
27}
28
29struct LinkEmailsPhase;
30
31impl MailQueuePhase for LinkEmailsPhase {
32    type Item = LinkingMailToQueue;
33    type Cache = ();
34
35    async fn claim(conn: &mut PgConnection, scope: &PhaseScope) -> anyhow::Result<Vec<Self::Item>> {
36        Ok(claim_unqueued(conn, QUEUE_LIMIT, scope.course_id).await?)
37    }
38
39    fn template_type(_item: &Self::Item) -> EmailTemplateType {
40        EmailTemplateType::CreditRegistrationAccountLinking
41    }
42
43    fn language(item: &Self::Item) -> String {
44        template_language(&item.course_language_code)
45    }
46
47    async fn queue(
48        ctx: &PhaseContext<'_>,
49        conn: &mut PgConnection,
50        item: &Self::Item,
51        template_id: Uuid,
52        _cache: &mut Self::Cache,
53    ) -> anyhow::Result<()> {
54        let delivery = insert_email_delivery_to_address(
55            conn,
56            &item.emailed_to,
57            template_id,
58            &placeholders(ctx.base_url, item),
59        )
60        .await?;
61        set_email_delivery_id(conn, item.id, delivery).await?;
62        Ok(())
63    }
64
65    fn missing_template_label(_template_type: EmailTemplateType, language: &str) -> String {
66        language.to_string()
67    }
68
69    fn missing_templates_error_prefix() -> &'static str {
70        "No credit_registration_account_linking email template for:"
71    }
72}
73
74/// Stored on the delivery row because the recipient may have no account here for the sender to read
75/// them from.
76fn placeholders(base_url: &str, mail: &LinkingMailToQueue) -> serde_json::Value {
77    json!({
78        "LINK": link_student_number_url(base_url, mail.token.expose_secret()),
79        "NAME": mail.first_names.clone().unwrap_or_default(),
80        "STUDENT_NUMBER": mail.student_number,
81        "COURSE_NAME": mail.course_name,
82    })
83}