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 headless_lms_models::credit_registration_account_linking_emails::{
24 self, CreditRegistrationAccountLinkingEmail,
25};
26use headless_lms_models::email_deliveries::EmailSendStatusReport;
27use headless_lms_models::student_number_verification_tokens;
28use utoipa::{OpenApi, ToSchema};
29
30use crate::domain::authorization::AuthorizationToken;
31use crate::prelude::*;
32use secrecy::ExposeSecret;
33
34#[derive(OpenApi)]
35#[openapi(paths(
36 dashboard::get_credit_registration_overview,
37 dashboard::get_suotar_health,
38 dashboard::admin_pause_phase,
39 dashboard::admin_resume_phase,
40 dashboard::admin_run_phase_now,
41 ledger::list_credit_registrations_for_admin,
42 ledger::get_credit_registration_for_admin,
43 ledger::admin_transition_credit_registration,
44 ledger::admin_bulk_transition_credit_registrations,
45 ledger::admin_requeue_retryable_credit_registrations,
46 errors::get_credit_registration_thresholds,
47 errors::get_credit_registration_attention_items,
48 errors::get_credit_registration_errors_by_code,
49 phases::list_credit_registration_phases,
50 api_log::list_suotar_api_calls,
51 api_log::get_suotar_api_call,
52 courses::get_credit_registration_stats_by_course,
53 courses::admin_pause_course_module_credit_registration,
54 courses::admin_resume_course_module_credit_registration,
55 reconciliation::get_credit_registration_reconciliation,
56 audit::list_credit_registration_admin_actions,
57 history::get_credit_registration_pipeline_history,
58 enrolment_checks::get_credit_registration_enrolment_checks,
59 account_linking::get_account_linking_stats,
60 account_linking::admin_resend_account_linking_email,
61 account_linking::admin_resolve_student_number_for_linking,
62 account_linking::admin_manually_link_student_number,
63 student_numbers::list_verified_student_numbers_for_admin,
64 student_numbers::admin_unlink_student_number,
65 materialize::admin_materialize_credit_registrations
66))]
67pub(crate) struct MainFrontendCreditRegistrationAdminApiDoc;
68
69const ATTENTION_TOO_MANY_ATTEMPTS: i32 = 5;
73
74async fn authorize_credit_registration_admin(
76 conn: &mut PgConnection,
77 user_id: Uuid,
78) -> Result<AuthorizationToken, ControllerError> {
79 authorize(
80 conn,
81 Act::AdministrateCreditRegistrations,
82 Some(user_id),
83 Res::GlobalPermissions,
84 )
85 .await
86 .map_err(Into::into)
87}
88
89fn required_reason(reason: &str) -> Result<&str, ControllerError> {
91 let trimmed = reason.trim();
92 if trimmed.is_empty() {
93 return Err(controller_err!(
94 BadRequest,
95 "A reason is required.".to_string()
96 ));
97 }
98 Ok(trimmed)
99}
100
101#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
102pub struct AdminLinkingEmail {
103 pub id: Uuid,
104 pub course_id: Uuid,
105 pub student_number: String,
106 pub sisu_person_id: String,
107 pub emailed_to: String,
109 pub claimed_at: DateTime<Utc>,
110 pub send_status: EmailSendStatusReport,
111 pub token_claimed_by_user_id: Option<Uuid>,
112 pub token_used_at: Option<DateTime<Utc>>,
113 pub token_expires_at: Option<DateTime<Utc>>,
114}
115
116async fn build_linking_emails(
119 conn: &mut PgConnection,
120 mails: Vec<CreditRegistrationAccountLinkingEmail>,
121) -> Result<Vec<AdminLinkingEmail>, ControllerError> {
122 let ids: Vec<Uuid> = mails.iter().map(|mail| mail.id).collect();
123 let reports =
124 credit_registration_account_linking_emails::get_send_status_reports(conn, &ids).await?;
125 let token_ids: Vec<Uuid> = mails
126 .iter()
127 .filter_map(|mail| mail.student_number_verification_token_id)
128 .collect();
129 let tokens = student_number_verification_tokens::get_by_ids(conn, &token_ids).await?;
130 Ok(mails
131 .into_iter()
132 .map(|mail| {
133 let token = mail
134 .student_number_verification_token_id
135 .and_then(|token_id| tokens.get(&token_id));
136 AdminLinkingEmail {
137 send_status: reports.get(&mail.id).cloned().unwrap_or_else(
138 credit_registration_account_linking_emails::not_handed_over_yet,
139 ),
140 id: mail.id,
141 course_id: mail.course_id,
142 student_number: mail.student_number.expose_secret().to_owned(),
143 sisu_person_id: mail.sisu_person_id.expose_secret().to_owned(),
144 emailed_to: mail.emailed_to.expose_secret().to_owned(),
145 claimed_at: mail.sent_at,
146 token_claimed_by_user_id: token.and_then(|row| row.claimed_by_user_id),
147 token_used_at: token.and_then(|row| row.used_at),
148 token_expires_at: token.map(|row| row.expires_at),
149 }
150 })
151 .collect())
152}
153
154pub fn _add_routes(cfg: &mut ServiceConfig) {
155 dashboard::_add_routes(cfg);
156 ledger::_add_routes(cfg);
157 errors::_add_routes(cfg);
158 phases::_add_routes(cfg);
159 api_log::_add_routes(cfg);
160 courses::_add_routes(cfg);
161 reconciliation::_add_routes(cfg);
162 audit::_add_routes(cfg);
163 history::_add_routes(cfg);
164 enrolment_checks::_add_routes(cfg);
165 account_linking::_add_routes(cfg);
166 student_numbers::_add_routes(cfg);
167 materialize::_add_routes(cfg);
168}