Skip to main content

headless_lms_models/
code_giveaway_codes.rs

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