Skip to main content

headless_lms_models/
verified_student_numbers.rs

1use utoipa::ToSchema;
2
3use crate::prelude::*;
4
5/// How a student number was proven to belong to an account.
6#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Type, ToSchema)]
7#[sqlx(
8    type_name = "student_number_verification_method",
9    rename_all = "snake_case"
10)]
11#[serde(rename_all = "snake_case")]
12pub enum StudentNumberVerificationMethod {
13    EmailedLink,
14    EmailMatchFastTrack,
15    AdminManual,
16}
17
18#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
19pub struct VerifiedStudentNumber {
20    pub id: Uuid,
21    pub created_at: DateTime<Utc>,
22    pub updated_at: DateTime<Utc>,
23    pub deleted_at: Option<DateTime<Utc>>,
24    pub user_id: Uuid,
25    pub student_number: String,
26    pub sisu_person_id: String,
27    pub first_names: Option<String>,
28    pub last_name: Option<String>,
29    pub verified_at: DateTime<Utc>,
30    pub verified_via: StudentNumberVerificationMethod,
31    pub verified_via_email: Option<String>,
32    pub verified_via_email_match_field: Option<String>,
33    pub account_email_verified_at: Option<DateTime<Utc>>,
34    pub linked_by_user_id: Option<Uuid>,
35    pub link_reason: Option<String>,
36    pub verified_from_course_id: Option<Uuid>,
37}
38
39#[derive(Debug, Clone, PartialEq)]
40pub struct NewVerifiedStudentNumber {
41    pub user_id: Uuid,
42    pub student_number: String,
43    pub sisu_person_id: String,
44    pub first_names: Option<String>,
45    pub last_name: Option<String>,
46    pub verified_via: StudentNumberVerificationMethod,
47    /// The Sisu-held address the proof rests on. Must be `None` exactly for `AdminManual`.
48    pub verified_via_email: Option<String>,
49    pub verified_via_email_match_field: Option<String>,
50    pub account_email_verified_at: Option<DateTime<Utc>>,
51    pub linked_by_user_id: Option<Uuid>,
52    pub link_reason: Option<String>,
53    pub verified_from_course_id: Option<Uuid>,
54}
55
56pub async fn insert(
57    conn: &mut PgConnection,
58    pkey_policy: PKeyPolicy<Uuid>,
59    new: &NewVerifiedStudentNumber,
60) -> ModelResult<Uuid> {
61    let res = sqlx::query!(
62        r#"
63INSERT INTO verified_student_numbers (
64    id,
65    user_id,
66    student_number,
67    sisu_person_id,
68    first_names,
69    last_name,
70    verified_via,
71    verified_via_email,
72    verified_via_email_match_field,
73    account_email_verified_at,
74    linked_by_user_id,
75    link_reason,
76    verified_from_course_id
77  )
78VALUES (
79    $1,
80    $2,
81    $3,
82    $4,
83    $5,
84    $6,
85    $7,
86    $8,
87    $9,
88    $10,
89    $11,
90    $12,
91    $13
92  )
93RETURNING id
94        "#,
95        pkey_policy.into_uuid(),
96        new.user_id,
97        new.student_number,
98        new.sisu_person_id,
99        new.first_names,
100        new.last_name,
101        new.verified_via as StudentNumberVerificationMethod,
102        new.verified_via_email,
103        new.verified_via_email_match_field,
104        new.account_email_verified_at,
105        new.linked_by_user_id,
106        new.link_reason,
107        new.verified_from_course_id,
108    )
109    .fetch_one(conn)
110    .await?;
111    Ok(res.id)
112}
113
114pub async fn get_by_id(conn: &mut PgConnection, id: Uuid) -> ModelResult<VerifiedStudentNumber> {
115    let res = sqlx::query_as!(
116        VerifiedStudentNumber,
117        r#"
118SELECT *
119FROM verified_student_numbers
120WHERE id = $1
121  AND deleted_at IS NULL
122        "#,
123        id
124    )
125    .fetch_one(conn)
126    .await?;
127    Ok(res)
128}
129
130/// The account's live link, if it has one. At most one exists by partial unique index.
131pub async fn get_by_user_id(
132    conn: &mut PgConnection,
133    user_id: Uuid,
134) -> ModelResult<Option<VerifiedStudentNumber>> {
135    let res = sqlx::query_as!(
136        VerifiedStudentNumber,
137        r#"
138SELECT *
139FROM verified_student_numbers
140WHERE user_id = $1
141  AND deleted_at IS NULL
142        "#,
143        user_id
144    )
145    .fetch_optional(conn)
146    .await?;
147    Ok(res)
148}
149
150pub async fn get_by_student_number(
151    conn: &mut PgConnection,
152    student_number: &str,
153) -> ModelResult<Option<VerifiedStudentNumber>> {
154    let res = sqlx::query_as!(
155        VerifiedStudentNumber,
156        r#"
157SELECT *
158FROM verified_student_numbers
159WHERE student_number = $1
160  AND deleted_at IS NULL
161        "#,
162        student_number
163    )
164    .fetch_optional(conn)
165    .await?;
166    Ok(res)
167}
168
169pub async fn get_by_user_ids(
170    conn: &mut PgConnection,
171    user_ids: &[Uuid],
172) -> ModelResult<Vec<VerifiedStudentNumber>> {
173    let res = sqlx::query_as!(
174        VerifiedStudentNumber,
175        r#"
176SELECT *
177FROM verified_student_numbers
178WHERE user_id = ANY($1::uuid [])
179  AND deleted_at IS NULL
180        "#,
181        user_ids
182    )
183    .fetch_all(conn)
184    .await?;
185    Ok(res)
186}
187
188pub async fn get_by_student_numbers(
189    conn: &mut PgConnection,
190    student_numbers: &[String],
191) -> ModelResult<Vec<VerifiedStudentNumber>> {
192    let res = sqlx::query_as!(
193        VerifiedStudentNumber,
194        r#"
195SELECT *
196FROM verified_student_numbers
197WHERE student_number = ANY($1::varchar [])
198  AND deleted_at IS NULL
199        "#,
200        student_numbers
201    )
202    .fetch_all(conn)
203    .await?;
204    Ok(res)
205}
206
207/// Unlinks by soft-delete; relinking inserts a new row, keeping the old number for audit.
208pub async fn soft_delete(conn: &mut PgConnection, id: Uuid) -> ModelResult<()> {
209    sqlx::query!(
210        r#"
211UPDATE verified_student_numbers
212SET deleted_at = now()
213WHERE id = $1
214  AND deleted_at IS NULL
215        "#,
216        id
217    )
218    .execute(conn)
219    .await?;
220    Ok(())
221}