backbone_maintenance/presentation/http/
maintenance_lifecycle_handler.rs1use std::sync::Arc;
19
20use axum::{
21 extract::{Path, State},
22 http::StatusCode,
23 response::IntoResponse,
24 routing::post,
25 Json, Router,
26};
27use chrono::NaiveDate;
28use rust_decimal::Decimal;
29use serde::Deserialize;
30use serde_json::{json, Value};
31use uuid::Uuid;
32
33use crate::application::service::{
34 GlPostSink, InventoryPort, MaintenanceError as WriteError, MaintenanceEventSink,
35 MaintenanceWriteService, NewVisit,
36};
37
38#[derive(Clone)]
44pub struct MaintenanceLifecycleState {
45 pub write_svc: Arc<MaintenanceWriteService>,
46 pub inventory: Arc<dyn InventoryPort>,
47 pub gl: Arc<dyn GlPostSink>,
48 pub event_sink: Arc<dyn MaintenanceEventSink>,
49}
50
51pub struct MaintenanceApiError(pub WriteError);
55
56impl IntoResponse for MaintenanceApiError {
57 fn into_response(self) -> axum::response::Response {
58 let msg = self.0.to_string();
59 let (status, code) = match self.0 {
60 WriteError::NotFound(_) => (StatusCode::NOT_FOUND, "MAINTENANCE_NOT_FOUND"),
61 WriteError::InvalidState(_) => (StatusCode::CONFLICT, "MAINTENANCE_INVALID_STATE"),
62 WriteError::Invalid(_) => (StatusCode::BAD_REQUEST, "MAINTENANCE_INVALID_INPUT"),
63 WriteError::Unbalanced
66 | WriteError::GlRejected(_)
67 | WriteError::InventoryRejected(_) => {
68 (StatusCode::FAILED_DEPENDENCY, "MAINTENANCE_POST_REJECTED")
69 }
70 WriteError::Db(_) => (StatusCode::INTERNAL_SERVER_ERROR, "MAINTENANCE_DATABASE_ERROR"),
71 };
72 (status, Json(json!({ "success": false, "error": code, "message": msg }))).into_response()
73 }
74}
75
76#[derive(Debug, Deserialize)]
82#[serde(rename_all = "camelCase")]
83pub struct PlanVisitRequest {
84 pub asset_id: Uuid,
85 #[serde(default)]
86 pub schedule_id: Option<Uuid>,
87 pub maintenance_type: String,
89 pub scheduled_date: NaiveDate,
90 #[serde(default)]
91 pub warehouse_id: Option<Uuid>,
92 #[serde(default)]
93 pub warranty_claim_id: Option<Uuid>,
94 #[serde(default)]
95 pub labor_cost: Decimal,
96 pub maintenance_expense_account_id: Uuid,
97 pub parts_inventory_account_id: Uuid,
98 pub labor_payable_account_id: Uuid,
99}
100
101#[derive(Debug, Deserialize)]
102#[serde(rename_all = "camelCase")]
103pub struct AddPartRequest {
104 pub item_id: Uuid,
105 pub quantity: Decimal,
106}
107
108#[derive(Debug, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct CompleteVisitRequest {
111 pub performed_date: NaiveDate,
112}
113
114pub async fn plan_visit(
120 State(st): State<MaintenanceLifecycleState>,
121 Json(req): Json<PlanVisitRequest>,
122) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
123 let id = st
124 .write_svc
125 .plan_visit(NewVisit {
126 asset_id: req.asset_id,
127 schedule_id: req.schedule_id,
128 maintenance_type: req.maintenance_type,
129 scheduled_date: req.scheduled_date,
130 warehouse_id: req.warehouse_id,
131 warranty_claim_id: req.warranty_claim_id,
132 labor_cost: req.labor_cost,
133 maintenance_expense_account_id: req.maintenance_expense_account_id,
134 parts_inventory_account_id: req.parts_inventory_account_id,
135 labor_payable_account_id: req.labor_payable_account_id,
136 })
137 .await
138 .map_err(MaintenanceApiError)?;
139 Ok((
140 StatusCode::CREATED,
141 Json(json!({ "success": true, "data": { "id": id } })),
142 ))
143}
144
145pub async fn start_visit(
147 State(st): State<MaintenanceLifecycleState>,
148 Path(id): Path<Uuid>,
149) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
150 st.write_svc.start_visit(id).await.map_err(MaintenanceApiError)?;
151 Ok((StatusCode::OK, Json(json!({ "success": true }))))
152}
153
154pub async fn add_part(
156 State(st): State<MaintenanceLifecycleState>,
157 Path(id): Path<Uuid>,
158 Json(req): Json<AddPartRequest>,
159) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
160 let part_id = st
161 .write_svc
162 .add_part(id, req.item_id, req.quantity)
163 .await
164 .map_err(MaintenanceApiError)?;
165 Ok((
166 StatusCode::CREATED,
167 Json(json!({ "success": true, "data": { "id": part_id } })),
168 ))
169}
170
171pub async fn complete_visit(
175 State(st): State<MaintenanceLifecycleState>,
176 Path(id): Path<Uuid>,
177 Json(req): Json<CompleteVisitRequest>,
178) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
179 let outcome = st
180 .write_svc
181 .complete_visit(
182 id,
183 req.performed_date,
184 st.inventory.as_ref(),
185 st.gl.as_ref(),
186 st.event_sink.as_ref(),
187 )
188 .await
189 .map_err(MaintenanceApiError)?;
190 Ok((
191 StatusCode::OK,
192 Json(json!({
193 "success": true,
194 "data": {
195 "visit_id": outcome.visit_id,
196 "journal_id": outcome.journal_id,
197 "total_cost": outcome.total_cost,
198 "already": outcome.already,
199 }
200 })),
201 ))
202}
203
204pub async fn cancel_visit(
206 State(st): State<MaintenanceLifecycleState>,
207 Path(id): Path<Uuid>,
208) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
209 st.write_svc.cancel_visit(id).await.map_err(MaintenanceApiError)?;
210 Ok((StatusCode::OK, Json(json!({ "success": true }))))
211}
212
213pub fn create_maintenance_lifecycle_routes(state: MaintenanceLifecycleState) -> Router {
216 Router::<MaintenanceLifecycleState>::new()
217 .route("/maintenance_visits", post(plan_visit))
218 .route("/maintenance_visits/:id/start", post(start_visit))
219 .route("/maintenance_visits/:id/parts", post(add_part))
220 .route("/maintenance_visits/:id/complete", post(complete_visit))
221 .route("/maintenance_visits/:id/cancel", post(cancel_visit))
222 .with_state(state)
223}