1use std::collections::HashMap;
2
3use crate::library::credit_registration::account_linking::link_student_number_url;
4use crate::prelude::*;
5use utoipa::ToSchema;
6
7#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone, Copy, sqlx::Type, ToSchema)]
8#[sqlx(type_name = "email_template_type", rename_all = "snake_case")]
9#[serde(rename_all = "snake_case")]
10pub enum EmailTemplateType {
11 ResetPasswordEmail,
12 DeleteUserEmail,
13 ConfirmEmailCode,
14 Generic,
15 CreditRegistrationAccountLinking,
16 VerifyEmailAddress,
17 CreditRegistrationActionNeeded,
18 CreditRegistrationRegistered,
19}
20
21impl EmailTemplateType {
22 pub fn uses_placeholder_bag(self) -> bool {
25 matches!(
26 self,
27 Self::CreditRegistrationAccountLinking
28 | Self::CreditRegistrationActionNeeded
29 | Self::CreditRegistrationRegistered
30 )
31 }
32
33 pub fn sample_placeholders(self, base_url: &str) -> HashMap<String, String> {
36 let base_url = base_url.trim_end_matches('/');
37 let sample_id = Uuid::nil();
38 let values: Vec<(&str, String)> = match self {
39 Self::Generic => vec![],
40 Self::ResetPasswordEmail => vec![(
41 "RESET_LINK",
42 format!("{base_url}/reset-user-password/{sample_id}"),
43 )],
44 Self::DeleteUserEmail | Self::ConfirmEmailCode | Self::VerifyEmailAddress => {
45 vec![("CODE", "123456".to_string())]
46 }
47 Self::CreditRegistrationAccountLinking => vec![
48 (
49 "LINK",
50 link_student_number_url(base_url, &sample_id.to_string()),
51 ),
52 ("NAME", "Alex".to_string()),
53 ("STUDENT_NUMBER", "012345678".to_string()),
54 ("COURSE_NAME", "Introduction to Programming".to_string()),
55 ],
56 Self::CreditRegistrationActionNeeded | Self::CreditRegistrationRegistered => vec![
57 ("NAME", "Alex".to_string()),
58 ("COURSE_NAME", "Introduction to Programming".to_string()),
59 ("MODULE_NAME", "Part 2".to_string()),
60 ("CREDITS", "5".to_string()),
61 ("ENROLMENT_LINK", "https://example.com/enrol".to_string()),
62 (
63 "STATUS_LINK",
64 format!("{base_url}/completion-registration/{sample_id}"),
65 ),
66 ],
67 };
68 values
69 .into_iter()
70 .map(|(key, value)| (key.to_string(), value))
71 .collect()
72 }
73}
74
75#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
76
77pub struct EmailTemplate {
78 pub id: Uuid,
79 pub created_at: DateTime<Utc>,
80 pub updated_at: DateTime<Utc>,
81 pub deleted_at: Option<DateTime<Utc>>,
82 pub content: Option<serde_json::Value>,
83 #[serde(rename = "template_type")]
84 #[schema(rename = "template_type")]
85 pub email_template_type: EmailTemplateType,
86 pub subject: Option<String>,
87 pub exercise_completions_threshold: Option<i32>,
88 pub points_threshold: Option<i32>,
89 pub course_id: Option<Uuid>,
90 pub language: Option<String>,
91}
92
93#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
94
95pub struct EmailTemplateNew {
96 pub template_type: EmailTemplateType,
97 pub language: Option<String>,
98 pub content: Option<serde_json::Value>,
99 pub subject: Option<String>,
100}
101
102#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
103
104pub struct EmailTemplateUpdate {
105 pub template_type: EmailTemplateType,
106 pub subject: String,
107 pub content: serde_json::Value,
108 pub exercise_completions_threshold: Option<i32>,
109 pub points_threshold: Option<i32>,
110}
111
112pub async fn get_email_templates(
113 conn: &mut PgConnection,
114 course_id: Uuid,
115) -> ModelResult<Vec<EmailTemplate>> {
116 let res = sqlx::query_as!(
117 EmailTemplate,
118 r#"
119SELECT *
120FROM email_templates
121WHERE course_id = $1
122 AND deleted_at IS NULL
123 "#,
124 course_id
125 )
126 .fetch_all(conn)
127 .await?;
128 Ok(res)
129}
130
131pub async fn get_all_email_templates(conn: &mut PgConnection) -> ModelResult<Vec<EmailTemplate>> {
132 let res = sqlx::query_as!(
133 EmailTemplate,
134 r#"
135SELECT *
136FROM email_templates
137WHERE deleted_at IS NULL
138 "#,
139 )
140 .fetch_all(conn)
141 .await?;
142 Ok(res)
143}
144
145pub async fn get_generic_email_template_by_type_and_language(
146 conn: &mut PgConnection,
147 template_type: EmailTemplateType,
148 language: &str,
149) -> ModelResult<EmailTemplate> {
150 let res = sqlx::query_as!(
151 EmailTemplate,
152 r#"
153SELECT *
154FROM email_templates
155WHERE email_template_type = $1
156 AND course_id IS NULL
157 AND deleted_at IS NULL
158 AND (
159 language = $2
160 OR language = 'en'
161 OR language IS NULL
162 )
163ORDER BY CASE
164 WHEN language = $2 THEN 0
165 WHEN language = 'en' THEN 1
166 WHEN language IS NULL THEN 2
167 ELSE 3
168 END
169LIMIT 1
170 "#,
171 template_type as EmailTemplateType,
172 language
173 )
174 .fetch_one(conn)
175 .await?;
176 Ok(res)
177}
178
179pub async fn generic_email_template_exists(
183 conn: &mut PgConnection,
184 template_type: EmailTemplateType,
185) -> ModelResult<bool> {
186 let res = sqlx::query_scalar!(
187 r#"
188SELECT EXISTS(
189 SELECT 1
190 FROM email_templates
191 WHERE email_template_type = $1
192 AND course_id IS NULL
193 AND deleted_at IS NULL
194 AND (language = 'en' OR language IS NULL)
195) AS "exists!"
196 "#,
197 template_type as EmailTemplateType,
198 )
199 .fetch_one(conn)
200 .await?;
201 Ok(res)
202}
203
204pub async fn insert_email_template(
205 conn: &mut PgConnection,
206 course_id: Option<Uuid>,
207 email_template: EmailTemplateNew,
208 subject: Option<&'_ str>,
209) -> ModelResult<EmailTemplate> {
210 let subject_to_use = email_template.subject.as_deref().or(subject);
211 let res = sqlx::query_as!(
212 EmailTemplate,
213 r#"
214INSERT INTO email_templates (
215 email_template_type,
216 course_id,
217 subject,
218 language,
219 content
220 )
221VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email_template_type, language, deleted_at)
222WHERE course_id IS NULL
223 AND deleted_at IS NULL DO
224UPDATE
225SET subject = COALESCE(EXCLUDED.subject, email_templates.subject),
226 content = COALESCE(EXCLUDED.content, email_templates.content),
227 updated_at = NOW()
228RETURNING *
229 "#,
230 email_template.template_type as EmailTemplateType,
231 course_id,
232 subject_to_use,
233 email_template.language,
234 email_template.content,
235 )
236 .fetch_one(conn)
237 .await?;
238 Ok(res)
239}
240
241pub async fn insert_global_email_template_if_absent(
244 conn: &mut PgConnection,
245 email_template: EmailTemplateNew,
246) -> ModelResult<Option<EmailTemplate>> {
247 let res = sqlx::query_as!(
248 EmailTemplate,
249 r#"
250INSERT INTO email_templates (
251 email_template_type,
252 subject,
253 language,
254 content
255 )
256VALUES ($1, $2, $3, $4) ON CONFLICT (email_template_type, language, deleted_at)
257WHERE course_id IS NULL
258 AND deleted_at IS NULL DO NOTHING
259RETURNING *
260 "#,
261 email_template.template_type as EmailTemplateType,
262 email_template.subject,
263 email_template.language,
264 email_template.content,
265 )
266 .fetch_optional(conn)
267 .await?;
268 Ok(res)
269}
270
271pub async fn get_email_template(
272 conn: &mut PgConnection,
273 email_template_id: Uuid,
274) -> ModelResult<EmailTemplate> {
275 let res = sqlx::query_as!(
276 EmailTemplate,
277 r#"
278SELECT *
279FROM email_templates
280WHERE id = $1
281 AND deleted_at IS NULL
282 "#,
283 email_template_id
284 )
285 .fetch_one(conn)
286 .await?;
287 Ok(res)
288}
289
290pub async fn get_language(
293 conn: &mut PgConnection,
294 email_template_id: Uuid,
295) -> ModelResult<Option<String>> {
296 let res = sqlx::query_scalar!(
297 r#"
298SELECT email_template_language(et) AS language
299FROM email_templates et
300WHERE et.id = $1
301 AND et.deleted_at IS NULL
302 "#,
303 email_template_id
304 )
305 .fetch_one(conn)
306 .await?;
307 Ok(res)
308}
309
310pub async fn update_email_template(
311 conn: &mut PgConnection,
312 email_template_id: Uuid,
313 email_template_update: EmailTemplateUpdate,
314) -> ModelResult<EmailTemplate> {
315 let res = sqlx::query_as!(
316 EmailTemplate,
317 r#"
318UPDATE email_templates
319SET email_template_type = $1,
320 subject = $2,
321 content = $3,
322 exercise_completions_threshold = $4,
323 points_threshold = $5
324WHERE id = $6
325 AND deleted_at IS NULL
326RETURNING *
327 "#,
328 email_template_update.template_type as EmailTemplateType,
329 email_template_update.subject,
330 email_template_update.content,
331 email_template_update.exercise_completions_threshold,
332 email_template_update.points_threshold,
333 email_template_id
334 )
335 .fetch_one(conn)
336 .await?;
337 Ok(res)
338}
339
340pub async fn delete_email_template(
341 conn: &mut PgConnection,
342 email_template_id: Uuid,
343) -> ModelResult<EmailTemplate> {
344 let deleted = sqlx::query_as!(
345 EmailTemplate,
346 r#"
347UPDATE email_templates
348SET deleted_at = NOW()
349WHERE id = $1
350 AND deleted_at IS NULL
351RETURNING *
352 "#,
353 email_template_id
354 )
355 .fetch_one(&mut *conn)
356 .await?;
357 sqlx::query!(
359 "
360UPDATE email_deliveries
361SET deleted_at = NOW()
362WHERE email_template_id = $1
363 AND sent = FALSE
364 AND deleted_at IS NULL
365 ",
366 email_template_id
367 )
368 .execute(conn)
369 .await?;
370 Ok(deleted)
371}