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