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, Clone, ToSchema)]
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    #[schema(value_type = String)]
17    pub code: OutboundSecret,
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    use secrecy::ExposeSecret;
225
226    #[tokio::test]
227    async fn test_insert_many_empty() {
228        insert_data!(:tx, :user, :org, :course);
229
230        let code_giveaway = crate::code_giveaways::insert(
231            tx.as_mut(),
232            &NewCodeGiveaway {
233                course_id: course,
234                name: "Test giveaway".to_string(),
235                course_module_id: None,
236                require_course_specific_consent_form_question_id: None,
237            },
238        )
239        .await
240        .unwrap();
241
242        let insert_result = insert_many(tx.as_mut(), code_giveaway.id, &[], user)
243            .await
244            .unwrap();
245
246        assert!(insert_result.is_empty());
247    }
248
249    #[tokio::test]
250    async fn test_insert_many_with_data() {
251        insert_data!(:tx, :user, :org, :course);
252
253        let code_giveaway = crate::code_giveaways::insert(
254            tx.as_mut(),
255            &NewCodeGiveaway {
256                course_id: course,
257                name: "Test giveaway".to_string(),
258                course_module_id: None,
259                require_course_specific_consent_form_question_id: None,
260            },
261        )
262        .await
263        .unwrap();
264
265        let codes = vec![
266            "code1".to_string(),
267            "code2".to_string(),
268            "code3".to_string(),
269        ];
270
271        let insert_result = insert_many(tx.as_mut(), code_giveaway.id, &codes, user)
272            .await
273            .unwrap();
274
275        assert_eq!(insert_result.len(), codes.len());
276        for code in &codes {
277            let found = insert_result
278                .iter()
279                .find(|c| c.code.expose_secret() == code.as_str());
280            assert!(found.is_some());
281        }
282        // Double checking
283        let all_codes = get_all_by_code_giveaway_id(tx.as_mut(), code_giveaway.id)
284            .await
285            .unwrap();
286        assert_eq!(all_codes.len(), codes.len());
287        for code in &codes {
288            let found = all_codes
289                .iter()
290                .find(|c| c.code.expose_secret() == code.as_str());
291            assert!(found.is_some());
292        }
293    }
294}