headless_lms_models/credit_registrations/
registration.rs1use super::state::{CreditRegistrationErrorCode, CreditRegistrationState, ResubmissionFacts};
4use crate::credit_registration_events::{CreditRegistrationEventKind, NewCreditRegistrationEvent};
5use crate::library::credit_registration::enrolment_check_schedule::{
6 EnrolmentCheckGroup, EnrolmentCheckSource,
7};
8use crate::library::credit_registration::grade_mapping::MappedGrade;
9use crate::prelude::*;
10
11#[derive(Debug, Deserialize, Clone)]
12pub struct CreditRegistration {
13 pub id: Uuid,
14 pub created_at: DateTime<Utc>,
15 pub updated_at: DateTime<Utc>,
16 pub deleted_at: Option<DateTime<Utc>>,
17 pub course_module_completion_id: Uuid,
18 pub user_id: Uuid,
19 pub course_id: Uuid,
20 pub course_module_id: Uuid,
21 pub course_instance_id: Uuid,
22 pub state: CreditRegistrationState,
23 pub state_entered_at: DateTime<Utc>,
24 pub error_code: Option<CreditRegistrationErrorCode>,
25 pub error_message: Option<String>,
26 pub needs_admin_attention: bool,
27 pub enrolment_banner_dismissed_at: Option<DateTime<Utc>>,
28 pub student_number: Option<DbSecret>,
29 pub sisu_person_id: Option<DbSecret>,
30 pub uh_course_code: Option<String>,
31 pub selected_enrolment_id: Option<String>,
32 pub selected_enrolment_kind: Option<String>,
33 pub selected_enrolment_realisation_id: Option<String>,
34 pub attained_at: Option<DateTime<Utc>>,
35 pub attainment_language: Option<String>,
36 pub grade_scale_id: Option<String>,
37 pub grade_id: Option<String>,
38 pub credits: Option<f32>,
39 pub submitted_attainment_id: Option<String>,
40 pub submitted_attainment_type: Option<String>,
41 pub sisu_attainment_id: Option<String>,
42 pub sisu_attainment_type: Option<String>,
43 pub submit_retry_count: i32,
44 pub verify_attempt_count: i32,
45 pub next_attempt_at: DateTime<Utc>,
46 pub first_failed_at: Option<DateTime<Utc>>,
47 pub last_attempt_at: Option<DateTime<Utc>>,
48 pub attempt_number: i32,
49 pub superseded_by_id: Option<Uuid>,
50 pub superseded_at: Option<DateTime<Utc>>,
51 pub enrolment_checked_at: Option<DateTime<Utc>>,
52 pub submitted_at: Option<DateTime<Utc>>,
53 pub registered_at: Option<DateTime<Utc>>,
54 pub terminal_at: Option<DateTime<Utc>>,
55 pub action_needed_email_delivery_id: Option<Uuid>,
58 pub registered_email_delivery_id: Option<Uuid>,
59 pub improvement_checked_completion_updated_at: Option<DateTime<Utc>>,
62 pub partially_registered_at: Option<DateTime<Utc>>,
64 pub not_registered_reimport_count: i32,
65 pub selected_enrolment_realisation_name: Option<serde_json::Value>,
67 pub resubmit_not_before: Option<DateTime<Utc>>,
69 pub pending_superseded_by_id: Option<Uuid>,
73 pub no_usable_enrolment_since: Option<DateTime<Utc>>,
76 pub enrolment_check_group: EnrolmentCheckGroup,
78 pub enrolment_check_anchor_at: Option<DateTime<Utc>>,
81 pub enrolment_check_step: Option<i32>,
82 pub enrolment_check_due_at: Option<DateTime<Utc>>,
84 pub is_enrolment_check_batched: bool,
85 pub enrolment_check_source: EnrolmentCheckSource,
86 pub enrolment_checks_stopped_at: Option<DateTime<Utc>>,
87 pub enrolment_check_requested_at: Option<DateTime<Utc>>,
88 pub enrolment_check_restart_window_started_at: Option<DateTime<Utc>>,
89 pub enrolment_check_restart_count: i32,
90 pub seen_enrolment_ids: Option<Vec<String>>,
93 pub enrolment_check_claimed_until: Option<DateTime<Utc>>,
96 pub state_changed_at: DateTime<Utc>,
99 pub phase_started_at: DateTime<Utc>,
102}
103
104impl CreditRegistration {
105 pub fn resubmission_facts(&self) -> ResubmissionFacts {
107 ResubmissionFacts {
108 state: self.state,
109 is_superseded: self.superseded_by_id.is_some(),
110 error_code: self.error_code,
111 resubmit_not_before: self.resubmit_not_before,
112 submitted_at: self.submitted_at,
113 }
114 }
115
116 pub fn frozen_grade(&self) -> Option<MappedGrade> {
118 MappedGrade::from_columns(self.grade_scale_id.as_deref(), self.grade_id.as_deref())
119 }
120
121 pub fn is_waiting_for_enrolment(&self) -> bool {
123 is_waiting_for_enrolment(
124 self.state,
125 self.enrolment_check_anchor_at,
126 self.no_usable_enrolment_since,
127 )
128 }
129}
130
131pub fn is_waiting_for_enrolment(
135 state: CreditRegistrationState,
136 enrolment_check_anchor_at: Option<DateTime<Utc>>,
137 no_usable_enrolment_since: Option<DateTime<Utc>>,
138) -> bool {
139 state.keeps_enrolment_check_schedule()
140 && (enrolment_check_anchor_at.is_some() || no_usable_enrolment_since.is_some())
141}
142
143#[derive(Debug, Clone, PartialEq)]
144pub struct NewCreditRegistration {
145 pub course_module_completion_id: Uuid,
146 pub user_id: Uuid,
147 pub course_id: Uuid,
148 pub course_module_id: Uuid,
149 pub course_instance_id: Uuid,
150 pub attempt_number: i32,
151}
152
153pub async fn insert(
155 conn: &mut PgConnection,
156 pkey_policy: PKeyPolicy<Uuid>,
157 new: &NewCreditRegistration,
158 event_message: Option<&str>,
159) -> ModelResult<Uuid> {
160 let id = pkey_policy.into_uuid();
161 let mut tx = conn.begin().await?;
162 sqlx::query!(
163 r#"
164INSERT INTO credit_registrations (
165 id,
166 course_module_completion_id,
167 user_id,
168 course_id,
169 course_module_id,
170 course_instance_id,
171 attempt_number
172 )
173VALUES ($1, $2, $3, $4, $5, $6, $7)
174 "#,
175 id,
176 new.course_module_completion_id,
177 new.user_id,
178 new.course_id,
179 new.course_module_id,
180 new.course_instance_id,
181 new.attempt_number,
182 )
183 .execute(&mut *tx)
184 .await?;
185
186 crate::credit_registration_events::insert(
187 &mut tx,
188 &NewCreditRegistrationEvent {
189 message: event_message.map(str::to_string),
190 ..NewCreditRegistrationEvent::new(id, CreditRegistrationEventKind::Created)
191 },
192 )
193 .await?;
194
195 tx.commit().await?;
196 Ok(id)
197}
198
199pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CreditRegistration> {
200 let res = sqlx::query_as!(
201 CreditRegistration,
202 r#"
203SELECT *
204FROM credit_registrations
205WHERE id = $1
206 AND deleted_at IS NULL
207 "#,
208 id
209 )
210 .fetch_one(conn)
211 .await?;
212 Ok(res)
213}
214
215pub async fn get_by_ids_for_update(
222 conn: &mut PgConnection,
223 ids: &[Uuid],
224) -> ModelResult<Vec<CreditRegistration>> {
225 let res = sqlx::query_as!(
226 CreditRegistration,
227 r#"
228SELECT *
229FROM credit_registrations
230WHERE id = ANY($1::uuid [])
231 AND deleted_at IS NULL
232ORDER BY id FOR UPDATE
233 "#,
234 ids
235 )
236 .fetch_all(conn)
237 .await?;
238 Ok(res)
239}
240
241pub async fn get_by_user_id(
242 conn: &mut PgConnection,
243 user_id: Uuid,
244) -> ModelResult<Vec<CreditRegistration>> {
245 let res = sqlx::query_as!(
246 CreditRegistration,
247 r#"
248SELECT *
249FROM credit_registrations
250WHERE user_id = $1
251 AND deleted_at IS NULL
252ORDER BY created_at DESC
253 "#,
254 user_id
255 )
256 .fetch_all(conn)
257 .await?;
258 Ok(res)
259}
260
261pub async fn get_by_course_id(
262 conn: &mut PgConnection,
263 course_id: Uuid,
264) -> ModelResult<Vec<CreditRegistration>> {
265 let res = sqlx::query_as!(
266 CreditRegistration,
267 r#"
268SELECT *
269FROM credit_registrations
270WHERE course_id = $1
271 AND deleted_at IS NULL
272ORDER BY created_at DESC
273 "#,
274 course_id
275 )
276 .fetch_all(conn)
277 .await?;
278 Ok(res)
279}
280
281pub async fn exists_for_user_and_course(
286 conn: &mut PgConnection,
287 user_id: Uuid,
288 course_id: Uuid,
289) -> ModelResult<bool> {
290 let exists = sqlx::query_scalar!(
291 r#"
292SELECT EXISTS (
293 SELECT 1
294 FROM credit_registrations
295 WHERE user_id = $1
296 AND course_id = $2
297 AND deleted_at IS NULL
298 ) AS "exists!"
299 "#,
300 user_id,
301 course_id,
302 )
303 .fetch_one(conn)
304 .await?;
305 Ok(exists)
306}