headless_lms_credit_registration/use_cases/
student_notifications.rs1use headless_lms_models::email_deliveries::insert_email_delivery_with_placeholders;
9use headless_lms_models::library::credit_registration::student_notifications::{
10 STUDENT_NOTIFICATION_LIMIT, StudentNotificationToQueue, claim_unnotified, set_email_delivery_id,
11};
12use serde_json::json;
13use sqlx::{Connection, PgPool};
14use uuid::Uuid;
15
16use crate::error::CreditRegistrationResult;
17use crate::use_cases::mail_queue::{MailQueueSummary, TemplateCache, template_language};
18use crate::workflow::Counts;
19use headless_lms_models::credit_registrations::RegistrationScope;
20
21pub(crate) async fn run(
23 pool: &PgPool,
24 scope: &RegistrationScope,
25 base_url: &str,
26 is_account_linking_enabled: bool,
27) -> CreditRegistrationResult<Counts> {
28 let mut conn = pool.acquire().await?;
29 let mut tx = conn.begin().await?;
30 let claimed = claim_unnotified(
31 &mut tx,
32 scope,
33 STUDENT_NOTIFICATION_LIMIT,
34 is_account_linking_enabled,
35 )
36 .await?;
37 let mut templates = TemplateCache::default();
38 let mut summary = MailQueueSummary::new(claimed.len());
39 for notification in &claimed {
40 let template_type = notification.kind.email_template_type();
41 let language = template_language(¬ification.course_language_code);
42 let Some(template_id) = templates.id_for(&mut tx, template_type, &language).await? else {
43 summary.skip_missing_template(
44 template_type,
45 &language,
46 format!("{template_type:?} in {language}"),
47 );
48 continue;
49 };
50 let delivery = insert_email_delivery_with_placeholders(
51 &mut tx,
52 notification.user_id,
53 template_id,
54 &placeholders(base_url, notification, &language),
55 )
56 .await?;
57 set_email_delivery_id(
58 &mut tx,
59 notification.credit_registration_id,
60 notification.kind,
61 delivery,
62 )
63 .await?;
64 }
65 tx.commit().await?;
66 Ok(summary.finish("No student notification email template for:"))
67}
68
69fn placeholders(
71 base_url: &str,
72 notification: &StudentNotificationToQueue,
73 language: &str,
74) -> serde_json::Value {
75 json!({
76 "NAME": notification.first_name.as_deref().unwrap_or_default(),
77 "COURSE_NAME": notification.course_name,
78 "MODULE_NAME": notification.course_module_name.as_deref().unwrap_or_default(),
79 "CREDITS": notification
80 .credits
81 .map(|credits| format_credits(credits, language))
82 .unwrap_or_default(),
83 "ENROLMENT_LINK": notification.enrolment_link.as_deref().unwrap_or_default(),
84 "STATUS_LINK": status_page_url(base_url, notification.course_module_id),
85 })
86}
87
88fn format_credits(credits: f32, language: &str) -> String {
91 let formatted = format!("{credits:.2}");
92 let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
93 match language {
94 "fi" | "sv" => formatted.replace('.', ","),
95 _ => formatted.to_string(),
96 }
97}
98
99fn status_page_url(base_url: &str, course_module_id: Uuid) -> String {
101 format!(
102 "{}/completion-registration/{course_module_id}",
103 base_url.trim_end_matches('/')
104 )
105}