api/routes/modules/
post.rs

1//! Module creation routes.
2//!
3//! Provides the `POST /api/modules` endpoint for creating new university modules.  
4//! Only accessible by admin users. Responses follow the standard `ApiResponse` format.
5
6use axum::{
7    extract::State,
8    http::StatusCode,
9    response::IntoResponse,
10    Json,
11};
12use chrono::{Datelike, Utc};
13use util::state::AppState;
14use validator::Validate;
15use db::models::module::{Model as Module};
16use crate::response::ApiResponse;
17use crate::routes::modules::common::{ModuleRequest, ModuleResponse};
18
19/// POST /api/modules
20///
21/// Create a new university module. Only accessible by admin users.
22///
23/// ### Request Body
24/// ```json
25/// {
26///   "code": "COS301",
27///   "year": 2025,
28///   "description": "Advanced Software Engineering",
29///   "credits": 16
30/// }
31/// ```
32///
33/// ### Validation Rules
34/// * `code`: required, must be uppercase alphanumeric (e.g., `^[A-Z]{3}\d{3}$`), unique
35/// * `year`: required, must be the current year or later
36/// * `description`: optional, max length 1000 characters
37/// * `credits`: required, must be a positive integer
38///
39/// ### Responses
40///
41/// - `201 Created`  
42/// ```json
43/// {
44///   "success": true,
45///   "data": {
46///     "id": 1,
47///     "code": "COS301",
48///     "year": 2025,
49///     "description": "Advanced Software Engineering",
50///     "credits": 16,
51///     "created_at": "2025-05-23T18:00:00Z",
52///     "updated_at": "2025-05-23T18:00:00Z"
53///   },
54///   "message": "Module created successfully"
55/// }
56/// ```
57///
58/// - `400 Bad Request` (validation failure)  
59/// ```json
60/// {
61///   "success": false,
62///   "message": "Invalid input: code format must be ABC123 and credits must be a positive number"
63/// }
64/// ```
65///
66/// - `403 Forbidden` (missing admin role)  
67/// ```json
68/// {
69///   "success": false,
70///   "message": "You do not have permission to perform this action"
71/// }
72/// ```
73///
74/// - `409 Conflict` (duplicate code)  
75/// ```json
76/// {
77///   "success": false,
78///   "message": "A module with this code already exists"
79/// }
80/// ```
81///
82/// - `500 Internal Server Error`  
83/// ```json
84/// {
85///   "success": false,
86///   "message": "Database error: detailed error here"
87/// }
88/// ```
89pub async fn create(
90    State(state): State<AppState>,
91    Json(req): Json<ModuleRequest>
92) -> impl IntoResponse {
93    let db = state.db();
94
95    if let Err(validation_errors) = req.validate() {
96        let error_message = common::format_validation_errors(&validation_errors);
97        return (
98            StatusCode::BAD_REQUEST,
99            Json(ApiResponse::<ModuleResponse>::error(error_message)),
100        );
101    }
102
103    let current_year = Utc::now().year();
104    if req.year < current_year {
105        return (
106            StatusCode::BAD_REQUEST,
107            Json(ApiResponse::<ModuleResponse>::error(format!(
108                "Year must be {} or later",
109                current_year
110            ))),
111        );
112    }
113
114    match Module::create(
115        db,
116        &req.code,
117        req.year,
118        req.description.as_deref(),
119        req.credits,
120    )
121    .await
122    {
123        Ok(module) => {
124            let response = ModuleResponse::from(module);
125            (
126                StatusCode::CREATED,
127                Json(ApiResponse::success(response, "Module created successfully")),
128            )
129        }
130        Err(e) => {
131            if let sea_orm::DbErr::Exec(err) = &e {
132                if err.to_string().contains("UNIQUE constraint failed: modules.code") {
133                    return (
134                        StatusCode::CONFLICT,
135                        Json(ApiResponse::<ModuleResponse>::error(
136                            "A module with this code already exists",
137                        )),
138                    );
139                }
140            }
141
142            (
143                StatusCode::INTERNAL_SERVER_ERROR,
144                Json(ApiResponse::<ModuleResponse>::error(format!(
145                    "Database error: {}",
146                    e
147                ))),
148            )
149        }
150    }
151}