use std::sync::Arc;
use axum::{
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
routing::post,
Json, Router,
};
use chrono::NaiveDate;
use rust_decimal::Decimal;
use serde::Deserialize;
use serde_json::{json, Value};
use uuid::Uuid;
use crate::application::service::{
GlPostSink, InventoryPort, MaintenanceError as WriteError, MaintenanceEventSink,
MaintenanceWriteService, NewVisit,
};
#[derive(Clone)]
pub struct MaintenanceLifecycleState {
pub write_svc: Arc<MaintenanceWriteService>,
pub inventory: Arc<dyn InventoryPort>,
pub gl: Arc<dyn GlPostSink>,
pub event_sink: Arc<dyn MaintenanceEventSink>,
}
pub struct MaintenanceApiError(pub WriteError);
impl IntoResponse for MaintenanceApiError {
fn into_response(self) -> axum::response::Response {
let msg = self.0.to_string();
let (status, code) = match self.0 {
WriteError::NotFound(_) => (StatusCode::NOT_FOUND, "MAINTENANCE_NOT_FOUND"),
WriteError::InvalidState(_) => (StatusCode::CONFLICT, "MAINTENANCE_INVALID_STATE"),
WriteError::Invalid(_) => (StatusCode::BAD_REQUEST, "MAINTENANCE_INVALID_INPUT"),
WriteError::Unbalanced
| WriteError::GlRejected(_)
| WriteError::InventoryRejected(_) => {
(StatusCode::FAILED_DEPENDENCY, "MAINTENANCE_POST_REJECTED")
}
WriteError::Db(_) => (StatusCode::INTERNAL_SERVER_ERROR, "MAINTENANCE_DATABASE_ERROR"),
};
(status, Json(json!({ "success": false, "error": code, "message": msg }))).into_response()
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanVisitRequest {
pub asset_id: Uuid,
#[serde(default)]
pub schedule_id: Option<Uuid>,
pub maintenance_type: String,
pub scheduled_date: NaiveDate,
#[serde(default)]
pub warehouse_id: Option<Uuid>,
#[serde(default)]
pub warranty_claim_id: Option<Uuid>,
#[serde(default)]
pub labor_cost: Decimal,
pub maintenance_expense_account_id: Uuid,
pub parts_inventory_account_id: Uuid,
pub labor_payable_account_id: Uuid,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AddPartRequest {
pub item_id: Uuid,
pub quantity: Decimal,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompleteVisitRequest {
pub performed_date: NaiveDate,
}
pub async fn plan_visit(
State(st): State<MaintenanceLifecycleState>,
Json(req): Json<PlanVisitRequest>,
) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
let id = st
.write_svc
.plan_visit(NewVisit {
asset_id: req.asset_id,
schedule_id: req.schedule_id,
maintenance_type: req.maintenance_type,
scheduled_date: req.scheduled_date,
warehouse_id: req.warehouse_id,
warranty_claim_id: req.warranty_claim_id,
labor_cost: req.labor_cost,
maintenance_expense_account_id: req.maintenance_expense_account_id,
parts_inventory_account_id: req.parts_inventory_account_id,
labor_payable_account_id: req.labor_payable_account_id,
})
.await
.map_err(MaintenanceApiError)?;
Ok((
StatusCode::CREATED,
Json(json!({ "success": true, "data": { "id": id } })),
))
}
pub async fn start_visit(
State(st): State<MaintenanceLifecycleState>,
Path(id): Path<Uuid>,
) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
st.write_svc.start_visit(id).await.map_err(MaintenanceApiError)?;
Ok((StatusCode::OK, Json(json!({ "success": true }))))
}
pub async fn add_part(
State(st): State<MaintenanceLifecycleState>,
Path(id): Path<Uuid>,
Json(req): Json<AddPartRequest>,
) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
let part_id = st
.write_svc
.add_part(id, req.item_id, req.quantity)
.await
.map_err(MaintenanceApiError)?;
Ok((
StatusCode::CREATED,
Json(json!({ "success": true, "data": { "id": part_id } })),
))
}
pub async fn complete_visit(
State(st): State<MaintenanceLifecycleState>,
Path(id): Path<Uuid>,
Json(req): Json<CompleteVisitRequest>,
) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
let outcome = st
.write_svc
.complete_visit(
id,
req.performed_date,
st.inventory.as_ref(),
st.gl.as_ref(),
st.event_sink.as_ref(),
)
.await
.map_err(MaintenanceApiError)?;
Ok((
StatusCode::OK,
Json(json!({
"success": true,
"data": {
"visit_id": outcome.visit_id,
"journal_id": outcome.journal_id,
"total_cost": outcome.total_cost,
"already": outcome.already,
}
})),
))
}
pub async fn cancel_visit(
State(st): State<MaintenanceLifecycleState>,
Path(id): Path<Uuid>,
) -> Result<(StatusCode, Json<Value>), MaintenanceApiError> {
st.write_svc.cancel_visit(id).await.map_err(MaintenanceApiError)?;
Ok((StatusCode::OK, Json(json!({ "success": true }))))
}
pub fn create_maintenance_lifecycle_routes(state: MaintenanceLifecycleState) -> Router {
Router::<MaintenanceLifecycleState>::new()
.route("/maintenance_visits", post(plan_visit))
.route("/maintenance_visits/:id/start", post(start_visit))
.route("/maintenance_visits/:id/parts", post(add_part))
.route("/maintenance_visits/:id/complete", post(complete_visit))
.route("/maintenance_visits/:id/cancel", post(cancel_visit))
.with_state(state)
}