Skip to main content

headless_lms_models/
email_templates.rs

1use crate::prelude::*;
2use utoipa::ToSchema;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, sqlx::Type, ToSchema)]
5#[sqlx(type_name = "email_template_type", rename_all = "snake_case")]
6#[serde(rename_all = "snake_case")]
7pub enum EmailTemplateType {
8    ResetPasswordEmail,
9    DeleteUserEmail,
10    ConfirmEmailCode,
11    Generic,
12    CreditRegistrationAccountLinking,
13    VerifyEmailAddress,
14    CreditRegistrationActionNeeded,
15    CreditRegistrationRegistered,
16    CreditRegistrationStudentNumberLinked,
17}
18
19impl EmailTemplateType {
20    /// Whether substitutions come from `email_deliveries.placeholders` instead of the recipient's
21    /// account, which may not exist.
22    pub fn uses_placeholder_bag(self) -> bool {
23        matches!(
24            self,
25            Self::CreditRegistrationAccountLinking
26                | Self::CreditRegistrationActionNeeded
27                | Self::CreditRegistrationRegistered
28                | Self::CreditRegistrationStudentNumberLinked
29        )
30    }
31}
32
33#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
34
35pub struct EmailTemplate {
36    pub id: Uuid,
37    pub created_at: DateTime<Utc>,
38    pub updated_at: DateTime<Utc>,
39    pub deleted_at: Option<DateTime<Utc>>,
40    pub content: Option<serde_json::Value>,
41    #[serde(rename = "template_type")]
42    #[schema(rename = "template_type")]
43    pub email_template_type: EmailTemplateType,
44    pub subject: Option<String>,
45    pub exercise_completions_threshold: Option<i32>,
46    pub points_threshold: Option<i32>,
47    pub course_id: Option<Uuid>,
48    pub language: Option<String>,
49}
50
51#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
52
53pub struct EmailTemplateNew {
54    pub template_type: EmailTemplateType,
55    pub language: Option<String>,
56    pub content: Option<serde_json::Value>,
57    pub subject: Option<String>,
58}
59
60#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
61
62pub struct EmailTemplateUpdate {
63    pub template_type: EmailTemplateType,
64    pub subject: String,
65    pub content: serde_json::Value,
66    pub exercise_completions_threshold: Option<i32>,
67    pub points_threshold: Option<i32>,
68}
69
70pub async fn get_email_templates(
71    conn: &mut PgConnection,
72    course_id: Uuid,
73) -> ModelResult<Vec<EmailTemplate>> {
74    let res = sqlx::query_as!(
75        EmailTemplate,
76        r#"
77SELECT *
78FROM email_templates
79WHERE course_id = $1
80  AND deleted_at IS NULL
81        "#,
82        course_id
83    )
84    .fetch_all(conn)
85    .await?;
86    Ok(res)
87}
88
89pub async fn get_all_email_templates(conn: &mut PgConnection) -> ModelResult<Vec<EmailTemplate>> {
90    let res = sqlx::query_as!(
91        EmailTemplate,
92        r#"
93SELECT *
94FROM email_templates
95WHERE deleted_at IS NULL
96        "#,
97    )
98    .fetch_all(conn)
99    .await?;
100    Ok(res)
101}
102
103pub async fn get_generic_email_template_by_type_and_language(
104    conn: &mut PgConnection,
105    template_type: EmailTemplateType,
106    language: &str,
107) -> ModelResult<EmailTemplate> {
108    let res = sqlx::query_as!(
109        EmailTemplate,
110        r#"
111SELECT *
112FROM email_templates
113WHERE email_template_type = $1
114  AND course_id IS NULL
115  AND deleted_at IS NULL
116  AND (
117    language = $2
118    OR language = 'en'
119    OR language IS NULL
120  )
121ORDER BY CASE
122    WHEN language = $2 THEN 0
123    WHEN language = 'en' THEN 1
124    WHEN language IS NULL THEN 2
125    ELSE 3
126  END
127LIMIT 1
128        "#,
129        template_type as EmailTemplateType,
130        language
131    )
132    .fetch_one(conn)
133    .await?;
134    Ok(res)
135}
136
137/// Whether a generic template of this type would actually be found: checks only the `"en"` and
138/// `NULL` language rows, the two the lookup above always falls back to regardless of the caller's
139/// UI language.
140pub async fn generic_email_template_exists(
141    conn: &mut PgConnection,
142    template_type: EmailTemplateType,
143) -> ModelResult<bool> {
144    let res = sqlx::query_scalar!(
145        r#"
146SELECT EXISTS(
147  SELECT 1
148  FROM email_templates
149  WHERE email_template_type = $1
150    AND course_id IS NULL
151    AND deleted_at IS NULL
152    AND (language = 'en' OR language IS NULL)
153) AS "exists!"
154        "#,
155        template_type as EmailTemplateType,
156    )
157    .fetch_one(conn)
158    .await?;
159    Ok(res)
160}
161
162pub async fn insert_email_template(
163    conn: &mut PgConnection,
164    course_id: Option<Uuid>,
165    email_template: EmailTemplateNew,
166    subject: Option<&'_ str>,
167) -> ModelResult<EmailTemplate> {
168    let subject_to_use = email_template.subject.as_deref().or(subject);
169    let res = sqlx::query_as!(
170        EmailTemplate,
171        r#"
172INSERT INTO email_templates (
173    email_template_type,
174    course_id,
175    subject,
176    language,
177    content
178  )
179VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email_template_type, language, deleted_at)
180WHERE course_id IS NULL
181  AND deleted_at IS NULL DO
182UPDATE
183SET subject = COALESCE(EXCLUDED.subject, email_templates.subject),
184  content = COALESCE(EXCLUDED.content, email_templates.content),
185  updated_at = NOW()
186RETURNING *
187        "#,
188        email_template.template_type as EmailTemplateType,
189        course_id,
190        subject_to_use,
191        email_template.language,
192        email_template.content,
193    )
194    .fetch_one(conn)
195    .await?;
196    Ok(res)
197}
198
199pub async fn get_email_template(
200    conn: &mut PgConnection,
201    email_template_id: Uuid,
202) -> ModelResult<EmailTemplate> {
203    let res = sqlx::query_as!(
204        EmailTemplate,
205        r#"
206SELECT *
207FROM email_templates
208WHERE id = $1
209  AND deleted_at IS NULL
210        "#,
211        email_template_id
212    )
213    .fetch_one(conn)
214    .await?;
215    Ok(res)
216}
217
218pub async fn update_email_template(
219    conn: &mut PgConnection,
220    email_template_id: Uuid,
221    email_template_update: EmailTemplateUpdate,
222) -> ModelResult<EmailTemplate> {
223    let res = sqlx::query_as!(
224        EmailTemplate,
225        r#"
226UPDATE email_templates
227SET email_template_type = $1,
228  subject = $2,
229  content = $3,
230  exercise_completions_threshold = $4,
231  points_threshold = $5
232WHERE id = $6
233  AND deleted_at IS NULL
234RETURNING *
235  "#,
236        email_template_update.template_type as EmailTemplateType,
237        email_template_update.subject,
238        email_template_update.content,
239        email_template_update.exercise_completions_threshold,
240        email_template_update.points_threshold,
241        email_template_id
242    )
243    .fetch_one(conn)
244    .await?;
245    Ok(res)
246}
247
248pub async fn delete_email_template(
249    conn: &mut PgConnection,
250    email_template_id: Uuid,
251) -> ModelResult<EmailTemplate> {
252    let deleted = sqlx::query_as!(
253        EmailTemplate,
254        r#"
255UPDATE email_templates
256SET deleted_at = NOW()
257WHERE id = $1
258  AND deleted_at IS NULL
259RETURNING *
260  "#,
261        email_template_id
262    )
263    .fetch_one(conn)
264    .await?;
265    Ok(deleted)
266}