1use crate::prelude::*;
2use headless_lms_utils as utils;
3use utoipa::ToSchema;
4
5#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
6
7pub struct GeneratedCertificate {
8 pub id: Uuid,
9 pub created_at: DateTime<Utc>,
10 pub updated_at: DateTime<Utc>,
11 pub deleted_at: Option<DateTime<Utc>>,
12 pub user_id: Uuid,
13 pub name_on_certificate: String,
14 pub verification_id: String,
15 pub certificate_configuration_id: Uuid,
16}
17
18pub async fn get_certificate_for_user(
19 conn: &mut PgConnection,
20 user_id: Uuid,
21 certificate_configuration_id: Uuid,
22) -> ModelResult<GeneratedCertificate> {
23 let res = sqlx::query_as!(
24 GeneratedCertificate,
25 "
26SELECT *
27FROM generated_certificates
28WHERE user_id = $1
29 AND certificate_configuration_id = $2
30 AND deleted_at IS NULL
31",
32 user_id,
33 certificate_configuration_id
34 )
35 .fetch_one(conn)
36 .await?;
37 Ok(res)
38}
39pub async fn generate_and_insert(
41 conn: &mut PgConnection,
42 user_id: Uuid,
43 name_on_certificate: &str,
44 certificate_configuration_id: Uuid,
45) -> ModelResult<GeneratedCertificate> {
46 let requirements = crate::certificate_configuration_to_requirements::get_all_requirements_for_certificate_configuration(conn, certificate_configuration_id).await?;
47 if !requirements
49 .has_user_completed_all_requirements(conn, user_id)
50 .await?
51 {
52 return Err(ModelError::new(
53 ModelErrorType::PreconditionFailed,
54 "User has not completed all the requirements to be eligible for this certificate."
55 .to_string(),
56 None,
57 ));
58 }
59
60 if sqlx::query!(
62 "
63SELECT id
64FROM generated_certificates
65WHERE user_id = $1
66 AND certificate_configuration_id = $2
67 AND deleted_at IS NULL
68",
69 user_id,
70 certificate_configuration_id,
71 )
72 .fetch_optional(&mut *conn)
73 .await?
74 .is_some()
75 {
76 return Err(ModelError::new(
78 ModelErrorType::PreconditionFailed,
79 "User already has a certificate for the given module and course instance".to_string(),
80 None,
81 ));
82 }
83
84 let verification_id = generate_verification_id();
85 let res = sqlx::query_as!(
86 GeneratedCertificate,
87 "
88INSERT INTO generated_certificates (
89 user_id,
90 certificate_configuration_id,
91 name_on_certificate,
92 verification_id
93 )
94VALUES ($1, $2, $3, $4)
95RETURNING *
96",
97 user_id,
98 certificate_configuration_id,
99 name_on_certificate,
100 verification_id,
101 )
102 .fetch_one(conn)
103 .await?;
104 Ok(res)
105}
106
107pub async fn get_certificate_by_verification_id(
108 conn: &mut PgConnection,
109 certificate_verification_id: &str,
110) -> ModelResult<GeneratedCertificate> {
111 let res = sqlx::query_as!(
112 GeneratedCertificate,
113 "
114SELECT *
115FROM generated_certificates
116WHERE verification_id = $1
117 AND deleted_at IS NULL
118",
119 certificate_verification_id
120 )
121 .fetch_one(conn)
122 .await?;
123 Ok(res)
124}
125
126fn generate_verification_id() -> String {
127 utils::strings::generate_easily_writable_random_string(15)
128}
129
130#[derive(Debug, Deserialize, Serialize, ToSchema)]
131
132pub struct CertificateUpdateRequest {
133 pub date_issued: DateTime<Utc>,
134 pub name_on_certificate: Option<String>,
135}
136
137pub async fn update_certificate(
148 conn: &mut PgConnection,
149 certificate_id: Uuid,
150 date_issued: DateTime<Utc>,
151 name_on_certificate: Option<String>,
152 expected_updated_at: Option<DateTime<Utc>>,
153) -> ModelResult<Option<GeneratedCertificate>> {
154 let updated = sqlx::query_as!(
155 GeneratedCertificate,
156 r#"
157UPDATE generated_certificates
158SET created_at = $2,
159 name_on_certificate = COALESCE($3, name_on_certificate),
160 updated_at = NOW()
161WHERE id = $1
162 AND deleted_at IS NULL
163 AND (
164 $4::timestamptz IS NULL
165 OR updated_at = $4
166 )
167RETURNING *
168 "#,
169 certificate_id,
170 date_issued,
171 name_on_certificate,
172 expected_updated_at
173 )
174 .fetch_optional(conn)
175 .await?;
176 Ok(updated)
177}
178
179pub async fn get_by_id(
180 conn: &mut PgConnection,
181 certificate_id: Uuid,
182) -> ModelResult<GeneratedCertificate> {
183 let res = sqlx::query_as!(
184 GeneratedCertificate,
185 r#"
186 SELECT *
187 FROM generated_certificates
188 WHERE id = $1
189 AND deleted_at IS NULL
190 "#,
191 certificate_id
192 )
193 .fetch_one(conn)
194 .await?;
195
196 Ok(res)
197}
198
199#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
201pub struct UserCertificate {
202 pub id: Uuid,
203 pub user_id: Uuid,
204 pub name_on_certificate: String,
205 pub verification_id: String,
207 pub created_at: DateTime<Utc>,
208 pub course_id: Uuid,
209 pub course_name: String,
210 pub course_module_name: Option<String>,
212}
213
214pub async fn get_all_by_user_id(
220 conn: &mut PgConnection,
221 user_id: Uuid,
222) -> ModelResult<Vec<UserCertificate>> {
223 let res = sqlx::query_as!(
224 UserCertificate,
225 r#"
226SELECT DISTINCT ON (gc.id) gc.id,
227 gc.user_id,
228 gc.name_on_certificate,
229 gc.verification_id,
230 gc.created_at,
231 c.id AS course_id,
232 c.name AS course_name,
233 cm.name AS course_module_name
234FROM generated_certificates gc
235 JOIN certificate_configuration_to_requirements cctr ON cctr.certificate_configuration_id = gc.certificate_configuration_id
236 AND cctr.deleted_at IS NULL
237 JOIN course_modules cm ON cm.id = cctr.course_module_id
238 AND cm.deleted_at IS NULL
239 JOIN courses c ON c.id = cm.course_id
240 AND c.deleted_at IS NULL
241WHERE gc.user_id = $1
242 AND gc.deleted_at IS NULL
243ORDER BY gc.id,
244 cm.order_number
245 "#,
246 user_id
247 )
248 .fetch_all(conn)
249 .await?;
250 let mut res = res;
252 res.sort_by_key(|certificate| std::cmp::Reverse(certificate.created_at));
253 Ok(res)
254}
255
256pub async fn get_by_verification_id(
261 conn: &mut PgConnection,
262 verification_id: &str,
263) -> ModelResult<Option<UserCertificate>> {
264 let res = sqlx::query_as!(
265 UserCertificate,
266 r#"
267SELECT DISTINCT ON (gc.id) gc.id,
268 gc.user_id,
269 gc.name_on_certificate,
270 gc.verification_id,
271 gc.created_at,
272 c.id AS course_id,
273 c.name AS course_name,
274 cm.name AS course_module_name
275FROM generated_certificates gc
276 JOIN certificate_configuration_to_requirements cctr ON cctr.certificate_configuration_id = gc.certificate_configuration_id
277 AND cctr.deleted_at IS NULL
278 JOIN course_modules cm ON cm.id = cctr.course_module_id
279 AND cm.deleted_at IS NULL
280 JOIN courses c ON c.id = cm.course_id
281 AND c.deleted_at IS NULL
282WHERE gc.verification_id = $1
283 AND gc.deleted_at IS NULL
284ORDER BY gc.id,
285 cm.order_number
286 "#,
287 verification_id
288 )
289 .fetch_optional(conn)
290 .await?;
291 Ok(res)
292}
293
294pub async fn find_existing(
295 conn: &mut PgConnection,
296 user_id: Uuid,
297 config_id: Uuid,
298) -> ModelResult<Option<Uuid>> {
299 let row = sqlx::query!(
300 r#"
301 SELECT id
302 FROM generated_certificates
303 WHERE user_id = $1
304 AND certificate_configuration_id = $2
305 AND deleted_at IS NULL
306 "#,
307 user_id,
308 config_id
309 )
310 .fetch_optional(conn)
311 .await?;
312
313 Ok(row.map(|r| r.id))
314}
315
316pub async fn insert_raw(
317 conn: &mut PgConnection,
318 user_id: Uuid,
319 config_id: Uuid,
320 name: &str,
321 verification_id: &str,
322) -> ModelResult<Uuid> {
323 let row = sqlx::query!(
324 r#"
325 INSERT INTO generated_certificates (
326 user_id,
327 certificate_configuration_id,
328 name_on_certificate,
329 verification_id
330 )
331 VALUES ($1, $2, $3, $4)
332 RETURNING *
333 "#,
334 user_id,
335 config_id,
336 name,
337 verification_id
338 )
339 .fetch_one(conn)
340 .await?;
341
342 Ok(row.id)
343}