Skip to main content

headless_lms_server/programs/seed/
seed_generic_emails.rs

1use headless_lms_models::{
2    email_templates::{EmailTemplateNew, EmailTemplateType, insert_email_template},
3    user_passwords::insert_password_reset_token,
4};
5use serde_json::json;
6use sqlx::{Pool, Postgres};
7use uuid::Uuid;
8
9use super::seed_users::SeedUsersResult;
10
11pub async fn seed_generic_emails(
12    db_pool: Pool<Postgres>,
13    seed_users_result: SeedUsersResult,
14) -> anyhow::Result<()> {
15    info!("inserting password reset emails");
16
17    let mut conn = db_pool.acquire().await?;
18
19    let english_subject = Some("Reset password request");
20    let english_body = json!([
21        {
22            "type": "core/paragraph",
23            "isValid": true,
24            "clientId": "95acea49-1c92-4d54-9854-707e1bfee010",
25            "attributes": {
26                "content": "Hello, it seems you requested a password reset.",
27                "drop_cap": false
28            },
29            "innerBlocks": []
30        },
31        {
32            "type": "core/paragraph",
33            "isValid": true,
34            "clientId": "ceac591d-b291-40b2-8da7-6f437a6a8fce",
35            "attributes": {
36                "content": "You can reset your password here: {{RESET_LINK}}",
37                "drop_cap": false
38            },
39            "innerBlocks": []
40        },
41        {
42            "type": "core/paragraph",
43            "isValid": true,
44            "clientId": "6a4165a1-38e1-4a17-b364-5f265afc7d23",
45            "attributes": {
46                "content": "If you did not request a password reset, please ignore this message.",
47                "drop_cap": false
48            },
49            "innerBlocks": []
50        }
51    ]);
52
53    let english_template = EmailTemplateNew {
54        template_type: EmailTemplateType::ResetPasswordEmail,
55        language: Some("en".to_string()),
56        content: Some(english_body),
57        subject: english_subject.map(|s| s.to_string()),
58    };
59
60    insert_email_template(&mut conn, None, english_template, english_subject).await?;
61
62    let finnish_subject = Some("Salasanan palautuspyyntö");
63    let finnish_body = json!([
64        {
65            "type": "core/paragraph",
66            "isValid": true,
67            "clientId": "a9de49ff-919f-44b4-a085-9210ce0da94b",
68            "attributes": {
69                "content": "Hei, olet pyytänyt salasanan palautusta.",
70                "drop_cap": false
71            },
72            "innerBlocks": []
73        },
74        {
75            "type": "core/paragraph",
76            "isValid": true,
77            "clientId": "c145bae4-9ff5-4194-8913-50f8900c20c8",
78            "attributes": {
79                "content": "Voit palauttaa salasanasi tästä: {{RESET_LINK}}",
80                "drop_cap": false
81            },
82            "innerBlocks": []
83        },
84        {
85            "type": "core/paragraph",
86            "isValid": true,
87            "clientId": "ebd0d430-0ae3-4b85-9e8e-96481660ded4",
88            "attributes": {
89                "content": "Jos et pyytänyt salasanan palautusta, voit jättää tämän viestin huomiotta.",
90                "drop_cap": false
91            },
92            "innerBlocks": []
93        }
94    ]);
95
96    let finnish_template = EmailTemplateNew {
97        template_type: EmailTemplateType::ResetPasswordEmail,
98        language: Some("fi".to_string()),
99        content: Some(finnish_body),
100        subject: finnish_subject.map(|s| s.to_string()),
101    };
102
103    insert_email_template(&mut conn, None, finnish_template, finnish_subject).await?;
104
105    info!("inserting password reset token for user");
106    let SeedUsersResult { sign_up_user, .. } = seed_users_result;
107    insert_password_reset_token(
108        &mut conn,
109        sign_up_user,
110        Uuid::parse_str("5a831370-6b7e-4ece-b962-6bc31c28fe53")?,
111    )
112    .await?;
113
114    info!("inserting delete account email");
115
116    let delete_subject = Some("Account deletion code");
117    let delete_body = json!([
118        {
119            "type": "core/paragraph",
120            "isValid": true,
121            "clientId": "11111111-1111-1111-1111-111111111111",
122            "attributes": {
123                "content": "Hello, it seems you requested a code for deleting your account",
124                "drop_cap": false
125            },
126            "innerBlocks": []
127        },
128        {
129            "type": "core/paragraph",
130            "isValid": true,
131            "clientId": "22222222-2222-2222-2222-222222222222",
132            "attributes": {
133                "content": "Use this verification code to delete your account: {{CODE}}",
134                "drop_cap": false
135            },
136            "innerBlocks": []
137        },
138        {
139            "type": "core/paragraph",
140            "isValid": true,
141            "clientId": "33333333-3333-3333-3333-333333333333",
142            "attributes": {
143                "content": "If you did not request a code, please ignore this message.",
144                "drop_cap": false
145            },
146            "innerBlocks": []
147        }
148    ]);
149
150    let delete_template = EmailTemplateNew {
151        template_type: EmailTemplateType::DeleteUserEmail,
152        language: Some("en".to_string()),
153        content: Some(delete_body),
154        subject: delete_subject.map(|s| s.to_string()),
155    };
156
157    insert_email_template(&mut conn, None, delete_template, delete_subject).await?;
158
159    info!("inserting confirm email code email");
160
161    let confirm_subject = Some("Email verification code");
162    let confirm_body = json!([
163        {
164            "type": "core/paragraph",
165            "isValid": true,
166            "clientId": "44444444-4444-4444-4444-444444444444",
167            "attributes": {
168                "content": "Hello, please use this code to verify your email address",
169                "drop_cap": false
170            },
171            "innerBlocks": []
172        },
173        {
174            "type": "core/paragraph",
175            "isValid": true,
176            "clientId": "55555555-5555-5555-5555-555555555555",
177            "attributes": {
178                "content": "Your verification code is: {{CODE}}",
179                "drop_cap": false
180            },
181            "innerBlocks": []
182        },
183        {
184            "type": "core/paragraph",
185            "isValid": true,
186            "clientId": "66666666-6666-6666-6666-666666666666",
187            "attributes": {
188                "content": "If you did not request this code, please ignore this message.",
189                "drop_cap": false
190            },
191            "innerBlocks": []
192        }
193    ]);
194
195    let confirm_template = EmailTemplateNew {
196        template_type: EmailTemplateType::ConfirmEmailCode,
197        language: Some("en".to_string()),
198        content: Some(confirm_body),
199        subject: confirm_subject.map(|s| s.to_string()),
200    };
201
202    insert_email_template(&mut conn, None, confirm_template, confirm_subject).await?;
203
204    seed_email_ownership_verification_templates(&mut conn).await?;
205    seed_account_linking_templates(&mut conn).await?;
206    seed_student_notification_templates(&mut conn).await?;
207    seed_student_number_linked_templates(&mut conn).await?;
208
209    Ok(())
210}
211
212/// The two terminal-state mails a student may get about a credit registration, and the only two
213/// that exist.
214///
215/// `{{ENROLMENT_LINK}}` is empty for a module with no open university product or no resolved access
216/// token, so the sentence carrying it has to read correctly on its own; that degraded case is what
217/// the configuration check reports as a problem.
218async fn seed_student_notification_templates(conn: &mut sqlx::PgConnection) -> anyhow::Result<()> {
219    info!("inserting credit registration student notification emails");
220
221    let english_subject = Some("We could not register your credits yet");
222    let english_body = json!([
223        {
224            "type": "core/paragraph",
225            "isValid": true,
226            "clientId": "d3000000-0000-0000-0000-000000000001",
227            "attributes": {
228                "content": "Hello {{NAME}}, we could not register your {{CREDITS}} credits for {{COURSE_NAME}} because the University of Helsinki study registry has no active enrolment for you on this course.",
229                "drop_cap": false
230            },
231            "innerBlocks": []
232        },
233        {
234            "type": "core/paragraph",
235            "isValid": true,
236            "clientId": "d3000000-0000-0000-0000-000000000002",
237            "attributes": {
238                "content": "Enrol on the course in Sisu and we will register the credits for you automatically. {{ENROLMENT_LINK}}",
239                "drop_cap": false
240            },
241            "innerBlocks": []
242        },
243        {
244            "type": "core/paragraph",
245            "isValid": true,
246            "clientId": "d3000000-0000-0000-0000-000000000003",
247            "attributes": {
248                "content": "You can follow the registration here: {{STATUS_LINK}}",
249                "drop_cap": false
250            },
251            "innerBlocks": []
252        }
253    ]);
254
255    insert_email_template(
256        conn,
257        None,
258        EmailTemplateNew {
259            template_type: EmailTemplateType::CreditRegistrationActionNeeded,
260            language: Some("en".to_string()),
261            content: Some(english_body),
262            subject: english_subject.map(|s| s.to_string()),
263        },
264        english_subject,
265    )
266    .await?;
267
268    let finnish_subject = Some("Emme voineet vielä kirjata opintopisteitäsi");
269    let finnish_body = json!([
270        {
271            "type": "core/paragraph",
272            "isValid": true,
273            "clientId": "d4000000-0000-0000-0000-000000000001",
274            "attributes": {
275                "content": "Hei {{NAME}}, emme voineet kirjata {{CREDITS}} opintopistettäsi kurssilta {{COURSE_NAME}}, koska Helsingin yliopiston opintorekisterissä ei ole sinulle voimassa olevaa ilmoittautumista tälle kurssille.",
276                "drop_cap": false
277            },
278            "innerBlocks": []
279        },
280        {
281            "type": "core/paragraph",
282            "isValid": true,
283            "clientId": "d4000000-0000-0000-0000-000000000002",
284            "attributes": {
285                "content": "Ilmoittaudu kurssille Sisussa, niin kirjaamme opintopisteet puolestasi automaattisesti. {{ENROLMENT_LINK}}",
286                "drop_cap": false
287            },
288            "innerBlocks": []
289        },
290        {
291            "type": "core/paragraph",
292            "isValid": true,
293            "clientId": "d4000000-0000-0000-0000-000000000003",
294            "attributes": {
295                "content": "Voit seurata kirjausta täältä: {{STATUS_LINK}}",
296                "drop_cap": false
297            },
298            "innerBlocks": []
299        }
300    ]);
301
302    insert_email_template(
303        conn,
304        None,
305        EmailTemplateNew {
306            template_type: EmailTemplateType::CreditRegistrationActionNeeded,
307            language: Some("fi".to_string()),
308            content: Some(finnish_body),
309            subject: finnish_subject.map(|s| s.to_string()),
310        },
311        finnish_subject,
312    )
313    .await?;
314
315    // One template for `registered`, `duplicate` and `not_improved`: from the student's side the
316    // credit exists either way, and a message that told them apart would only invite a support
317    // question about a distinction they cannot act on.
318    let english_subject = Some("Your credits are recorded in Sisu");
319    let english_body = json!([
320        {
321            "type": "core/paragraph",
322            "isValid": true,
323            "clientId": "d5000000-0000-0000-0000-000000000001",
324            "attributes": {
325                "content": "Hello {{NAME}}, your {{CREDITS}} credits for {{COURSE_NAME}} are now recorded in the University of Helsinki study registry.",
326                "drop_cap": false
327            },
328            "innerBlocks": []
329        },
330        {
331            "type": "core/paragraph",
332            "isValid": true,
333            "clientId": "d5000000-0000-0000-0000-000000000002",
334            "attributes": {
335                "content": "If the credits were already recorded there, this message simply confirms it. You can see the details here: {{STATUS_LINK}}",
336                "drop_cap": false
337            },
338            "innerBlocks": []
339        }
340    ]);
341
342    insert_email_template(
343        conn,
344        None,
345        EmailTemplateNew {
346            template_type: EmailTemplateType::CreditRegistrationRegistered,
347            language: Some("en".to_string()),
348            content: Some(english_body),
349            subject: english_subject.map(|s| s.to_string()),
350        },
351        english_subject,
352    )
353    .await?;
354
355    let finnish_subject = Some("Opintopisteesi on kirjattu Sisuun");
356    let finnish_body = json!([
357        {
358            "type": "core/paragraph",
359            "isValid": true,
360            "clientId": "d6000000-0000-0000-0000-000000000001",
361            "attributes": {
362                "content": "Hei {{NAME}}, {{CREDITS}} opintopistettäsi kurssilta {{COURSE_NAME}} on nyt kirjattu Helsingin yliopiston opintorekisteriin.",
363                "drop_cap": false
364            },
365            "innerBlocks": []
366        },
367        {
368            "type": "core/paragraph",
369            "isValid": true,
370            "clientId": "d6000000-0000-0000-0000-000000000002",
371            "attributes": {
372                "content": "Jos opintopisteet oli jo kirjattu sinne, tämä viesti vain vahvistaa sen. Näet tiedot täältä: {{STATUS_LINK}}",
373                "drop_cap": false
374            },
375            "innerBlocks": []
376        }
377    ]);
378
379    insert_email_template(
380        conn,
381        None,
382        EmailTemplateNew {
383            template_type: EmailTemplateType::CreditRegistrationRegistered,
384            language: Some("fi".to_string()),
385            content: Some(finnish_body),
386            subject: finnish_subject.map(|s| s.to_string()),
387        },
388        finnish_subject,
389    )
390    .await?;
391
392    Ok(())
393}
394
395/// Told to a student whose student number we linked from a matching verified email address, without
396/// them clicking anything. A compensating control for that automatic link: it is how someone finds
397/// out a number was attached to their account and can detach it.
398async fn seed_student_number_linked_templates(conn: &mut sqlx::PgConnection) -> anyhow::Result<()> {
399    info!("inserting credit registration student number linked emails");
400
401    let english_subject = Some("Your student number was linked to your account");
402    let english_body = json!([
403        {
404            "type": "core/paragraph",
405            "isValid": true,
406            "clientId": "d7000000-0000-0000-0000-000000000001",
407            "attributes": {
408                "content": "Hello {{NAME}}, we linked student number {{STUDENT_NUMBER}} to your courses.mooc.fi account, because the University of Helsinki study registry holds this same confirmed email address for that student number.",
409                "drop_cap": false
410            },
411            "innerBlocks": []
412        },
413        {
414            "type": "core/paragraph",
415            "isValid": true,
416            "clientId": "d7000000-0000-0000-0000-000000000002",
417            "attributes": {
418                "content": "Your credits will be registered under this student number. If it is not yours, remove it here: {{LINK}}",
419                "drop_cap": false
420            },
421            "innerBlocks": []
422        }
423    ]);
424
425    insert_email_template(
426        conn,
427        None,
428        EmailTemplateNew {
429            template_type: EmailTemplateType::CreditRegistrationStudentNumberLinked,
430            language: Some("en".to_string()),
431            content: Some(english_body),
432            subject: english_subject.map(|s| s.to_string()),
433        },
434        english_subject,
435    )
436    .await?;
437
438    let finnish_subject = Some("Opiskelijanumerosi liitettiin tiliisi");
439    let finnish_body = json!([
440        {
441            "type": "core/paragraph",
442            "isValid": true,
443            "clientId": "d8000000-0000-0000-0000-000000000001",
444            "attributes": {
445                "content": "Hei {{NAME}}, liitimme opiskelijanumeron {{STUDENT_NUMBER}} courses.mooc.fi-tiliisi, koska Helsingin yliopiston opintorekisterissä on samalle opiskelijanumerolle tämä sama vahvistettu sähköpostiosoite.",
446                "drop_cap": false
447            },
448            "innerBlocks": []
449        },
450        {
451            "type": "core/paragraph",
452            "isValid": true,
453            "clientId": "d8000000-0000-0000-0000-000000000002",
454            "attributes": {
455                "content": "Opintopisteesi kirjataan tälle opiskelijanumerolle. Jos se ei ole sinun, poista se täältä: {{LINK}}",
456                "drop_cap": false
457            },
458            "innerBlocks": []
459        }
460    ]);
461
462    insert_email_template(
463        conn,
464        None,
465        EmailTemplateNew {
466            template_type: EmailTemplateType::CreditRegistrationStudentNumberLinked,
467            language: Some("fi".to_string()),
468            content: Some(finnish_body),
469            subject: finnish_subject.map(|s| s.to_string()),
470        },
471        finnish_subject,
472    )
473    .await?;
474
475    Ok(())
476}
477
478/// The mail that carries a student-number linking link. Every placeholder comes from the delivery
479/// row because the recipient may have no account here, and the "you received this because" line has
480/// to stay because the message is unsolicited. A migration cannot insert a row using an enum value
481/// it adds itself, so in dev and tests these templates come only from here.
482async fn seed_account_linking_templates(conn: &mut sqlx::PgConnection) -> anyhow::Result<()> {
483    info!("inserting credit registration account linking emails");
484
485    let english_subject = Some("Link your student number to register your credits");
486    let english_body = json!([
487        {
488            "type": "core/paragraph",
489            "isValid": true,
490            "clientId": "d1000000-0000-0000-0000-000000000001",
491            "attributes": {
492                "content": "Hello {{NAME}}, you completed {{COURSE_NAME}} on courses.mooc.fi and we can register the credits for you.",
493                "drop_cap": false
494            },
495            "innerBlocks": []
496        },
497        {
498            "type": "core/paragraph",
499            "isValid": true,
500            "clientId": "d1000000-0000-0000-0000-000000000002",
501            "attributes": {
502                "content": "Open this link while logged in to confirm that student number {{STUDENT_NUMBER}} is yours: {{LINK}}",
503                "drop_cap": false
504            },
505            "innerBlocks": []
506        },
507        {
508            "type": "core/paragraph",
509            "isValid": true,
510            "clientId": "d1000000-0000-0000-0000-000000000003",
511            "attributes": {
512                "content": "The link is valid for 14 days and can be used once. You received this message because you are enrolled in {{COURSE_NAME}} at the University of Helsinki and completed it on courses.mooc.fi. If this was not you, please ignore this message.",
513                "drop_cap": false
514            },
515            "innerBlocks": []
516        }
517    ]);
518
519    insert_email_template(
520        conn,
521        None,
522        EmailTemplateNew {
523            template_type: EmailTemplateType::CreditRegistrationAccountLinking,
524            language: Some("en".to_string()),
525            content: Some(english_body),
526            subject: english_subject.map(|s| s.to_string()),
527        },
528        english_subject,
529    )
530    .await?;
531
532    let finnish_subject = Some("Yhdistä opiskelijanumerosi, jotta voimme kirjata opintopisteesi");
533    let finnish_body = json!([
534        {
535            "type": "core/paragraph",
536            "isValid": true,
537            "clientId": "d2000000-0000-0000-0000-000000000001",
538            "attributes": {
539                "content": "Hei {{NAME}}, olet suorittanut kurssin {{COURSE_NAME}} courses.mooc.fi-palvelussa ja voimme kirjata opintopisteet puolestasi.",
540                "drop_cap": false
541            },
542            "innerBlocks": []
543        },
544        {
545            "type": "core/paragraph",
546            "isValid": true,
547            "clientId": "d2000000-0000-0000-0000-000000000002",
548            "attributes": {
549                "content": "Avaa tämä linkki kirjautuneena vahvistaaksesi, että opiskelijanumero {{STUDENT_NUMBER}} on sinun: {{LINK}}",
550                "drop_cap": false
551            },
552            "innerBlocks": []
553        },
554        {
555            "type": "core/paragraph",
556            "isValid": true,
557            "clientId": "d2000000-0000-0000-0000-000000000003",
558            "attributes": {
559                "content": "Linkki on voimassa 14 päivää ja sen voi käyttää kertaalleen. Sait tämän viestin, koska olet ilmoittautunut kurssille {{COURSE_NAME}} Helsingin yliopistossa ja suorittanut sen courses.mooc.fi-palvelussa. Jos tämä ei ollut sinä, voit jättää viestin huomiotta.",
560                "drop_cap": false
561            },
562            "innerBlocks": []
563        }
564    ]);
565
566    insert_email_template(
567        conn,
568        None,
569        EmailTemplateNew {
570            template_type: EmailTemplateType::CreditRegistrationAccountLinking,
571            language: Some("fi".to_string()),
572            content: Some(finnish_body),
573            subject: finnish_subject.map(|s| s.to_string()),
574        },
575        finnish_subject,
576    )
577    .await?;
578
579    Ok(())
580}
581
582/// The sender looks the account's pending code up at send time and substitutes `{{CODE}}`, as it does
583/// for the login and account deletion codes.
584///
585/// A migration cannot insert a template row using an enum value it adds itself, so in dev and tests
586/// the `verify_email_address` templates come only from here.
587async fn seed_email_ownership_verification_templates(
588    conn: &mut sqlx::PgConnection,
589) -> anyhow::Result<()> {
590    info!("inserting email address verification emails");
591
592    let english_subject = Some("Confirm your email address");
593    let english_body = json!([
594        {
595            "type": "core/paragraph",
596            "isValid": true,
597            "clientId": "77777777-7777-7777-7777-777777777777",
598            "attributes": {
599                "content": "Hello, please use this code to confirm the email address on your account.",
600                "drop_cap": false
601            },
602            "innerBlocks": []
603        },
604        {
605            "type": "core/paragraph",
606            "isValid": true,
607            "clientId": "88888888-8888-8888-8888-888888888888",
608            "attributes": {
609                "content": "Your confirmation code is: {{CODE}}",
610                "drop_cap": false
611            },
612            "innerBlocks": []
613        },
614        {
615            "type": "core/paragraph",
616            "isValid": true,
617            "clientId": "99999999-9999-9999-9999-999999999999",
618            "attributes": {
619                "content": "If you did not request this, you can ignore this message. Nothing changes until the code is entered.",
620                "drop_cap": false
621            },
622            "innerBlocks": []
623        }
624    ]);
625
626    insert_email_template(
627        conn,
628        None,
629        EmailTemplateNew {
630            template_type: EmailTemplateType::VerifyEmailAddress,
631            language: Some("en".to_string()),
632            content: Some(english_body),
633            subject: english_subject.map(|s| s.to_string()),
634        },
635        english_subject,
636    )
637    .await?;
638
639    let finnish_subject = Some("Vahvista sähköpostiosoitteesi");
640    let finnish_body = json!([
641        {
642            "type": "core/paragraph",
643            "isValid": true,
644            "clientId": "aaaaaaaa-7777-7777-7777-777777777777",
645            "attributes": {
646                "content": "Hei, vahvista tilisi sähköpostiosoite tällä koodilla.",
647                "drop_cap": false
648            },
649            "innerBlocks": []
650        },
651        {
652            "type": "core/paragraph",
653            "isValid": true,
654            "clientId": "bbbbbbbb-8888-8888-8888-888888888888",
655            "attributes": {
656                "content": "Vahvistuskoodisi on: {{CODE}}",
657                "drop_cap": false
658            },
659            "innerBlocks": []
660        },
661        {
662            "type": "core/paragraph",
663            "isValid": true,
664            "clientId": "cccccccc-9999-9999-9999-999999999999",
665            "attributes": {
666                "content": "Jos et pyytänyt tätä, voit jättää viestin huomiotta. Mikään ei muutu ennen kuin koodi syötetään.",
667                "drop_cap": false
668            },
669            "innerBlocks": []
670        }
671    ]);
672
673    insert_email_template(
674        conn,
675        None,
676        EmailTemplateNew {
677            template_type: EmailTemplateType::VerifyEmailAddress,
678            language: Some("fi".to_string()),
679            content: Some(finnish_body),
680            subject: finnish_subject.map(|s| s.to_string()),
681        },
682        finnish_subject,
683    )
684    .await?;
685
686    Ok(())
687}