api/routes/modules/assignments/
get.rs

1//! Assignment routes and response models.
2//!
3//! Provides endpoints and data structures for managing assignments within modules:
4//!
5//! - `GET /api/modules/{module_id}/assignments/{assignment_id}`  
6//!   Retrieve a specific assignment along with its associated files.
7//!
8//! - `GET /api/modules/{module_id}/assignments`  
9//!   Retrieve a paginated and optionally filtered list of assignments.
10//!
11//! - `GET /api/modules/{module_id}/assignments/{assignment_id}/stats`  
12//!   Retrieve submission statistics for a specific assignment.
13//!
14//! - `GET /api/modules/{module_id}/assignments/{assignment_id}/readiness`  
15//!   Retrieve a readiness report for a specific assignment, checking whether all required components are present.
16//!
17//! **Models:**  
18//! - `AssignmentFileResponse`: Assignment data plus associated files.  
19//! - `FilterReq` / `FilterResponse`: Query and response for paginated assignment lists.  
20//! - `StatResponse` / `PerStudentSubmission`: Assignment submission statistics.  
21//! - `AssignmentReadiness`: Detailed readiness report for an assignment.
22//!
23//! All endpoints use `AppState` for database access and return JSON-wrapped `ApiResponse`.
24
25use axum::{
26    extract::{State, Path, Query},
27    http::StatusCode,
28    response::{IntoResponse, Json},
29};
30use chrono::{DateTime, Utc};
31use serde::{Deserialize, Serialize};
32use sea_orm::{
33    ColumnTrait, Condition, EntityTrait, PaginatorTrait, QueryFilter,
34    QueryOrder, sea_query::Expr,
35};
36use util::state::AppState;
37use crate::response::ApiResponse;
38use crate::routes::modules::assignments::common::{File, AssignmentResponse};
39use db::{
40    models::{
41        assignment::{
42            self, AssignmentType, Column as AssignmentColumn, Entity as AssignmentEntity, Model as AssignmentModel
43        }, 
44        assignment_file,
45        assignment_submission, 
46        user
47    },
48};
49
50#[derive(Debug, Serialize, Deserialize)]
51pub struct AssignmentFileResponse {
52    pub assignment: AssignmentResponse,
53    pub files: Vec<File>,
54}
55
56impl From<AssignmentModel> for AssignmentFileResponse {
57    fn from(assignment: AssignmentModel) -> Self {
58        Self {
59            assignment: AssignmentResponse {
60                id: assignment.id,
61                module_id: assignment.module_id as i64,
62                name: assignment.name,
63                description: assignment.description,
64                status: assignment.status.to_string(),
65                assignment_type: assignment.assignment_type.to_string(),
66                available_from: assignment.available_from.to_rfc3339(),
67                due_date: assignment.due_date.to_rfc3339(),
68                created_at: assignment.created_at.to_rfc3339(),
69                updated_at: assignment.updated_at.to_rfc3339(),
70            },
71            files: Vec::new(),
72        }
73    }
74}
75
76/// GET /api/modules/{module_id}/assignments/{assignment_id}
77///
78/// Retrieve a specific assignment along with its associated files. Accessible to users assigned to the module.
79///
80/// ### Path Parameters
81/// - `module_id` (i64): The ID of the module containing the assignment
82/// - `assignment_id` (i64): The ID of the assignment to retrieve
83///
84/// ### Responses
85///
86/// - `200 OK`
87/// ```json
88/// {
89///   "success": true,
90///   "message": "Assignment retrieved successfully",
91///   "data": {
92///     "assignment": {
93///       "id": 123,
94///       "module_id": 456,
95///       "name": "Assignment 1",
96///       "description": "This is a sample assignment",
97///       "assignment_type": "Assignment",
98///       "available_from": "2024-01-01T00:00:00Z",
99///       "due_date": "2024-01-31T23:59:59Z",
100///       "created_at": "2024-01-01T00:00:00Z",
101///       "updated_at": "2024-01-01T00:00:00Z"
102///     },
103///     "files": [
104///       {
105///         "id": "789",
106///         "filename": "assignment.pdf",
107///         "path": "module_456/assignment_123/assignment.pdf",
108///         "created_at": "2024-01-01T00:00:00Z",
109///         "updated_at": "2024-01-01T00:00:00Z"
110///       }
111///     ]
112///   }
113/// }
114/// ```
115///
116/// - `404 Not Found`
117/// ```json
118/// {
119///   "success": false,
120///   "message": "Assignment not found"
121/// }
122/// ```
123///
124/// - `500 Internal Server Error`
125/// ```json
126/// {
127///   "success": false,
128///   "message": "Failed to retrieve files: <error details>" // or "An error occurred: <error details>"
129/// }
130/// ```
131pub async fn get_assignment(
132    State(app_state): State<AppState>,
133    Path((module_id, assignment_id)): Path<(i64, i64)>,
134) -> impl IntoResponse {
135    let db = app_state.db();
136
137    let assignment_res = assignment::Entity::find()
138        .filter(assignment::Column::Id.eq(assignment_id as i32))
139        .filter(assignment::Column::ModuleId.eq(module_id as i32))
140        .one(db)
141        .await;
142
143    match assignment_res {
144        Ok(Some(a)) => {
145            let files_res = assignment_file::Entity::find()
146                .filter(assignment_file::Column::AssignmentId.eq(a.id))
147                .all(db)
148                .await;
149
150            match files_res {
151                Ok(files) => {
152                    let converted_files: Vec<File> = files
153                        .into_iter()
154                        .map(|f| File {
155                            id: f.id.to_string(),
156                            filename: f.filename,
157                            path: f.path,
158                            file_type: f.file_type.to_string(),
159                            created_at: f.created_at.to_rfc3339(),
160                            updated_at: f.updated_at.to_rfc3339(),
161                        })
162                        .collect();
163
164                    let response = AssignmentFileResponse {
165                        assignment: AssignmentResponse::from(a),
166                        files: converted_files,
167                    };
168
169                    (
170                        StatusCode::OK,
171                        Json(ApiResponse::success(
172                            response,
173                            "Assignment retrieved successfully",
174                        )),
175                    )
176                }
177                Err(e) => (
178                    StatusCode::INTERNAL_SERVER_ERROR,
179                    Json(ApiResponse::<AssignmentFileResponse>::error(&format!(
180                        "Failed to retrieve files: {}",
181                        e
182                    ))),
183                ),
184            }
185        }
186        Ok(None) => (
187            StatusCode::NOT_FOUND,
188            Json(ApiResponse::<AssignmentFileResponse>::error(
189                "Assignment not found",
190            )),
191        ),
192        Err(e) => (
193            StatusCode::INTERNAL_SERVER_ERROR,
194            Json(ApiResponse::<AssignmentFileResponse>::error(&format!(
195                "An error occurred: {}",
196                e
197            ))),
198        ),
199    }
200}
201
202#[derive(Debug, Deserialize)]
203pub struct FilterReq {
204    pub page: Option<i32>,
205    pub per_page: Option<i32>,
206    pub sort: Option<String>,
207    pub query: Option<String>,
208    pub name: Option<String>,
209    pub assignment_type: Option<String>,
210    pub available_before: Option<String>,
211    pub available_after: Option<String>,
212    pub due_before: Option<String>,
213    pub due_after: Option<String>,
214}
215
216#[derive(Serialize)]
217pub struct FilterResponse {
218    pub assignments: Vec<AssignmentResponse>,
219    pub page: i32,
220    pub per_page: i32,
221    pub total: i32,
222}
223
224impl FilterResponse {
225    fn new(
226        assignments: Vec<AssignmentResponse>,
227        page: i32,
228        per_page: i32,
229        total: i32,
230    ) -> Self {
231        Self {
232            assignments,
233            page,
234            per_page,
235            total,
236        }
237    }
238}
239
240/// GET /api/modules/{module_id}/assignments
241///
242/// Retrieve a paginated and optionally filtered list of assignments for a module. Accessible to users assigned to the module.
243///
244/// ### Path Parameters
245/// - `module_id` (i64): The ID of the module to retrieve assignments from
246///
247/// ### Query Parameters
248/// - `page` (optional, i32): Page number for pagination. Defaults to 1, minimum value is 1
249/// - `per_page` (optional, i32): Number of items per page. Defaults to 20, maximum is 100, minimum is 1
250/// - `sort` (optional, string): Comma-separated list of fields to sort by. Prefix with `-` for descending order (e.g., `-due_date`)
251/// - `query` (optional, string): Case-insensitive substring match applied to both `name` and `description`
252/// - `name` (optional, string): Case-insensitive filter to match assignment names
253/// - `assignment_type` (optional, string): Filter by assignment type ("Assignment" or "Practical")
254/// - `available_before` (optional, string): Filter assignments available before this date/time (ISO 8601)
255/// - `available_after` (optional, string): Filter assignments available after this date/time (ISO 8601)
256/// - `due_before` (optional, string): Filter assignments due before this date/time (ISO 8601)
257/// - `due_after` (optional, string): Filter assignments due after this date/time (ISO 8601)
258///
259/// **Allowed sort fields:** `name`, `description`, `due_date`, `available_from`, `assignment_type`, `created_at`, `updated_at`
260///
261/// ### Responses
262///
263/// - `200 OK`
264/// ```json
265/// {
266///   "success": true,
267///   "message": "Assignments retrieved successfully",
268///   "data": {
269///     "assignments": [
270///       {
271///         "id": 123,
272///         "module_id": 456,
273///         "name": "Assignment 1",
274///         "description": "This is a sample assignment",
275///         "assignment_type": "Assignment",
276///         "available_from": "2024-01-01T00:00:00Z",
277///         "due_date": "2024-01-31T23:59:59Z",
278///         "created_at": "2024-01-01T00:00:00Z",
279///         "updated_at": "2024-01-01T00:00:00Z"
280///       }
281///     ],
282///     "page": 1,
283///     "per_page": 20,
284///     "total": 1
285///   }
286/// }
287/// ```
288///
289/// - `400 Bad Request`
290/// ```json
291/// {
292///   "success": false,
293///   "message": "Invalid field used" // or "Invalid assignment_type"
294/// }
295/// ```
296///
297/// - `500 Internal Server Error`
298/// ```json
299/// {
300///   "success": false,
301///   "message": "<database error details>"
302/// }
303/// ```
304pub async fn get_assignments(
305    State(app_state): State<AppState>,
306    Path(module_id): Path<i64>,
307    Query(params): Query<FilterReq>,
308) -> impl IntoResponse {
309    let db = app_state.db();
310    
311    let page = params.page.unwrap_or(1).max(1);
312    let per_page = params.per_page.unwrap_or(20).min(100).max(1);
313
314    if let Some(sort_field) = &params.sort {
315        let valid_fields = [
316            "name",
317            "description",
318            "due_date",
319            "available_from",
320            "assignment_type",
321            "created_at",
322            "updated_at",
323        ];
324        for field in sort_field.split(',') {
325            let field = field.trim().trim_start_matches('-');
326            if !valid_fields.contains(&field) {
327                return (
328                    StatusCode::BAD_REQUEST,
329                    Json(ApiResponse::<FilterResponse>::error("Invalid field used")),
330                );
331            }
332        }
333    }
334
335    let mut condition = Condition::all().add(AssignmentColumn::ModuleId.eq(module_id as i32));
336
337    if let Some(ref query) = params.query {
338        let pattern = format!("%{}%", query.to_lowercase());
339        condition = condition.add(
340            Condition::any()
341                .add(Expr::cust("LOWER(name)").like(&pattern))
342                .add(Expr::cust("LOWER(description)").like(&pattern)),
343        );
344    }
345
346    if let Some(ref name) = params.name {
347        let pattern = format!("%{}%", name.to_lowercase());
348        condition = condition.add(Expr::cust("LOWER(name)").like(&pattern));
349    }
350
351    if let Some(ref assignment_type) = params.assignment_type {
352        match assignment_type.parse::<AssignmentType>() {
353            Ok(atype_enum) => {
354                condition = condition.add(AssignmentColumn::AssignmentType.eq(atype_enum));
355            }
356            Err(_) => {
357                return (
358                    StatusCode::BAD_REQUEST,
359                    Json(ApiResponse::<FilterResponse>::error(
360                        "Invalid assignment_type",
361                    )),
362                );
363            }
364        }
365    }
366
367    if let Some(ref before) = params.available_before {
368        if let Ok(date) = DateTime::parse_from_rfc3339(before) {
369            condition = condition.add(AssignmentColumn::AvailableFrom.lt(date.with_timezone(&Utc)));
370        }
371    }
372
373    if let Some(ref after) = params.available_after {
374        if let Ok(date) = DateTime::parse_from_rfc3339(after) {
375            condition = condition.add(AssignmentColumn::AvailableFrom.gt(date.with_timezone(&Utc)));
376        }
377    }
378
379    if let Some(ref before) = params.due_before {
380        if let Ok(date) = DateTime::parse_from_rfc3339(before) {
381            condition = condition.add(AssignmentColumn::DueDate.lt(date.with_timezone(&Utc)));
382        }
383    }
384
385    if let Some(ref after) = params.due_after {
386        if let Ok(date) = DateTime::parse_from_rfc3339(after) {
387            condition = condition.add(AssignmentColumn::DueDate.gt(date.with_timezone(&Utc)));
388        }
389    }
390
391    let mut query = AssignmentEntity::find().filter(condition);
392
393    if let Some(sort_param) = &params.sort {
394        for sort in sort_param.split(',') {
395            let (field, asc) = if sort.starts_with('-') {
396                (&sort[1..], false)
397            } else {
398                (sort, true)
399            };
400
401            query = match field {
402                "name" => {
403                    if asc {
404                        query.order_by_asc(AssignmentColumn::Name)
405                    } else {
406                        query.order_by_desc(AssignmentColumn::Name)
407                    }
408                }
409                "description" => {
410                    if asc {
411                        query.order_by_asc(AssignmentColumn::Description)
412                    } else {
413                        query.order_by_desc(AssignmentColumn::Description)
414                    }
415                }
416                "due_date" => {
417                    if asc {
418                        query.order_by_asc(AssignmentColumn::DueDate)
419                    } else {
420                        query.order_by_desc(AssignmentColumn::DueDate)
421                    }
422                }
423                "available_from" => {
424                    if asc {
425                        query.order_by_asc(AssignmentColumn::AvailableFrom)
426                    } else {
427                        query.order_by_desc(AssignmentColumn::AvailableFrom)
428                    }
429                }
430                "assignment_type" => {
431                    if asc {
432                        query.order_by_asc(AssignmentColumn::AssignmentType)
433                    } else {
434                        query.order_by_desc(AssignmentColumn::AssignmentType)
435                    }
436                }
437                "created_at" => {
438                    if asc {
439                        query.order_by_asc(AssignmentColumn::CreatedAt)
440                    } else {
441                        query.order_by_desc(AssignmentColumn::CreatedAt)
442                    }
443                }
444                "updated_at" => {
445                    if asc {
446                        query.order_by_asc(AssignmentColumn::UpdatedAt)
447                    } else {
448                        query.order_by_desc(AssignmentColumn::UpdatedAt)
449                    }
450                }
451                _ => query,
452            };
453        }
454    }
455
456    let paginator = query.clone().paginate(db, per_page as u64);
457    let total = match paginator.num_items().await {
458        Ok(n) => n as i32,
459        Err(e) => {
460            eprintln!("Error counting items: {:?}", e);
461            return (
462                StatusCode::INTERNAL_SERVER_ERROR,
463                Json(ApiResponse::<FilterResponse>::error("Error counting items")),
464            );
465        }
466    };
467
468    match paginator.fetch_page((page - 1) as u64).await {
469        Ok(results) => {
470            let assignments: Vec<AssignmentResponse> = results
471                .into_iter()
472                .map(AssignmentResponse::from)
473                .collect();
474
475            let response = FilterResponse::new(assignments, page, per_page, total);
476            (
477                StatusCode::OK,
478                Json(ApiResponse::success(
479                    response,
480                    "Assignments retrieved successfully",
481                )),
482            )
483        }
484        Err(err) => {
485            eprintln!("DB error: {:?}", err);
486            (
487                StatusCode::INTERNAL_SERVER_ERROR,
488                Json(ApiResponse::<FilterResponse>::error(
489                    "Failed to retrieve assignments",
490                )),
491            )
492        }
493    }
494}
495
496#[derive(Debug, Serialize)]
497pub struct PerStudentSubmission {
498    pub user_id: i64,
499    pub username: String,
500    pub count: i8,
501    pub latest_at: DateTime<Utc>,
502    pub latest_late: bool
503}
504
505#[derive(Debug, Serialize)]
506pub struct StatResponse {
507    pub assignment_id: i64,
508    pub total_submissions: i8,
509    pub unique_submitters: i8,
510    pub late_submissions: i8,
511    pub per_student_submission_count: Vec<PerStudentSubmission>
512}
513
514pub fn is_late(submission: DateTime<Utc>, due_date: DateTime<Utc>) -> bool {
515    submission > due_date
516}
517
518/// GET /api/modules/{module_id}/assignments/{assignment_id}/stats
519///
520/// Retrieve submission statistics for a specific assignment. Only accessible by lecturers assigned to the module.
521///
522/// ### Path Parameters
523/// - `module_id` (i64): The ID of the module containing the assignment
524/// - `assignment_id` (i64): The ID of the assignment to get statistics for
525///
526/// ### Responses
527///
528/// - `200 OK`
529/// ```json
530/// {
531///   "success": true,
532///   "message": "Stats retrieved successfully",
533///   "data": {
534///     "assignment_id": 123,
535///     "total_submissions": 15,
536///     "unique_submitters": 12,
537///     "late_submissions": 3,
538///     "per_student_submission_count": [
539///       {
540///         "user_id": 456,
541///         "username": "john.doe",
542///         "count": 2,
543///         "latest_at": "2024-01-31T23:59:59Z",
544///         "latest_late": false
545///       },
546///       {
547///         "user_id": 789,
548///         "username": "jane.smith",
549///         "count": 1,
550///         "latest_at": "2024-02-01T01:30:00Z",
551///         "latest_late": true
552///       }
553///     ]
554///   }
555/// }
556/// ```
557///
558/// - `404 Not Found`
559/// ```json
560/// {
561///   "success": false,
562///   "message": "Assignment not found"
563/// }
564/// ```
565///
566/// - `500 Internal Server Error`
567/// ```json
568/// {
569///   "success": false,
570///   "message": "Database error" // or "Failed to fetch student numbers"
571/// }
572/// ```
573pub async fn get_assignment_stats(
574    State(app_state): State<AppState>,
575    Path((module_id, assignment_id)): Path<(i64, i64)>
576) -> impl IntoResponse {
577    let db = app_state.db();
578
579    let assignment = match AssignmentEntity::find()
580        .filter(AssignmentColumn::Id.eq(assignment_id as i32))
581        .filter(AssignmentColumn::ModuleId.eq(module_id as i32))
582        .one(db)
583        .await
584    {
585        Ok(Some(a)) => a,
586        Ok(None) => {
587            return (
588                StatusCode::NOT_FOUND,
589                Json(ApiResponse::<StatResponse>::error("Assignment not found")),
590            )
591                .into_response();
592        }
593        Err(err) => {
594            eprintln!("DB error fetching assignment: {:?}", err);
595            return (
596                StatusCode::INTERNAL_SERVER_ERROR,
597                Json(ApiResponse::<StatResponse>::error("Database error")),
598            )
599                .into_response();
600        }
601    };
602
603    match assignment_submission::Entity::find()
604        .filter(assignment_submission::Column::AssignmentId.eq(assignment_id as i32))
605        .order_by_desc(assignment_submission::Column::CreatedAt)
606        .all(db)
607        .await
608    {
609        Ok(submissions) => {
610            use std::collections::HashMap;
611
612            let mut total_submissions = 0;
613            let mut late_submissions = 0;
614            let mut unique_users: HashMap<i64, Vec<DateTime<Utc>>> = HashMap::new(); // user_id -> Vec<created_at>
615
616            for sub in &submissions {
617                total_submissions += 1;
618                if is_late(sub.created_at, assignment.due_date) {
619                    late_submissions += 1;
620                }
621
622                unique_users
623                    .entry(sub.user_id)
624                    .or_insert_with(Vec::new)
625                    .push(sub.created_at);
626            }
627
628            let user_ids: Vec<i64> = unique_users.keys().copied().collect();
629            
630            let user_models = user::Entity::find()
631                .filter(user::Column::Id.is_in(user_ids.clone()))
632                .all(db)
633                .await;
634
635            let mut user_id_to_username = HashMap::new();
636            match user_models {
637                Ok(users) => {
638                    for user in users {
639                        user_id_to_username.insert(user.id, user.username);
640                    }
641                }
642                Err(err) => {
643                    eprintln!("DB error fetching student numbers: {:?}", err);
644                    return (
645                        StatusCode::INTERNAL_SERVER_ERROR,
646                        Json(ApiResponse::<StatResponse>::error("Failed to fetch student numbers")),
647                    )
648                        .into_response();
649                }
650            }
651
652            let mut per_student_submission_count = Vec::new();
653
654            for (user_id, created_times) in unique_users.iter() {
655                let latest_at = *created_times.iter().max().unwrap();
656                let latest_late = is_late(latest_at, assignment.due_date);
657                let username = user_id_to_username
658                    .get(user_id)
659                    .cloned()
660                    .unwrap_or_else(|| "UNKNOWN".to_string());
661
662                per_student_submission_count.push(PerStudentSubmission {
663                    user_id: *user_id,
664                    username,
665                    count: created_times.len() as i8,
666                    latest_at,
667                    latest_late,
668                });
669            }
670
671            let response = StatResponse {
672                assignment_id,
673                total_submissions,
674                unique_submitters: unique_users.len() as i8,
675                late_submissions,
676                per_student_submission_count,
677            };
678
679            (
680                StatusCode::OK,
681                Json(ApiResponse::success(response, "Stats retrieved successfully")),
682            )
683                .into_response()
684        }
685        Err(err) => {
686            eprintln!("DB error fetching submissions for stats: {:?}", err);
687            (
688                StatusCode::INTERNAL_SERVER_ERROR,
689                Json(ApiResponse::<StatResponse>::error("Database error")),
690            )
691                .into_response()
692        }
693    }
694}
695
696#[derive(Debug, Serialize)]
697pub struct AssignmentReadiness {
698    pub config_present: bool,
699    pub tasks_present: bool,
700    pub main_present: bool,
701    pub memo_present: bool,
702    pub makefile_present: bool,
703    pub memo_output_present: bool,
704    pub mark_allocator_present: bool,
705    pub is_ready: bool,
706}
707
708/// GET /api/modules/:module_id/assignments/:assignment_id/readiness
709///
710/// Retrieve a detailed readiness report for a specific assignment.
711/// The report includes boolean flags indicating whether each required
712/// component of the assignment is present on disk or in the database.
713///
714/// This endpoint is useful to check if an assignment is fully set up
715/// and eligible to transition from `Setup` to `Ready` state.
716///
717/// ### Path Parameters
718/// - `module_id` (i64): The ID of the module containing the assignment.
719/// - `assignment_id` (i64): The ID of the assignment to check readiness for.
720///
721/// ### Responses
722///
723/// - `200 OK`
724/// ```json
725/// {
726///   "success": true,
727///   "message": "Assignment readiness checked successfully",
728///   "data": {
729///     "config_present": true,
730///     "tasks_present": true,
731///     "main_present": true,
732///     "memo_present": true,
733///     "makefile_present": true,
734///     "memo_output_present": true,
735///     "mark_allocator_present": true,
736///     "is_ready": true
737///   }
738/// }
739/// ```
740///
741/// - `500 Internal Server Error`
742/// ```json
743/// {
744///   "success": false,
745///   "message": "Failed to compute readiness: <error details>"
746/// }
747/// ```
748///
749pub async fn get_assignment_readiness(
750    State(app_state): State<AppState>,
751    Path((module_id, assignment_id)): Path<(i64, i64)>,
752) -> (StatusCode, Json<ApiResponse<AssignmentReadiness>>) {
753    let db = app_state.db();
754
755    match AssignmentModel::compute_readiness_report(db, module_id, assignment_id).await {
756        Ok(report) => {
757            if report.is_ready() {
758                if let Err(e) =
759                    AssignmentModel::try_transition_to_ready(db, module_id, assignment_id).await
760                {
761                    tracing::warn!(
762                        "Failed to transition assignment {} to Ready: {:?}",
763                        assignment_id,
764                        e
765                    );
766                }
767            }
768
769            let response = AssignmentReadiness {
770                config_present: report.config_present,
771                tasks_present: report.tasks_present,
772                main_present: report.main_present,
773                memo_present: report.memo_present,
774                makefile_present: report.makefile_present,
775                memo_output_present: report.memo_output_present,
776                mark_allocator_present: report.mark_allocator_present,
777                is_ready: report.is_ready(),
778            };
779
780            (
781                StatusCode::OK,
782                Json(ApiResponse::success(
783                    response,
784                    "Assignment readiness checked successfully",
785                )),
786            )
787        }
788        Err(e) => (
789            StatusCode::INTERNAL_SERVER_ERROR,
790            Json(ApiResponse::<AssignmentReadiness>::error(&format!(
791                "Failed to compute readiness: {}",
792                e
793            ))),
794        ),
795    }
796}