Skip to main content

headless_lms_models/
credit_registration_enrolment_routes.rs

1//! What a student said about enrolling, on the new registration page: the route, the questions
2//! that steer students who would struggle to enrol towards a certificate, and the certificate
3//! choice.
4//!
5//! Advisory throughout: the pipeline never reads these rows. They decide which enrolment
6//! instructions the page shows and how far its step list has come, and the study registry is what
7//! actually settles whether an enrolment exists.
8
9use utoipa::ToSchema;
10
11use crate::prelude::*;
12
13/// Which university relationship a student picked, which decides only where they are told to enrol.
14#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
15#[sqlx(
16    type_name = "credit_registration_enrolment_route",
17    rename_all = "snake_case"
18)]
19#[serde(rename_all = "snake_case")]
20pub enum CreditRegistrationEnrolmentRoute {
21    UniversityOfHelsinki,
22    OpenUniversity,
23}
24
25/// Whether a student said they need a certificate or credits in the study registry.
26#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
27#[sqlx(type_name = "credit_registration_need", rename_all = "snake_case")]
28#[serde(rename_all = "snake_case")]
29pub enum CreditRegistrationNeed {
30    Certificate,
31    Credits,
32}
33
34/// How a student without a Finnish personal identity code said they can identify themselves for
35/// enrolling.
36#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
37#[sqlx(
38    type_name = "credit_registration_identification",
39    rename_all = "snake_case"
40)]
41#[serde(rename_all = "snake_case")]
42pub enum CreditRegistrationIdentification {
43    Eidas,
44    OtherSuomiFi,
45    #[sqlx(rename = "none")]
46    #[serde(rename = "none")]
47    NoSuomiFi,
48}
49
50/// The student's answers to the enrolment questions, written together and replaced as a whole.
51///
52/// The detour answers are kept whichever way the answers above them point, so a student who
53/// changes an earlier answer and changes it back finds their place.
54#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
55pub struct EnrolmentAnswers {
56    pub route: CreditRegistrationEnrolmentRoute,
57    pub has_finnish_personal_identity_code: Option<bool>,
58    pub need: Option<CreditRegistrationNeed>,
59    pub identification: Option<CreditRegistrationIdentification>,
60    /// What they need when asked again after saying they cannot identify themselves.
61    pub need_after_reconsider: Option<CreditRegistrationNeed>,
62}
63
64#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
65pub struct EnrolmentRouteAnswer {
66    pub course_module_completion_id: Uuid,
67    pub route: CreditRegistrationEnrolmentRoute,
68    pub has_finnish_personal_identity_code: Option<bool>,
69    pub need: Option<CreditRegistrationNeed>,
70    pub identification: Option<CreditRegistrationIdentification>,
71    pub need_after_reconsider: Option<CreditRegistrationNeed>,
72    /// When the student pressed Done. `None` until they do, and cleared again if they take it back.
73    pub enrolment_confirmed_at: Option<DateTime<Utc>>,
74    /// When the student took the certificate instead of registering credits. `None` until they
75    /// do, and cleared again if they take it back.
76    pub certificate_chosen_at: Option<DateTime<Utc>>,
77}
78
79pub async fn get_by_completion_id(
80    conn: &mut PgConnection,
81    course_module_completion_id: Uuid,
82) -> ModelResult<Option<EnrolmentRouteAnswer>> {
83    let res = sqlx::query_as!(
84        EnrolmentRouteAnswer,
85        r#"
86SELECT course_module_completion_id,
87  route AS "route: _",
88  has_finnish_personal_identity_code,
89  need AS "need: _",
90  identification AS "identification: _",
91  need_after_reconsider AS "need_after_reconsider: _",
92  enrolment_confirmed_at,
93  certificate_chosen_at
94FROM credit_registration_enrolment_routes
95WHERE course_module_completion_id = $1
96  AND deleted_at IS NULL
97        "#,
98        course_module_completion_id,
99    )
100    .fetch_optional(conn)
101    .await?;
102    Ok(res)
103}
104
105/// Records the student's answers, replacing any earlier ones.
106///
107/// Changing the route clears the Done that went with it: the confirmation was about enrolling the
108/// way the old route described, and it says nothing about the new one.
109pub async fn set_answers(
110    conn: &mut PgConnection,
111    course_module_completion_id: Uuid,
112    user_id: Uuid,
113    answers: &EnrolmentAnswers,
114) -> ModelResult<EnrolmentRouteAnswer> {
115    let res = sqlx::query_as!(
116        EnrolmentRouteAnswer,
117        r#"
118INSERT INTO credit_registration_enrolment_routes (
119    course_module_completion_id,
120    user_id,
121    route,
122    has_finnish_personal_identity_code,
123    need,
124    identification,
125    need_after_reconsider
126  )
127VALUES ($1, $2, $3, $4, $5, $6, $7)
128ON CONFLICT (course_module_completion_id, deleted_at) DO
129UPDATE
130SET route = EXCLUDED.route,
131  has_finnish_personal_identity_code = EXCLUDED.has_finnish_personal_identity_code,
132  need = EXCLUDED.need,
133  identification = EXCLUDED.identification,
134  need_after_reconsider = EXCLUDED.need_after_reconsider,
135  enrolment_confirmed_at = CASE
136    WHEN credit_registration_enrolment_routes.route = EXCLUDED.route THEN credit_registration_enrolment_routes.enrolment_confirmed_at
137    ELSE NULL
138  END
139RETURNING course_module_completion_id,
140  route AS "route: _",
141  has_finnish_personal_identity_code,
142  need AS "need: _",
143  identification AS "identification: _",
144  need_after_reconsider AS "need_after_reconsider: _",
145  enrolment_confirmed_at,
146  certificate_chosen_at
147        "#,
148        course_module_completion_id,
149        user_id,
150        answers.route as CreditRegistrationEnrolmentRoute,
151        answers.has_finnish_personal_identity_code,
152        answers.need as Option<CreditRegistrationNeed>,
153        answers.identification as Option<CreditRegistrationIdentification>,
154        answers.need_after_reconsider as Option<CreditRegistrationNeed>,
155    )
156    .fetch_one(conn)
157    .await?;
158    Ok(res)
159}
160
161/// Sets or clears the Done the student pressed to say they had enrolled.
162///
163/// Errors with not-found when the student has not answered yet.
164pub async fn set_enrolment_confirmed(
165    conn: &mut PgConnection,
166    course_module_completion_id: Uuid,
167    confirmed: bool,
168) -> ModelResult<EnrolmentRouteAnswer> {
169    let res = sqlx::query_as!(
170        EnrolmentRouteAnswer,
171        r#"
172UPDATE credit_registration_enrolment_routes
173SET enrolment_confirmed_at = CASE
174    WHEN $2 THEN COALESCE(enrolment_confirmed_at, now())
175    ELSE NULL
176  END
177WHERE course_module_completion_id = $1
178  AND deleted_at IS NULL
179RETURNING course_module_completion_id,
180  route AS "route: _",
181  has_finnish_personal_identity_code,
182  need AS "need: _",
183  identification AS "identification: _",
184  need_after_reconsider AS "need_after_reconsider: _",
185  enrolment_confirmed_at,
186  certificate_chosen_at
187        "#,
188        course_module_completion_id,
189        confirmed,
190    )
191    .fetch_one(conn)
192    .await?;
193    Ok(res)
194}
195
196/// Sets or clears the student's choice of the certificate over registering credits.
197///
198/// Errors with not-found when the student has not answered yet.
199pub async fn set_certificate_chosen(
200    conn: &mut PgConnection,
201    course_module_completion_id: Uuid,
202    chosen: bool,
203) -> ModelResult<EnrolmentRouteAnswer> {
204    let res = sqlx::query_as!(
205        EnrolmentRouteAnswer,
206        r#"
207UPDATE credit_registration_enrolment_routes
208SET certificate_chosen_at = CASE
209    WHEN $2 THEN COALESCE(certificate_chosen_at, now())
210    ELSE NULL
211  END
212WHERE course_module_completion_id = $1
213  AND deleted_at IS NULL
214RETURNING course_module_completion_id,
215  route AS "route: _",
216  has_finnish_personal_identity_code,
217  need AS "need: _",
218  identification AS "identification: _",
219  need_after_reconsider AS "need_after_reconsider: _",
220  enrolment_confirmed_at,
221  certificate_chosen_at
222        "#,
223        course_module_completion_id,
224        chosen,
225    )
226    .fetch_one(conn)
227    .await?;
228    Ok(res)
229}
230
231/// Erases every answer the user gave. Called from account deletion, which is why this is a hard
232/// delete: the answers are personal circumstances the student told us.
233pub async fn delete_all_by_user_id(conn: &mut PgConnection, user_id: Uuid) -> ModelResult<()> {
234    sqlx::query!(
235        "
236DELETE FROM credit_registration_enrolment_routes
237WHERE user_id = $1
238        ",
239        user_id,
240    )
241    .execute(conn)
242    .await?;
243    Ok(())
244}