headless_lms_server/controllers/main_frontend/credit_registration_admin/
mod.rs1mod account_linking;
10mod api_log;
11mod audit;
12mod courses;
13mod dashboard;
14mod errors;
15mod history;
16mod ledger;
17mod materialize;
18mod phases;
19mod reconciliation;
20mod student_numbers;
21
22use headless_lms_models::credit_registration_account_linking_emails::{
23 self, CreditRegistrationAccountLinkingEmail,
24};
25use headless_lms_models::email_deliveries::EmailSendStatusReport;
26use headless_lms_models::student_number_verification_tokens;
27use utoipa::{OpenApi, ToSchema};
28
29use crate::domain::authorization::AuthorizationToken;
30use crate::prelude::*;
31
32#[derive(OpenApi)]
33#[openapi(paths(
34 dashboard::get_credit_registration_overview,
35 dashboard::get_suotar_health,
36 dashboard::admin_pause_phase,
37 dashboard::admin_resume_phase,
38 dashboard::admin_run_phase_now,
39 ledger::list_credit_registrations_for_admin,
40 ledger::get_credit_registration_for_admin,
41 ledger::admin_transition_credit_registration,
42 ledger::admin_bulk_transition_credit_registrations,
43 ledger::admin_requeue_retryable_credit_registrations,
44 errors::get_credit_registration_thresholds,
45 errors::get_credit_registration_attention_items,
46 errors::get_credit_registration_errors_by_code,
47 phases::list_credit_registration_phases,
48 api_log::list_suotar_api_calls,
49 api_log::get_suotar_api_call,
50 courses::get_credit_registration_stats_by_course,
51 courses::admin_pause_course_module_credit_registration,
52 courses::admin_resume_course_module_credit_registration,
53 reconciliation::get_credit_registration_reconciliation,
54 audit::list_credit_registration_admin_actions,
55 history::get_credit_registration_pipeline_history,
56 account_linking::get_account_linking_stats,
57 account_linking::admin_resend_account_linking_email,
58 account_linking::admin_resolve_student_number_for_linking,
59 account_linking::admin_manually_link_student_number,
60 student_numbers::list_verified_student_numbers_for_admin,
61 student_numbers::admin_unlink_student_number,
62 materialize::admin_materialize_credit_registrations
63))]
64pub(crate) struct MainFrontendCreditRegistrationAdminApiDoc;
65
66async fn authorize_credit_registration_admin(
68 conn: &mut PgConnection,
69 user_id: Uuid,
70) -> Result<AuthorizationToken, ControllerError> {
71 authorize(
72 conn,
73 Act::Administrate,
74 Some(user_id),
75 Res::GlobalPermissions,
76 )
77 .await
78 .map_err(Into::into)
79}
80
81fn required_reason(reason: &str) -> Result<&str, ControllerError> {
83 let trimmed = reason.trim();
84 if trimmed.is_empty() {
85 return Err(controller_err!(
86 BadRequest,
87 "A reason is required.".to_string()
88 ));
89 }
90 Ok(trimmed)
91}
92
93fn one_or_many<'de, D, T>(deserializer: D) -> Result<Option<Vec<T>>, D::Error>
96where
97 D: serde::Deserializer<'de>,
98 T: Deserialize<'de>,
99{
100 #[derive(Deserialize)]
101 #[serde(untagged)]
102 enum OneOrMany<T> {
103 One(T),
104 Many(Vec<T>),
105 }
106 Ok(
107 Option::<OneOrMany<T>>::deserialize(deserializer)?.map(|repr| match repr {
108 OneOrMany::One(value) => vec![value],
109 OneOrMany::Many(values) => values,
110 }),
111 )
112}
113
114#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
115pub struct AdminLinkingEmail {
116 pub id: Uuid,
117 pub course_id: Uuid,
118 pub student_number: String,
119 pub sisu_person_id: String,
120 pub emailed_to: String,
122 pub claimed_at: DateTime<Utc>,
123 pub send_status: EmailSendStatusReport,
124 pub token_claimed_by_user_id: Option<Uuid>,
125 pub token_used_at: Option<DateTime<Utc>>,
126 pub token_expires_at: Option<DateTime<Utc>>,
127}
128
129async fn build_linking_emails(
132 conn: &mut PgConnection,
133 mails: Vec<CreditRegistrationAccountLinkingEmail>,
134) -> Result<Vec<AdminLinkingEmail>, ControllerError> {
135 let ids: Vec<Uuid> = mails.iter().map(|mail| mail.id).collect();
136 let reports =
137 credit_registration_account_linking_emails::get_send_status_reports(conn, &ids).await?;
138 let token_ids: Vec<Uuid> = mails
139 .iter()
140 .filter_map(|mail| mail.student_number_verification_token_id)
141 .collect();
142 let tokens = student_number_verification_tokens::get_by_ids(conn, &token_ids).await?;
143 Ok(mails
144 .into_iter()
145 .map(|mail| {
146 let token = mail
147 .student_number_verification_token_id
148 .and_then(|token_id| tokens.get(&token_id));
149 AdminLinkingEmail {
150 send_status: reports.get(&mail.id).cloned().unwrap_or_else(
151 credit_registration_account_linking_emails::not_handed_over_yet,
152 ),
153 id: mail.id,
154 course_id: mail.course_id,
155 student_number: mail.student_number,
156 sisu_person_id: mail.sisu_person_id,
157 emailed_to: mail.emailed_to,
158 claimed_at: mail.sent_at,
159 token_claimed_by_user_id: token.and_then(|row| row.claimed_by_user_id),
160 token_used_at: token.and_then(|row| row.used_at),
161 token_expires_at: token.map(|row| row.expires_at),
162 }
163 })
164 .collect())
165}
166
167pub fn _add_routes(cfg: &mut ServiceConfig) {
168 dashboard::_add_routes(cfg);
169 ledger::_add_routes(cfg);
170 errors::_add_routes(cfg);
171 phases::_add_routes(cfg);
172 api_log::_add_routes(cfg);
173 courses::_add_routes(cfg);
174 reconciliation::_add_routes(cfg);
175 audit::_add_routes(cfg);
176 history::_add_routes(cfg);
177 account_linking::_add_routes(cfg);
178 student_numbers::_add_routes(cfg);
179 materialize::_add_routes(cfg);
180}