headless_lms_server/controllers/main_frontend/credit_registration_admin/
mod.rs1mod account_linking;
10mod api_log;
11mod audit;
12mod courses;
13mod dashboard;
14mod enrolment_checks;
15mod errors;
16mod history;
17mod ledger;
18mod materialize;
19mod phases;
20mod reconciliation;
21mod student_numbers;
22
23use std::collections::HashMap;
24
25use headless_lms_base::config::ApplicationConfiguration;
26use headless_lms_models::credit_registration_account_linking_emails::{
27 self, CreditRegistrationAccountLinkingEmail,
28};
29use headless_lms_models::credit_registration_admin_actions::{self, ManualLinkingEmailSend};
30use headless_lms_models::credit_registration_events::{
31 self, CreditRegistrationEventKind, NewCreditRegistrationEvent,
32};
33use headless_lms_models::credit_registrations::AttentionRules;
34use headless_lms_models::email_deliveries::EmailSendStatusReport;
35use headless_lms_models::student_number_verification_tokens;
36use utoipa::{OpenApi, ToSchema};
37
38use crate::domain::authorization::AuthorizationToken;
39use crate::prelude::*;
40use secrecy::ExposeSecret;
41
42#[derive(OpenApi)]
43#[openapi(paths(
44 dashboard::get_credit_registration_overview,
45 dashboard::get_suotar_health,
46 dashboard::admin_pause_phase,
47 dashboard::admin_resume_phase,
48 dashboard::admin_run_phase_now,
49 ledger::list_credit_registrations_for_admin,
50 ledger::get_credit_registration_for_admin,
51 ledger::admin_transition_credit_registration,
52 ledger::admin_bulk_transition_credit_registrations,
53 ledger::admin_requeue_retryable_credit_registrations,
54 errors::get_credit_registration_thresholds,
55 errors::get_credit_registration_attention_items,
56 errors::get_credit_registration_errors_by_code,
57 errors::admin_dismiss_credit_registration_attention,
58 errors::admin_add_credit_registration_note,
59 phases::list_credit_registration_phases,
60 api_log::list_suotar_api_calls,
61 api_log::get_suotar_api_call,
62 courses::get_credit_registration_stats_by_course,
63 courses::admin_pause_course_module_credit_registration,
64 courses::admin_resume_course_module_credit_registration,
65 reconciliation::get_credit_registration_reconciliation,
66 audit::list_credit_registration_admin_actions,
67 audit::get_credit_registration_earlier_reasons,
68 history::get_credit_registration_pipeline_history,
69 enrolment_checks::get_credit_registration_enrolment_checks,
70 account_linking::get_account_linking_stats,
71 account_linking::list_account_linking_emails,
72 account_linking::admin_resend_account_linking_email,
73 account_linking::admin_resolve_student_number_for_linking,
74 account_linking::admin_manually_link_student_number,
75 account_linking::admin_request_enrolment_list_fetch,
76 account_linking::admin_dismiss_study_registry_conflict,
77 account_linking::get_credit_registration_linking_candidates,
78 account_linking::get_account_linking_courses,
79 account_linking::get_account_linking_unlinked_enrolees,
80 student_numbers::list_verified_student_numbers_for_admin,
81 student_numbers::admin_unlink_student_number,
82 materialize::admin_materialize_credit_registrations
83), components(schemas(
84 headless_lms_models::credit_registration_account_linking_emails::LinkingEmailListStatus
86)))]
87pub(crate) struct MainFrontendCreditRegistrationAdminApiDoc;
88
89async fn attention_rules(
91 conn: &mut PgConnection,
92 app_conf: &ApplicationConfiguration,
93) -> Result<AttentionRules, ControllerError> {
94 Ok(
95 headless_lms_credit_registration::attention::attention_rules(
96 conn,
97 app_conf.suotar_configuration.account_linking_since,
98 )
99 .await?,
100 )
101}
102
103async fn authorize_credit_registration_admin(
105 conn: &mut PgConnection,
106 user_id: Uuid,
107) -> Result<AuthorizationToken, ControllerError> {
108 authorize(
109 conn,
110 Act::AdministrateCreditRegistrations,
111 Some(user_id),
112 Res::GlobalPermissions,
113 )
114 .await
115 .map_err(Into::into)
116}
117
118fn required_reason(reason: &str) -> Result<&str, ControllerError> {
120 let trimmed = reason.trim();
121 if trimmed.is_empty() {
122 return Err(controller_err!(
123 BadRequest,
124 "A reason is required.".to_string()
125 ));
126 }
127 Ok(trimmed)
128}
129
130async fn insert_admin_action_event(
132 tx: &mut PgConnection,
133 id: Uuid,
134 actor_user_id: Uuid,
135 reason: &str,
136) -> Result<(), ControllerError> {
137 credit_registration_events::insert(
138 tx,
139 &NewCreditRegistrationEvent {
140 actor_user_id: Some(actor_user_id),
141 message: Some(reason.to_string()),
142 ..NewCreditRegistrationEvent::new(id, CreditRegistrationEventKind::AdminAction)
143 },
144 )
145 .await?;
146 Ok(())
147}
148
149#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
150pub struct AdminLinkingEmail {
151 pub id: Uuid,
152 pub course_id: Uuid,
153 pub student_number: String,
154 pub sisu_person_id: String,
155 pub emailed_to: String,
157 pub claimed_at: DateTime<Utc>,
158 pub send_status: EmailSendStatusReport,
159 pub token_claimed_by_user_id: Option<Uuid>,
160 pub token_used_at: Option<DateTime<Utc>>,
161 pub token_expires_at: Option<DateTime<Utc>>,
162}
163
164#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
166pub struct AdminManualLinkingEmailSend {
167 pub sent_at: DateTime<Utc>,
168 pub sent_by_user_id: Uuid,
169 pub sent_by_first_name: Option<String>,
170 pub sent_by_last_name: Option<String>,
171 pub sent_by_email: Option<String>,
172 pub emailed_to: Option<String>,
174}
175
176impl From<ManualLinkingEmailSend> for AdminManualLinkingEmailSend {
177 fn from(send: ManualLinkingEmailSend) -> Self {
178 Self {
179 sent_at: send.sent_at,
180 sent_by_user_id: send.sent_by_user_id,
181 sent_by_first_name: send.sent_by_first_name,
182 sent_by_last_name: send.sent_by_last_name,
183 sent_by_email: send.sent_by_email,
184 emailed_to: send.emailed_to,
185 }
186 }
187}
188
189async fn manual_linking_email_sends_by_registration(
192 conn: &mut PgConnection,
193 course_ids: &[Uuid],
194) -> Result<HashMap<Uuid, Vec<AdminManualLinkingEmailSend>>, ControllerError> {
195 let mut by_registration: HashMap<Uuid, Vec<AdminManualLinkingEmailSend>> = HashMap::new();
196 if course_ids.is_empty() {
197 return Ok(by_registration);
198 }
199 let sends =
200 credit_registration_admin_actions::get_manual_linking_email_sends(conn, course_ids).await?;
201 for send in sends {
202 if let Some(id) = send.credit_registration_id {
203 by_registration.entry(id).or_default().push(send.into());
204 }
205 }
206 Ok(by_registration)
207}
208
209async fn build_linking_emails(
212 conn: &mut PgConnection,
213 mails: Vec<CreditRegistrationAccountLinkingEmail>,
214) -> Result<Vec<AdminLinkingEmail>, ControllerError> {
215 let ids: Vec<Uuid> = mails.iter().map(|mail| mail.id).collect();
216 let reports =
217 credit_registration_account_linking_emails::get_send_status_reports(conn, &ids).await?;
218 let token_ids: Vec<Uuid> = mails
219 .iter()
220 .filter_map(|mail| mail.student_number_verification_token_id)
221 .collect();
222 let tokens = student_number_verification_tokens::get_by_ids(conn, &token_ids).await?;
223 Ok(mails
224 .into_iter()
225 .map(|mail| {
226 let token = mail
227 .student_number_verification_token_id
228 .and_then(|token_id| tokens.get(&token_id));
229 AdminLinkingEmail {
230 send_status: reports.get(&mail.id).cloned().unwrap_or_else(
231 credit_registration_account_linking_emails::not_handed_over_yet,
232 ),
233 id: mail.id,
234 course_id: mail.course_id,
235 student_number: mail.student_number.expose_secret().to_owned(),
236 sisu_person_id: mail.sisu_person_id.expose_secret().to_owned(),
237 emailed_to: mail.emailed_to.expose_secret().to_owned(),
238 claimed_at: mail.sent_at,
239 token_claimed_by_user_id: token.and_then(|row| row.claimed_by_user_id),
240 token_used_at: token.and_then(|row| row.used_at),
241 token_expires_at: token.map(|row| row.expires_at),
242 }
243 })
244 .collect())
245}
246
247pub fn _add_routes(cfg: &mut ServiceConfig) {
248 dashboard::_add_routes(cfg);
249 ledger::_add_routes(cfg);
250 errors::_add_routes(cfg);
251 phases::_add_routes(cfg);
252 api_log::_add_routes(cfg);
253 courses::_add_routes(cfg);
254 reconciliation::_add_routes(cfg);
255 audit::_add_routes(cfg);
256 history::_add_routes(cfg);
257 enrolment_checks::_add_routes(cfg);
258 account_linking::_add_routes(cfg);
259 student_numbers::_add_routes(cfg);
260 materialize::_add_routes(cfg);
261}