Skip to main content

headless_lms_server/controllers/main_frontend/credit_registration_admin/
materialize.rs

1//! Manually running the two database-only steps that turn eligible completions into ledger rows.
2
3use headless_lms_models::credit_registration_admin_actions::{
4    CreditRegistrationAdminAction, CreditRegistrationAdminActionTarget, GLOBAL_ADMIN_ROLE,
5    NewCreditRegistrationAdminAction,
6};
7use headless_lms_models::credit_registrations;
8use headless_lms_models::library::credit_registration::materialize::{
9    MATERIALIZE_LIMIT, ensure_registration_rows_for_eligible_completions,
10};
11use headless_lms_models::library::credit_registration::preconditions::{
12    PRECONDITIONS_LIMIT, recompute_preconditions,
13};
14use utoipa::ToSchema;
15
16use crate::domain::credit_registration_phases::CreditRegistrationPhase;
17use crate::prelude::*;
18
19use super::authorize_credit_registration_admin;
20
21#[derive(Debug, Deserialize, ToSchema)]
22pub struct AdminMaterializePayload {
23    pub reason: Option<String>,
24}
25
26#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, ToSchema)]
27pub struct AdminMaterializeResult {
28    pub created_registration_count: i64,
29    pub moved_registration_count: i64,
30}
31
32/**
33POST `/api/v0/main-frontend/credit-registration-admin/materialize` - Creates ledger rows for eligible
34completions and recomputes preconditions, now.
35
36Runs the two database-only steps directly rather than through the phase dispatcher, because the
37phase-state row describes the worker loops: an admin pressing a button must not make a dead worker look
38alive.
39*/
40#[instrument(skip(pool, payload))]
41#[utoipa::path(
42    post,
43    path = "/materialize",
44    operation_id = "adminMaterializeCreditRegistrations",
45    tag = "credit-registration-admin",
46    request_body = AdminMaterializePayload,
47    responses(
48        (status = 200, description = "How many rows were created and moved", body = AdminMaterializeResult)
49    )
50)]
51pub async fn admin_materialize_credit_registrations(
52    user: AuthUser,
53    pool: web::Data<PgPool>,
54    payload: web::Json<AdminMaterializePayload>,
55) -> ControllerResult<web::Json<AdminMaterializeResult>> {
56    let mut conn = pool.acquire().await?;
57    let token = authorize_credit_registration_admin(&mut conn, user.id).await?;
58
59    let mut tx = conn.begin().await?;
60    let scope = credit_registrations::RegistrationScope::default();
61    let created_registration_count =
62        ensure_registration_rows_for_eligible_completions(&mut tx, &scope, MATERIALIZE_LIMIT)
63            .await?;
64    let moved_registration_count =
65        recompute_preconditions(&mut tx, &scope, PRECONDITIONS_LIMIT).await?;
66    models::credit_registration_admin_actions::record(
67        &mut tx,
68        &NewCreditRegistrationAdminAction {
69            target_phase: Some(CreditRegistrationPhase::Materialize.as_str().to_string()),
70            reason: payload.reason.clone(),
71            details: Some(serde_json::json!({
72                "created_registration_count": created_registration_count,
73                "moved_registration_count": moved_registration_count,
74            })),
75            affected_row_count: Some(
76                i32::try_from(created_registration_count + moved_registration_count)
77                    .unwrap_or(i32::MAX),
78            ),
79            ..NewCreditRegistrationAdminAction::new(
80                CreditRegistrationAdminAction::RequeueBatch,
81                CreditRegistrationAdminActionTarget::Phase,
82                user.id,
83                GLOBAL_ADMIN_ROLE,
84            )
85        },
86    )
87    .await?;
88    tx.commit().await?;
89
90    token.authorized_ok(web::Json(AdminMaterializeResult {
91        created_registration_count,
92        moved_registration_count,
93    }))
94}
95
96pub fn _add_routes(cfg: &mut ServiceConfig) {
97    cfg.route(
98        "/materialize",
99        web::post().to(admin_materialize_credit_registrations),
100    );
101}