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 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 res.exists.ok_or_else(|| {
211 ModelError::new(
212 ModelErrorType::Database,
213 "EXISTS query returned None - this should never happen".to_string(),
214 None,
215 )
216 })
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use crate::{code_giveaways::NewCodeGiveaway, test_helper::*};
223
224 #[tokio::test]
225 async fn test_insert_many_empty() {
226 insert_data!(:tx, :user, :org, :course);
227
228 let code_giveaway = crate::code_giveaways::insert(
229 tx.as_mut(),
230 &NewCodeGiveaway {
231 course_id: course,
232 name: "Test giveaway".to_string(),
233 course_module_id: None,
234 require_course_specific_consent_form_question_id: None,
235 },
236 )
237 .await
238 .unwrap();
239
240 let insert_result = insert_many(tx.as_mut(), code_giveaway.id, &[], user)
241 .await
242 .unwrap();
243
244 assert!(insert_result.is_empty());
245 }
246
247 #[tokio::test]
248 async fn test_insert_many_with_data() {
249 insert_data!(:tx, :user, :org, :course);
250
251 let code_giveaway = crate::code_giveaways::insert(
252 tx.as_mut(),
253 &NewCodeGiveaway {
254 course_id: course,
255 name: "Test giveaway".to_string(),
256 course_module_id: None,
257 require_course_specific_consent_form_question_id: None,
258 },
259 )
260 .await
261 .unwrap();
262
263 let codes = vec![
264 "code1".to_string(),
265 "code2".to_string(),
266 "code3".to_string(),
267 ];
268
269 let insert_result = insert_many(tx.as_mut(), code_giveaway.id, &codes, user)
270 .await
271 .unwrap();
272
273 assert_eq!(insert_result.len(), codes.len());
274 for code in &codes {
275 let found = insert_result.iter().find(|c| c.code == *code);
276 assert!(found.is_some());
277 }
278 let all_codes = get_all_by_code_giveaway_id(tx.as_mut(), code_giveaway.id)
280 .await
281 .unwrap();
282 assert_eq!(all_codes.len(), codes.len());
283 for code in &codes {
284 let found = all_codes.iter().find(|c| c.code == *code);
285 assert!(found.is_some());
286 }
287 }
288}