headless_lms_models/
code_giveaway_codes.rs

1use futures::Stream;
2use sqlx::{QueryBuilder, Row};
3
4use crate::prelude::*;
5
6#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
7#[cfg_attr(feature = "ts_rs", derive(TS))]
8pub struct CodeGiveawayCode {
9    pub id: Uuid,
10    pub created_at: DateTime<Utc>,
11    pub updated_at: DateTime<Utc>,
12    pub deleted_at: Option<DateTime<Utc>>,
13    pub code_giveaway_id: Uuid,
14    pub code_given_to_user_id: Option<Uuid>,
15    pub added_by_user_id: Uuid,
16    pub code: String,
17}
18
19pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CodeGiveawayCode> {
20    let res = sqlx::query_as!(
21        CodeGiveawayCode,
22        r#"
23SELECT *
24FROM code_giveaway_codes
25WHERE id = $1
26        "#,
27        id
28    )
29    .fetch_one(conn)
30    .await?;
31    Ok(res)
32}
33
34pub async fn insert_many(
35    conn: &mut PgConnection,
36    code_giveaway_id: Uuid,
37    input: &[String],
38    added_by_user_id: Uuid,
39) -> ModelResult<Vec<CodeGiveawayCode>> {
40    if input.is_empty() {
41        return Ok(vec![]);
42    }
43    let mut query_builder = QueryBuilder::new(
44        "INSERT INTO code_giveaway_codes (code_giveaway_id, code, added_by_user_id) ",
45    );
46
47    query_builder.push_values(input, |mut b, code| {
48        b.push_bind(code_giveaway_id)
49            .push_bind(code.trim())
50            .push_bind(added_by_user_id);
51    });
52
53    query_builder.push(" RETURNING id");
54
55    let query = query_builder.build();
56
57    let ids: Vec<Uuid> = query
58        .fetch_all(&mut *conn)
59        .await?
60        .iter()
61        .map(|row| row.get("id"))
62        .collect();
63
64    // Fetch the inserted rows to return them
65    let res = sqlx::query_as!(
66        CodeGiveawayCode,
67        r#"
68SELECT *
69FROM code_giveaway_codes
70WHERE code_giveaway_id = $1
71  AND added_by_user_id = $2
72  AND id = ANY($3)
73    "#,
74        code_giveaway_id,
75        added_by_user_id,
76        &ids
77    )
78    .fetch_all(&mut *conn)
79    .await?;
80
81    Ok(res)
82}
83
84pub async fn get_all_by_code_giveaway_id(
85    conn: &mut PgConnection,
86    code_giveaway_id: Uuid,
87) -> ModelResult<Vec<CodeGiveawayCode>> {
88    let res = sqlx::query_as!(
89        CodeGiveawayCode,
90        r#"
91SELECT *
92FROM code_giveaway_codes
93WHERE code_giveaway_id = $1
94  AND deleted_at IS NULL
95        "#,
96        code_giveaway_id
97    )
98    .fetch_all(conn)
99    .await?;
100    Ok(res)
101}
102
103pub async fn get_code_given_to_user(
104    conn: &mut PgConnection,
105    code_giveaway_id: Uuid,
106    user_id: Uuid,
107) -> ModelResult<Option<CodeGiveawayCode>> {
108    let res = sqlx::query_as!(
109        CodeGiveawayCode,
110        r#"
111SELECT *
112FROM code_giveaway_codes
113WHERE code_giveaway_id = $1
114  AND code_given_to_user_id = $2
115  AND deleted_at IS NULL
116        "#,
117        code_giveaway_id,
118        user_id
119    )
120    .fetch_optional(conn)
121    .await?;
122    Ok(res)
123}
124
125pub async fn give_some_code_to_user(
126    conn: &mut PgConnection,
127    code_giveaway_id: Uuid,
128    user_id: Uuid,
129) -> ModelResult<CodeGiveawayCode> {
130    let res = sqlx::query_as!(
131        CodeGiveawayCode,
132        r#"
133WITH to_update AS (
134    SELECT *
135    FROM code_giveaway_codes
136    WHERE code_giveaway_id = $1
137      AND code_given_to_user_id IS NULL
138      AND deleted_at IS NULL
139    LIMIT 1
140)
141UPDATE code_giveaway_codes cgc
142SET code_given_to_user_id = $2
143FROM to_update
144WHERE cgc.id = to_update.id
145RETURNING cgc.*
146        "#,
147        code_giveaway_id,
148        user_id
149    )
150    .fetch_one(conn)
151    .await?;
152    Ok(res)
153}
154
155#[allow(clippy::needless_lifetimes)]
156pub async fn stream_given_code_giveaway_codes<'a>(
157    conn: &'a mut PgConnection,
158    code_giveaway_id: Uuid,
159) -> impl Stream<Item = sqlx::Result<CodeGiveawayCode>> + 'a {
160    sqlx::query_as!(
161        CodeGiveawayCode,
162        r#"
163SELECT *
164FROM code_giveaway_codes
165WHERE code_giveaway_id = $1
166  AND deleted_at IS NULL
167  AND code_given_to_user_id IS NOT NULL
168            "#,
169        code_giveaway_id
170    )
171    .fetch(conn)
172}
173
174pub async fn delete_by_id(conn: &mut PgConnection, code_id: Uuid) -> ModelResult<CodeGiveawayCode> {
175    let res = sqlx::query_as!(
176        CodeGiveawayCode,
177        r#"
178UPDATE code_giveaway_codes
179SET deleted_at = now()
180WHERE id = $1
181AND deleted_at IS NULL
182RETURNING *
183        "#,
184        code_id
185    )
186    .fetch_one(conn)
187    .await?;
188    Ok(res)
189}
190
191pub async fn are_any_codes_left(
192    conn: &mut PgConnection,
193    code_giveaway_id: Uuid,
194) -> ModelResult<bool> {
195    let res = sqlx::query!(
196        r#"
197SELECT EXISTS(
198    SELECT 1
199    FROM code_giveaway_codes
200    WHERE code_giveaway_id = $1
201      AND code_given_to_user_id IS NULL
202      AND deleted_at IS NULL
203    LIMIT 1
204  )
205        "#,
206        code_giveaway_id
207    )
208    .fetch_one(conn)
209    .await?;
210    Ok(res.exists.unwrap())
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::{code_giveaways::NewCodeGiveaway, test_helper::*};
217
218    #[tokio::test]
219    async fn test_insert_many_empty() {
220        insert_data!(:tx, :user, :org, :course);
221
222        let code_giveaway = crate::code_giveaways::insert(
223            tx.as_mut(),
224            &NewCodeGiveaway {
225                course_id: course,
226                name: "Test giveaway".to_string(),
227                course_module_id: None,
228                require_course_specific_consent_form_question_id: None,
229            },
230        )
231        .await
232        .unwrap();
233
234        let insert_result = insert_many(tx.as_mut(), code_giveaway.id, &[], user)
235            .await
236            .unwrap();
237
238        assert!(insert_result.is_empty());
239    }
240
241    #[tokio::test]
242    async fn test_insert_many_with_data() {
243        insert_data!(:tx, :user, :org, :course);
244
245        let code_giveaway = crate::code_giveaways::insert(
246            tx.as_mut(),
247            &NewCodeGiveaway {
248                course_id: course,
249                name: "Test giveaway".to_string(),
250                course_module_id: None,
251                require_course_specific_consent_form_question_id: None,
252            },
253        )
254        .await
255        .unwrap();
256
257        let codes = vec![
258            "code1".to_string(),
259            "code2".to_string(),
260            "code3".to_string(),
261        ];
262
263        let insert_result = insert_many(tx.as_mut(), code_giveaway.id, &codes, user)
264            .await
265            .unwrap();
266
267        assert_eq!(insert_result.len(), codes.len());
268        for code in &codes {
269            let found = insert_result.iter().find(|c| c.code == *code);
270            assert!(found.is_some());
271        }
272        // Double checking
273        let all_codes = get_all_by_code_giveaway_id(tx.as_mut(), code_giveaway.id)
274            .await
275            .unwrap();
276        assert_eq!(all_codes.len(), codes.len());
277        for code in &codes {
278            let found = all_codes.iter().find(|c| c.code == *code);
279            assert!(found.is_some());
280        }
281    }
282}