Skip to main content

headless_lms_models/
email_templates.rs

1use crate::prelude::*;
2use utoipa::ToSchema;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, 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
137pub async fn insert_email_template(
138    conn: &mut PgConnection,
139    course_id: Option<Uuid>,
140    email_template: EmailTemplateNew,
141    subject: Option<&'_ str>,
142) -> ModelResult<EmailTemplate> {
143    let subject_to_use = email_template.subject.as_deref().or(subject);
144    let res = sqlx::query_as!(
145        EmailTemplate,
146        r#"
147INSERT INTO email_templates (
148    email_template_type,
149    course_id,
150    subject,
151    language,
152    content
153  )
154VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email_template_type, language, deleted_at)
155WHERE course_id IS NULL
156  AND deleted_at IS NULL DO
157UPDATE
158SET subject = COALESCE(EXCLUDED.subject, email_templates.subject),
159  content = COALESCE(EXCLUDED.content, email_templates.content),
160  updated_at = NOW()
161RETURNING *
162        "#,
163        email_template.template_type as EmailTemplateType,
164        course_id,
165        subject_to_use,
166        email_template.language,
167        email_template.content,
168    )
169    .fetch_one(conn)
170    .await?;
171    Ok(res)
172}
173
174pub async fn get_email_template(
175    conn: &mut PgConnection,
176    email_template_id: Uuid,
177) -> ModelResult<EmailTemplate> {
178    let res = sqlx::query_as!(
179        EmailTemplate,
180        r#"
181SELECT *
182FROM email_templates
183WHERE id = $1
184  AND deleted_at IS NULL
185        "#,
186        email_template_id
187    )
188    .fetch_one(conn)
189    .await?;
190    Ok(res)
191}
192
193pub async fn update_email_template(
194    conn: &mut PgConnection,
195    email_template_id: Uuid,
196    email_template_update: EmailTemplateUpdate,
197) -> ModelResult<EmailTemplate> {
198    let res = sqlx::query_as!(
199        EmailTemplate,
200        r#"
201UPDATE email_templates
202SET email_template_type = $1,
203  subject = $2,
204  content = $3,
205  exercise_completions_threshold = $4,
206  points_threshold = $5
207WHERE id = $6
208  AND deleted_at IS NULL
209RETURNING *
210  "#,
211        email_template_update.template_type as EmailTemplateType,
212        email_template_update.subject,
213        email_template_update.content,
214        email_template_update.exercise_completions_threshold,
215        email_template_update.points_threshold,
216        email_template_id
217    )
218    .fetch_one(conn)
219    .await?;
220    Ok(res)
221}
222
223pub async fn delete_email_template(
224    conn: &mut PgConnection,
225    email_template_id: Uuid,
226) -> ModelResult<EmailTemplate> {
227    let deleted = sqlx::query_as!(
228        EmailTemplate,
229        r#"
230UPDATE email_templates
231SET deleted_at = NOW()
232WHERE id = $1
233  AND deleted_at IS NULL
234RETURNING *
235  "#,
236        email_template_id
237    )
238    .fetch_one(conn)
239    .await?;
240    Ok(deleted)
241}