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}
17
18impl EmailTemplateType {
19 pub fn uses_placeholder_bag(self) -> bool {
22 matches!(
23 self,
24 Self::CreditRegistrationAccountLinking
25 | Self::CreditRegistrationActionNeeded
26 | Self::CreditRegistrationRegistered
27 )
28 }
29}
30
31#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
32
33pub struct EmailTemplate {
34 pub id: Uuid,
35 pub created_at: DateTime<Utc>,
36 pub updated_at: DateTime<Utc>,
37 pub deleted_at: Option<DateTime<Utc>>,
38 pub content: Option<serde_json::Value>,
39 #[serde(rename = "template_type")]
40 #[schema(rename = "template_type")]
41 pub email_template_type: EmailTemplateType,
42 pub subject: Option<String>,
43 pub exercise_completions_threshold: Option<i32>,
44 pub points_threshold: Option<i32>,
45 pub course_id: Option<Uuid>,
46 pub language: Option<String>,
47}
48
49#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
50
51pub struct EmailTemplateNew {
52 pub template_type: EmailTemplateType,
53 pub language: Option<String>,
54 pub content: Option<serde_json::Value>,
55 pub subject: Option<String>,
56}
57
58#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
59
60pub struct EmailTemplateUpdate {
61 pub template_type: EmailTemplateType,
62 pub subject: String,
63 pub content: serde_json::Value,
64 pub exercise_completions_threshold: Option<i32>,
65 pub points_threshold: Option<i32>,
66}
67
68pub async fn get_email_templates(
69 conn: &mut PgConnection,
70 course_id: Uuid,
71) -> ModelResult<Vec<EmailTemplate>> {
72 let res = sqlx::query_as!(
73 EmailTemplate,
74 r#"
75SELECT *
76FROM email_templates
77WHERE course_id = $1
78 AND deleted_at IS NULL
79 "#,
80 course_id
81 )
82 .fetch_all(conn)
83 .await?;
84 Ok(res)
85}
86
87pub async fn get_all_email_templates(conn: &mut PgConnection) -> ModelResult<Vec<EmailTemplate>> {
88 let res = sqlx::query_as!(
89 EmailTemplate,
90 r#"
91SELECT *
92FROM email_templates
93WHERE deleted_at IS NULL
94 "#,
95 )
96 .fetch_all(conn)
97 .await?;
98 Ok(res)
99}
100
101pub async fn get_generic_email_template_by_type_and_language(
102 conn: &mut PgConnection,
103 template_type: EmailTemplateType,
104 language: &str,
105) -> ModelResult<EmailTemplate> {
106 let res = sqlx::query_as!(
107 EmailTemplate,
108 r#"
109SELECT *
110FROM email_templates
111WHERE email_template_type = $1
112 AND course_id IS NULL
113 AND deleted_at IS NULL
114 AND (
115 language = $2
116 OR language = 'en'
117 OR language IS NULL
118 )
119ORDER BY CASE
120 WHEN language = $2 THEN 0
121 WHEN language = 'en' THEN 1
122 WHEN language IS NULL THEN 2
123 ELSE 3
124 END
125LIMIT 1
126 "#,
127 template_type as EmailTemplateType,
128 language
129 )
130 .fetch_one(conn)
131 .await?;
132 Ok(res)
133}
134
135pub async fn generic_email_template_exists(
139 conn: &mut PgConnection,
140 template_type: EmailTemplateType,
141) -> ModelResult<bool> {
142 let res = sqlx::query_scalar!(
143 r#"
144SELECT EXISTS(
145 SELECT 1
146 FROM email_templates
147 WHERE email_template_type = $1
148 AND course_id IS NULL
149 AND deleted_at IS NULL
150 AND (language = 'en' OR language IS NULL)
151) AS "exists!"
152 "#,
153 template_type as EmailTemplateType,
154 )
155 .fetch_one(conn)
156 .await?;
157 Ok(res)
158}
159
160pub async fn insert_email_template(
161 conn: &mut PgConnection,
162 course_id: Option<Uuid>,
163 email_template: EmailTemplateNew,
164 subject: Option<&'_ str>,
165) -> ModelResult<EmailTemplate> {
166 let subject_to_use = email_template.subject.as_deref().or(subject);
167 let res = sqlx::query_as!(
168 EmailTemplate,
169 r#"
170INSERT INTO email_templates (
171 email_template_type,
172 course_id,
173 subject,
174 language,
175 content
176 )
177VALUES ($1, $2, $3, $4, $5) ON CONFLICT (email_template_type, language, deleted_at)
178WHERE course_id IS NULL
179 AND deleted_at IS NULL DO
180UPDATE
181SET subject = COALESCE(EXCLUDED.subject, email_templates.subject),
182 content = COALESCE(EXCLUDED.content, email_templates.content),
183 updated_at = NOW()
184RETURNING *
185 "#,
186 email_template.template_type as EmailTemplateType,
187 course_id,
188 subject_to_use,
189 email_template.language,
190 email_template.content,
191 )
192 .fetch_one(conn)
193 .await?;
194 Ok(res)
195}
196
197pub async fn insert_global_email_template_if_absent(
200 conn: &mut PgConnection,
201 email_template: EmailTemplateNew,
202) -> ModelResult<Option<EmailTemplate>> {
203 let res = sqlx::query_as!(
204 EmailTemplate,
205 r#"
206INSERT INTO email_templates (
207 email_template_type,
208 subject,
209 language,
210 content
211 )
212VALUES ($1, $2, $3, $4) ON CONFLICT (email_template_type, language, deleted_at)
213WHERE course_id IS NULL
214 AND deleted_at IS NULL DO NOTHING
215RETURNING *
216 "#,
217 email_template.template_type as EmailTemplateType,
218 email_template.subject,
219 email_template.language,
220 email_template.content,
221 )
222 .fetch_optional(conn)
223 .await?;
224 Ok(res)
225}
226
227pub async fn get_email_template(
228 conn: &mut PgConnection,
229 email_template_id: Uuid,
230) -> ModelResult<EmailTemplate> {
231 let res = sqlx::query_as!(
232 EmailTemplate,
233 r#"
234SELECT *
235FROM email_templates
236WHERE id = $1
237 AND deleted_at IS NULL
238 "#,
239 email_template_id
240 )
241 .fetch_one(conn)
242 .await?;
243 Ok(res)
244}
245
246pub async fn update_email_template(
247 conn: &mut PgConnection,
248 email_template_id: Uuid,
249 email_template_update: EmailTemplateUpdate,
250) -> ModelResult<EmailTemplate> {
251 let res = sqlx::query_as!(
252 EmailTemplate,
253 r#"
254UPDATE email_templates
255SET email_template_type = $1,
256 subject = $2,
257 content = $3,
258 exercise_completions_threshold = $4,
259 points_threshold = $5
260WHERE id = $6
261 AND deleted_at IS NULL
262RETURNING *
263 "#,
264 email_template_update.template_type as EmailTemplateType,
265 email_template_update.subject,
266 email_template_update.content,
267 email_template_update.exercise_completions_threshold,
268 email_template_update.points_threshold,
269 email_template_id
270 )
271 .fetch_one(conn)
272 .await?;
273 Ok(res)
274}
275
276pub async fn delete_email_template(
277 conn: &mut PgConnection,
278 email_template_id: Uuid,
279) -> ModelResult<EmailTemplate> {
280 let deleted = sqlx::query_as!(
281 EmailTemplate,
282 r#"
283UPDATE email_templates
284SET deleted_at = NOW()
285WHERE id = $1
286 AND deleted_at IS NULL
287RETURNING *
288 "#,
289 email_template_id
290 )
291 .fetch_one(conn)
292 .await?;
293 Ok(deleted)
294}