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 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 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}