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}