backbone_maintenance/presentation/http/
maintenance_request_lifecycle_handler.rs1use std::sync::Arc;
18
19use axum::{
20 extract::{Path, State},
21 http::StatusCode,
22 response::IntoResponse,
23 routing::post,
24 Json, Router,
25};
26use chrono::{DateTime, NaiveDate, Utc};
27use rust_decimal::Decimal;
28use serde::Deserialize;
29use serde_json::{json, Value};
30use uuid::Uuid;
31
32use crate::application::service::{
33 MaintenanceEventSink, MaintenanceRequestUpdate, MaintenanceRequestWriteService,
34 NewMaintenanceRequest, RequestWriteError as WriteError,
35};
36use crate::domain::entity::{MaintenanceType, RequestKanbanState, RequestPriority, RepeatType, RepeatUnit};
37
38#[derive(Clone)]
42pub struct MaintenanceRequestLifecycleState {
43 pub write_svc: Arc<MaintenanceRequestWriteService>,
44 pub event_sink: Arc<dyn MaintenanceEventSink>,
45}
46
47pub struct MaintenanceRequestApiError(pub WriteError);
51
52impl IntoResponse for MaintenanceRequestApiError {
53 fn into_response(self) -> axum::response::Response {
54 let msg = self.0.to_string();
55 let (status, code) = match self.0 {
56 WriteError::NotFound(_) => (StatusCode::NOT_FOUND, "MAINTENANCE_REQUEST_NOT_FOUND"),
57 WriteError::ScheduleEndBeforeStart => (
59 StatusCode::UNPROCESSABLE_ENTITY,
60 "MAINTENANCE_REQUEST_SCHEDULE_END_BEFORE_START",
61 ),
62 WriteError::RepeatIntervalBelowOne => (
64 StatusCode::UNPROCESSABLE_ENTITY,
65 "MAINTENANCE_REQUEST_REPEAT_INTERVAL_BELOW_ONE",
66 ),
67 WriteError::RepeatUntilMissing => (
69 StatusCode::UNPROCESSABLE_ENTITY,
70 "MAINTENANCE_REQUEST_REPEAT_UNTIL_MISSING",
71 ),
72 WriteError::CorrectiveCannotRecur => (
74 StatusCode::UNPROCESSABLE_ENTITY,
75 "MAINTENANCE_REQUEST_CORRECTIVE_CANNOT_RECUR",
76 ),
77 WriteError::InvalidState(_) => (StatusCode::CONFLICT, "MAINTENANCE_REQUEST_INVALID_STATE"),
78 WriteError::Invalid(_) => (StatusCode::BAD_REQUEST, "MAINTENANCE_REQUEST_INVALID_INPUT"),
79 WriteError::Db(_) => (StatusCode::INTERNAL_SERVER_ERROR, "MAINTENANCE_REQUEST_DATABASE_ERROR"),
80 };
81 (status, Json(json!({ "success": false, "error": code, "message": msg }))).into_response()
82 }
83}
84
85#[derive(Debug, Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct CreateMaintenanceRequestBody {
93 pub name: String,
94 #[serde(default)]
95 pub description: Option<String>,
96 #[serde(default)]
97 pub schedule_date: Option<DateTime<Utc>>,
98 #[serde(default)]
99 pub schedule_end: Option<DateTime<Utc>>,
100 #[serde(default)]
101 pub duration: Decimal,
102 #[serde(default)]
103 pub owner_user_id: Option<Uuid>,
104 #[serde(default)]
105 pub user_id: Option<Uuid>,
106 #[serde(default)]
107 pub asset_id: Option<Uuid>,
108 #[serde(default)]
109 pub stage_id: Option<Uuid>,
110 #[serde(default)]
111 pub kanban_state: RequestKanbanState,
112 #[serde(default)]
113 pub priority: RequestPriority,
114 #[serde(default = "default_maintenance_type")]
115 pub maintenance_type: MaintenanceType,
116 #[serde(default)]
117 pub recurring: bool,
118 #[serde(default = "default_repeat_interval")]
119 pub repeat_interval: i32,
120 #[serde(default)]
121 pub repeat_unit: RepeatUnit,
122 #[serde(default)]
123 pub repeat_type: RepeatType,
124 #[serde(default)]
125 pub repeat_until: Option<NaiveDate>,
126}
127
128fn default_maintenance_type() -> MaintenanceType {
129 MaintenanceType::Corrective
130}
131
132fn default_repeat_interval() -> i32 {
133 1
134}
135
136#[derive(Debug, Deserialize)]
139#[serde(rename_all = "camelCase")]
140pub struct TransitionRequestBody {
141 pub target_stage_id: Uuid,
142 #[serde(default)]
143 pub kanban_state: Option<RequestKanbanState>,
144}
145
146#[derive(Debug, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct UpdateMaintenanceRequestBody {
151 #[serde(default)]
152 pub name: Option<String>,
153 #[serde(default)]
154 pub description: Option<String>,
155 #[serde(default)]
156 pub schedule_date: Option<DateTime<Utc>>,
157 #[serde(default)]
158 pub schedule_end: Option<DateTime<Utc>>,
159 #[serde(default)]
160 pub duration: Option<Decimal>,
161 #[serde(default)]
162 pub owner_user_id: Option<Uuid>,
163 #[serde(default)]
164 pub user_id: Option<Uuid>,
165 #[serde(default)]
166 pub asset_id: Option<Uuid>,
167 #[serde(default)]
168 pub priority: Option<RequestPriority>,
169 #[serde(default)]
170 pub maintenance_type: Option<MaintenanceType>,
171 #[serde(default)]
172 pub recurring: Option<bool>,
173 #[serde(default)]
174 pub repeat_interval: Option<i32>,
175 #[serde(default)]
176 pub repeat_unit: Option<RepeatUnit>,
177 #[serde(default)]
178 pub repeat_type: Option<RepeatType>,
179 #[serde(default)]
180 pub repeat_until: Option<NaiveDate>,
181}
182
183pub async fn create_request(
189 State(st): State<MaintenanceRequestLifecycleState>,
190 Json(req): Json<CreateMaintenanceRequestBody>,
191) -> Result<(StatusCode, Json<Value>), MaintenanceRequestApiError> {
192 let id = st
193 .write_svc
194 .create_request(NewMaintenanceRequest {
195 name: req.name,
196 description: req.description,
197 schedule_date: req.schedule_date,
198 schedule_end: req.schedule_end,
199 duration: req.duration,
200 owner_user_id: req.owner_user_id,
201 user_id: req.user_id,
202 asset_id: req.asset_id,
203 stage_id: req.stage_id,
204 kanban_state: req.kanban_state,
205 priority: req.priority,
206 maintenance_type: req.maintenance_type,
207 recurring: req.recurring,
208 repeat_interval: req.repeat_interval,
209 repeat_unit: req.repeat_unit,
210 repeat_type: req.repeat_type,
211 repeat_until: req.repeat_until,
212 })
213 .await
214 .map_err(MaintenanceRequestApiError)?;
215 Ok((
216 StatusCode::CREATED,
217 Json(json!({ "success": true, "data": { "id": id } })),
218 ))
219}
220
221pub async fn transition_request(
224 State(st): State<MaintenanceRequestLifecycleState>,
225 Path(id): Path<Uuid>,
226 Json(req): Json<TransitionRequestBody>,
227) -> Result<(StatusCode, Json<Value>), MaintenanceRequestApiError> {
228 let outcome = st
229 .write_svc
230 .transition_request(id, req.target_stage_id, req.kanban_state, st.event_sink.as_ref())
231 .await
232 .map_err(MaintenanceRequestApiError)?;
233 Ok((
234 StatusCode::OK,
235 Json(json!({
236 "success": true,
237 "data": {
238 "request_id": outcome.request_id,
239 "from_stage_id": outcome.from_stage_id,
240 "to_stage_id": outcome.to_stage_id,
241 "close_date": outcome.close_date,
242 "spawned_successor_id": outcome.spawned_successor_id,
243 "already": outcome.already,
244 }
245 })),
246 ))
247}
248
249pub async fn update_request(
252 State(st): State<MaintenanceRequestLifecycleState>,
253 Path(id): Path<Uuid>,
254 Json(req): Json<UpdateMaintenanceRequestBody>,
255) -> Result<(StatusCode, Json<Value>), MaintenanceRequestApiError> {
256 st.write_svc
257 .update_request(
258 id,
259 MaintenanceRequestUpdate {
260 name: req.name,
261 description: req.description,
262 schedule_date: req.schedule_date,
263 schedule_end: req.schedule_end,
264 duration: req.duration,
265 owner_user_id: req.owner_user_id,
266 user_id: req.user_id,
267 asset_id: req.asset_id,
268 priority: req.priority,
269 maintenance_type: req.maintenance_type,
270 recurring: req.recurring,
271 repeat_interval: req.repeat_interval,
272 repeat_unit: req.repeat_unit,
273 repeat_type: req.repeat_type,
274 repeat_until: req.repeat_until,
275 },
276 )
277 .await
278 .map_err(MaintenanceRequestApiError)?;
279 Ok((StatusCode::OK, Json(json!({ "success": true }))))
280}
281
282pub fn create_maintenance_request_lifecycle_routes(state: MaintenanceRequestLifecycleState) -> Router {
285 Router::<MaintenanceRequestLifecycleState>::new()
286 .route("/maintenance_requests", post(create_request))
287 .route("/maintenance_requests/:id/transition", post(transition_request))
288 .route("/maintenance_requests/:id", post(update_request))
289 .with_state(state)
290}