headless_lms_models/
generated_certificates.rs

1use crate::prelude::*;
2use headless_lms_utils as utils;
3
4#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
5#[cfg_attr(feature = "ts_rs", derive(TS))]
6pub struct GeneratedCertificate {
7    pub id: Uuid,
8    pub created_at: DateTime<Utc>,
9    pub updated_at: DateTime<Utc>,
10    pub deleted_at: Option<DateTime<Utc>>,
11    pub user_id: Uuid,
12    pub name_on_certificate: String,
13    pub verification_id: String,
14    pub certificate_configuration_id: Uuid,
15}
16
17pub async fn get_certificate_for_user(
18    conn: &mut PgConnection,
19    user_id: Uuid,
20    certificate_configuration_id: Uuid,
21) -> ModelResult<GeneratedCertificate> {
22    let res = sqlx::query_as!(
23        GeneratedCertificate,
24        "
25SELECT *
26FROM generated_certificates
27WHERE user_id = $1
28  AND certificate_configuration_id = $2
29  AND deleted_at IS NULL
30",
31        user_id,
32        certificate_configuration_id
33    )
34    .fetch_one(conn)
35    .await?;
36    Ok(res)
37}
38/// Verifies that the user has completed the given module and creates the certificate in the database.
39pub async fn generate_and_insert(
40    conn: &mut PgConnection,
41    user_id: Uuid,
42    name_on_certificate: &str,
43    certificate_configuration_id: Uuid,
44) -> ModelResult<GeneratedCertificate> {
45    let requirements = crate::certificate_configuration_to_requirements::get_all_requirements_for_certificate_configuration(conn, certificate_configuration_id).await?;
46    // Verify that the user has completed the module in the course instance
47    if !requirements
48        .has_user_completed_all_requirements(conn, user_id)
49        .await?
50    {
51        return Err(ModelError::new(
52            ModelErrorType::PreconditionFailed,
53            "User has not completed all the requirements to be eligible for this certificate."
54                .to_string(),
55            None,
56        ));
57    }
58
59    // Verify that a certificate doesn't already exist
60    if sqlx::query!(
61        "
62SELECT id
63FROM generated_certificates
64WHERE user_id = $1
65    AND certificate_configuration_id = $2
66    AND deleted_at IS NULL
67",
68        user_id,
69        certificate_configuration_id,
70    )
71    .fetch_optional(&mut *conn)
72    .await?
73    .is_some()
74    {
75        // Certificate already exists
76        return Err(ModelError::new(
77            ModelErrorType::PreconditionFailed,
78            "User already has a certificate for the given module and course instance".to_string(),
79            None,
80        ));
81    }
82
83    let verification_id = generate_verification_id();
84    let res = sqlx::query_as!(
85        GeneratedCertificate,
86        "
87INSERT INTO generated_certificates (
88    user_id,
89    certificate_configuration_id,
90    name_on_certificate,
91    verification_id
92  )
93VALUES ($1, $2, $3, $4)
94RETURNING *
95",
96        user_id,
97        certificate_configuration_id,
98        name_on_certificate,
99        verification_id,
100    )
101    .fetch_one(conn)
102    .await?;
103    Ok(res)
104}
105
106pub async fn get_certificate_by_verification_id(
107    conn: &mut PgConnection,
108    certificate_verification_id: &str,
109) -> ModelResult<GeneratedCertificate> {
110    let res = sqlx::query_as!(
111        GeneratedCertificate,
112        "
113SELECT *
114FROM generated_certificates
115WHERE verification_id = $1
116  AND deleted_at IS NULL
117",
118        certificate_verification_id
119    )
120    .fetch_one(conn)
121    .await?;
122    Ok(res)
123}
124
125fn generate_verification_id() -> String {
126    utils::strings::generate_easily_writable_random_string(15)
127}
128
129#[derive(Debug, Deserialize, Serialize)]
130#[cfg_attr(feature = "ts_rs", derive(TS))]
131pub struct CertificateUpdateRequest {
132    pub date_issued: DateTime<Utc>,
133    pub name_on_certificate: Option<String>,
134}
135
136pub async fn update_certificate(
137    conn: &mut PgConnection,
138    certificate_id: Uuid,
139    date_issued: DateTime<Utc>,
140    name_on_certificate: Option<String>,
141) -> ModelResult<GeneratedCertificate> {
142    let updated = if let Some(name) = name_on_certificate {
143        sqlx::query_as!(
144            GeneratedCertificate,
145            r#"
146            UPDATE generated_certificates
147            SET created_at = $1,
148                name_on_certificate = $2,
149                updated_at = NOW()
150            WHERE id = $3
151              AND deleted_at IS NULL
152            RETURNING *
153            "#,
154            date_issued,
155            name,
156            certificate_id
157        )
158        .fetch_one(conn)
159        .await?
160    } else {
161        sqlx::query_as!(
162            GeneratedCertificate,
163            r#"
164            UPDATE generated_certificates
165            SET created_at = $1,
166                updated_at = NOW()
167            WHERE id = $2
168              AND deleted_at IS NULL
169            RETURNING *
170            "#,
171            date_issued,
172            certificate_id
173        )
174        .fetch_one(conn)
175        .await?
176    };
177
178    Ok(updated)
179}
180
181pub async fn get_by_id(
182    conn: &mut PgConnection,
183    certificate_id: Uuid,
184) -> ModelResult<GeneratedCertificate> {
185    let res = sqlx::query_as!(
186        GeneratedCertificate,
187        r#"
188        SELECT *
189        FROM generated_certificates
190        WHERE id = $1
191          AND deleted_at IS NULL
192        "#,
193        certificate_id
194    )
195    .fetch_one(conn)
196    .await?;
197
198    Ok(res)
199}
200
201pub async fn find_existing(
202    conn: &mut PgConnection,
203    user_id: Uuid,
204    config_id: Uuid,
205) -> ModelResult<Option<Uuid>> {
206    let row = sqlx::query!(
207        r#"
208        SELECT id
209        FROM generated_certificates
210        WHERE user_id = $1
211          AND certificate_configuration_id = $2
212          AND deleted_at IS NULL
213        "#,
214        user_id,
215        config_id
216    )
217    .fetch_optional(conn)
218    .await?;
219
220    Ok(row.map(|r| r.id))
221}
222
223pub async fn insert_raw(
224    conn: &mut PgConnection,
225    user_id: Uuid,
226    config_id: Uuid,
227    name: &str,
228    verification_id: &str,
229) -> ModelResult<Uuid> {
230    let row = sqlx::query!(
231        r#"
232        INSERT INTO generated_certificates (
233            user_id,
234            certificate_configuration_id,
235            name_on_certificate,
236            verification_id
237        )
238        VALUES ($1, $2, $3, $4)
239        RETURNING id
240        "#,
241        user_id,
242        config_id,
243        name,
244        verification_id
245    )
246    .fetch_one(conn)
247    .await?;
248
249    Ok(row.id)
250}