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}
97
98impl CreditRegistration {
99 pub fn resubmission_facts(&self) -> ResubmissionFacts {
101 ResubmissionFacts {
102 state: self.state,
103 is_superseded: self.superseded_by_id.is_some(),
104 error_code: self.error_code,
105 resubmit_not_before: self.resubmit_not_before,
106 submitted_at: self.submitted_at,
107 }
108 }
109
110 pub fn frozen_grade(&self) -> Option<MappedGrade> {
112 MappedGrade::from_columns(self.grade_scale_id.as_deref(), self.grade_id.as_deref())
113 }
114
115 pub fn is_waiting_for_enrolment(&self) -> bool {
117 is_waiting_for_enrolment(
118 self.state,
119 self.enrolment_check_anchor_at,
120 self.no_usable_enrolment_since,
121 )
122 }
123}
124
125pub fn is_waiting_for_enrolment(
129 state: CreditRegistrationState,
130 enrolment_check_anchor_at: Option<DateTime<Utc>>,
131 no_usable_enrolment_since: Option<DateTime<Utc>>,
132) -> bool {
133 state.keeps_enrolment_check_schedule()
134 && (enrolment_check_anchor_at.is_some() || no_usable_enrolment_since.is_some())
135}
136
137#[derive(Debug, Clone, PartialEq)]
138pub struct NewCreditRegistration {
139 pub course_module_completion_id: Uuid,
140 pub user_id: Uuid,
141 pub course_id: Uuid,
142 pub course_module_id: Uuid,
143 pub course_instance_id: Uuid,
144 pub attempt_number: i32,
145}
146
147pub async fn insert(
149 conn: &mut PgConnection,
150 pkey_policy: PKeyPolicy<Uuid>,
151 new: &NewCreditRegistration,
152 event_message: Option<&str>,
153) -> ModelResult<Uuid> {
154 let id = pkey_policy.into_uuid();
155 let mut tx = conn.begin().await?;
156 sqlx::query!(
157 r#"
158INSERT INTO credit_registrations (
159 id,
160 course_module_completion_id,
161 user_id,
162 course_id,
163 course_module_id,
164 course_instance_id,
165 attempt_number
166 )
167VALUES ($1, $2, $3, $4, $5, $6, $7)
168 "#,
169 id,
170 new.course_module_completion_id,
171 new.user_id,
172 new.course_id,
173 new.course_module_id,
174 new.course_instance_id,
175 new.attempt_number,
176 )
177 .execute(&mut *tx)
178 .await?;
179
180 crate::credit_registration_events::insert(
181 &mut tx,
182 &NewCreditRegistrationEvent {
183 message: event_message.map(str::to_string),
184 ..NewCreditRegistrationEvent::new(id, CreditRegistrationEventKind::Created)
185 },
186 )
187 .await?;
188
189 tx.commit().await?;
190 Ok(id)
191}
192
193pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<CreditRegistration> {
194 let res = sqlx::query_as!(
195 CreditRegistration,
196 r#"
197SELECT *
198FROM credit_registrations
199WHERE id = $1
200 AND deleted_at IS NULL
201 "#,
202 id
203 )
204 .fetch_one(conn)
205 .await?;
206 Ok(res)
207}
208
209pub async fn get_by_ids_for_update(
216 conn: &mut PgConnection,
217 ids: &[Uuid],
218) -> ModelResult<Vec<CreditRegistration>> {
219 let res = sqlx::query_as!(
220 CreditRegistration,
221 r#"
222SELECT *
223FROM credit_registrations
224WHERE id = ANY($1::uuid [])
225 AND deleted_at IS NULL
226ORDER BY id FOR UPDATE
227 "#,
228 ids
229 )
230 .fetch_all(conn)
231 .await?;
232 Ok(res)
233}
234
235pub async fn get_by_user_id(
236 conn: &mut PgConnection,
237 user_id: Uuid,
238) -> ModelResult<Vec<CreditRegistration>> {
239 let res = sqlx::query_as!(
240 CreditRegistration,
241 r#"
242SELECT *
243FROM credit_registrations
244WHERE user_id = $1
245 AND deleted_at IS NULL
246ORDER BY created_at DESC
247 "#,
248 user_id
249 )
250 .fetch_all(conn)
251 .await?;
252 Ok(res)
253}
254
255pub async fn get_by_course_id(
256 conn: &mut PgConnection,
257 course_id: Uuid,
258) -> ModelResult<Vec<CreditRegistration>> {
259 let res = sqlx::query_as!(
260 CreditRegistration,
261 r#"
262SELECT *
263FROM credit_registrations
264WHERE course_id = $1
265 AND deleted_at IS NULL
266ORDER BY created_at DESC
267 "#,
268 course_id
269 )
270 .fetch_all(conn)
271 .await?;
272 Ok(res)
273}
274
275pub async fn exists_for_user_and_course(
280 conn: &mut PgConnection,
281 user_id: Uuid,
282 course_id: Uuid,
283) -> ModelResult<bool> {
284 let exists = sqlx::query_scalar!(
285 r#"
286SELECT EXISTS (
287 SELECT 1
288 FROM credit_registrations
289 WHERE user_id = $1
290 AND course_id = $2
291 AND deleted_at IS NULL
292 ) AS "exists!"
293 "#,
294 user_id,
295 course_id,
296 )
297 .fetch_one(conn)
298 .await?;
299 Ok(exists)
300}