headless_lms_models/library/credit_registration/
student_notifications.rs1use std::collections::HashMap;
9
10use utoipa::ToSchema;
11
12use crate::credit_registrations::{CreditRegistrationState, RegistrationScope};
13use crate::email_deliveries::{EmailSendStatusReport, get_send_statuses};
14use crate::email_templates::EmailTemplateType;
15use crate::prelude::*;
16
17pub const STUDENT_NOTIFICATION_LIMIT: i64 = 200;
19
20#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, ToSchema)]
22#[serde(rename_all = "snake_case")]
23pub enum CreditRegistrationNotificationKind {
24 ActionNeeded,
26 Registered,
28}
29
30impl CreditRegistrationNotificationKind {
31 pub fn email_template_type(self) -> EmailTemplateType {
32 match self {
33 Self::ActionNeeded => EmailTemplateType::CreditRegistrationActionNeeded,
34 Self::Registered => EmailTemplateType::CreditRegistrationRegistered,
35 }
36 }
37
38 pub fn for_state(state: CreditRegistrationState) -> Option<Self> {
42 match state {
43 CreditRegistrationState::NoUsableEnrolment => Some(Self::ActionNeeded),
44 _ if CreditRegistrationState::SUCCESS_STATES.contains(&state) => Some(Self::Registered),
45 _ => None,
46 }
47 }
48}
49
50#[derive(Debug, Clone, PartialEq)]
53pub struct StudentNotificationToQueue {
54 pub credit_registration_id: Uuid,
55 pub kind: CreditRegistrationNotificationKind,
56 pub user_id: Uuid,
57 pub course_module_id: Uuid,
58 pub course_name: String,
59 pub course_language_code: String,
60 pub course_module_name: Option<String>,
61 pub first_name: Option<String>,
62 pub ects_credits: Option<f32>,
63 pub open_university_product_id: Option<String>,
64}
65
66pub async fn claim_unnotified(
70 conn: &mut PgConnection,
71 scope: &RegistrationScope,
72 limit: i64,
73) -> ModelResult<Vec<StudentNotificationToQueue>> {
74 let res = sqlx::query!(
75 r#"
76SELECT cr.id AS "credit_registration_id!",
77 cr.state AS "state!: CreditRegistrationState",
78 cr.user_id AS "user_id!",
79 cr.course_module_id AS "course_module_id!",
80 c.name AS "course_name!",
81 c.language_code AS "course_language_code!",
82 cm.name AS "course_module_name?",
83 ud.first_name AS "first_name?",
84 cm.ects_credits AS "ects_credits?",
85 conf.open_university_product_id AS "open_university_product_id?"
86FROM credit_registrations cr
87 JOIN courses c ON c.id = cr.course_id
88 JOIN course_modules cm ON cm.id = cr.course_module_id
89 LEFT JOIN user_details ud ON ud.user_id = cr.user_id
90 LEFT JOIN course_module_suotar_configurations conf ON conf.course_module_id = cr.course_module_id
91 AND conf.deleted_at IS NULL
92WHERE cr.deleted_at IS NULL
93 AND (
94 (
95 cr.state = 'no_usable_enrolment'
96 AND cr.action_needed_email_delivery_id IS NULL
97 )
98 OR (
99 cr.state = ANY($5::credit_registration_state [])
100 AND cr.registered_email_delivery_id IS NULL
101 )
102 )
103 AND ($2::uuid IS NULL OR cr.course_id = $2)
104 AND ($3::uuid IS NULL OR cr.user_id = $3)
105 AND (
106 cardinality($4::uuid []) = 0
107 OR cr.id = ANY($4::uuid [])
108 )
109ORDER BY cr.state_entered_at
110FOR UPDATE OF cr SKIP LOCKED
111LIMIT $1
112 "#,
113 limit,
114 scope.course_id,
115 scope.user_id,
116 &scope.credit_registration_ids,
117 &CreditRegistrationState::SUCCESS_STATES as &[CreditRegistrationState],
118 )
119 .fetch_all(conn)
120 .await?;
121
122 Ok(res
123 .into_iter()
124 .map(|row| StudentNotificationToQueue {
125 kind: CreditRegistrationNotificationKind::for_state(row.state)
126 .unwrap_or(CreditRegistrationNotificationKind::Registered),
127 credit_registration_id: row.credit_registration_id,
128 user_id: row.user_id,
129 course_module_id: row.course_module_id,
130 course_name: row.course_name,
131 course_language_code: row.course_language_code,
132 course_module_name: row.course_module_name,
133 first_name: row.first_name,
134 ects_credits: row.ects_credits,
135 open_university_product_id: row.open_university_product_id,
136 })
137 .collect())
138}
139
140pub async fn set_email_delivery_id(
142 conn: &mut PgConnection,
143 credit_registration_id: Uuid,
144 kind: CreditRegistrationNotificationKind,
145 email_delivery_id: Uuid,
146) -> ModelResult<()> {
147 let action_needed = kind == CreditRegistrationNotificationKind::ActionNeeded;
148 sqlx::query!(
149 r#"
150UPDATE credit_registrations
151SET action_needed_email_delivery_id = CASE
152 WHEN $3 THEN $2
153 ELSE action_needed_email_delivery_id
154 END,
155 registered_email_delivery_id = CASE
156 WHEN $3 THEN registered_email_delivery_id
157 ELSE $2
158 END
159WHERE id = $1
160 AND deleted_at IS NULL
161 "#,
162 credit_registration_id,
163 email_delivery_id,
164 action_needed,
165 )
166 .execute(conn)
167 .await?;
168 Ok(())
169}
170
171#[derive(Debug, Clone, PartialEq)]
173pub struct RegistrationNotificationEmail {
174 pub credit_registration_id: Uuid,
175 pub kind: CreditRegistrationNotificationKind,
176 pub email_delivery_id: Uuid,
179 pub send_status: EmailSendStatusReport,
180}
181
182pub async fn get_for_registrations(
185 conn: &mut PgConnection,
186 credit_registration_ids: &[Uuid],
187) -> ModelResult<Vec<RegistrationNotificationEmail>> {
188 let rows = sqlx::query!(
189 r#"
190SELECT id,
191 action_needed_email_delivery_id,
192 registered_email_delivery_id
193FROM credit_registrations
194WHERE id = ANY($1::uuid [])
195 AND deleted_at IS NULL
196 AND (
197 action_needed_email_delivery_id IS NOT NULL
198 OR registered_email_delivery_id IS NOT NULL
199 )
200 "#,
201 credit_registration_ids
202 )
203 .fetch_all(&mut *conn)
204 .await?;
205
206 let delivery_ids: Vec<Uuid> = rows
207 .iter()
208 .flat_map(|row| {
209 [
210 row.action_needed_email_delivery_id,
211 row.registered_email_delivery_id,
212 ]
213 })
214 .flatten()
215 .collect();
216 let reports: HashMap<Uuid, EmailSendStatusReport> =
217 get_send_statuses(conn, &delivery_ids).await?;
218
219 let mut res = Vec::new();
220 for row in rows {
221 for (kind, delivery_id) in [
222 (
223 CreditRegistrationNotificationKind::ActionNeeded,
224 row.action_needed_email_delivery_id,
225 ),
226 (
227 CreditRegistrationNotificationKind::Registered,
228 row.registered_email_delivery_id,
229 ),
230 ] {
231 let Some((delivery_id, report)) =
232 delivery_id.and_then(|id| reports.get(&id).map(|report| (id, report)))
233 else {
234 continue;
235 };
236 res.push(RegistrationNotificationEmail {
237 credit_registration_id: row.id,
238 kind,
239 email_delivery_id: delivery_id,
240 send_status: report.clone(),
241 });
242 }
243 }
244 Ok(res)
245}