use crate::{ errors::ApiError, extractors::{AdminApiUser, AuthenticatedUser}, state::AppState, }; use api_types::wrapup::{ GenerateWrapUpRequest, WrapUpGeneratedResponse, WrapUpListResponse, WrapUpStatusResponse, }; use application::wrapup::{ commands::RequestWrapUpCommand, delete as delete_wrapup, generate, get_wrapup, list_wrapups::{self, ListWrapUpsQuery}, }; use axum::{ Json, extract::{Path, State}, http::StatusCode, response::IntoResponse, }; use chrono::NaiveDate; use domain::errors::DomainError; use domain::models::wrapup::{WrapUpRecord, WrapUpStatus}; use domain::value_objects::WrapUpId; use uuid::Uuid; fn record_to_dto(r: &WrapUpRecord) -> WrapUpStatusResponse { WrapUpStatusResponse { id: r.id.value().to_string(), user_id: r.user_id.map(|u| u.to_string()), status: format!("{:?}", r.status), start_date: r.start_date.to_string(), end_date: r.end_date.to_string(), created_at: r.created_at.to_string(), completed_at: r.completed_at.map(|t| t.to_string()), error_message: r.error_message.clone(), } } #[utoipa::path( post, path = "/api/v1/wrapups/generate", request_body = GenerateWrapUpRequest, responses( (status = 200, body = WrapUpGeneratedResponse), (status = 400, description = "Invalid date format"), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden — admin only"), ), security(("bearer_auth" = [])) )] pub async fn post_generate( State(state): State, _admin: AdminApiUser, Json(req): Json, ) -> Result, ApiError> { let start = NaiveDate::parse_from_str(&req.start_date, "%Y-%m-%d") .map_err(|_| DomainError::ValidationError("invalid start_date".into()))?; let end = NaiveDate::parse_from_str(&req.end_date, "%Y-%m-%d") .map_err(|_| DomainError::ValidationError("invalid end_date".into()))?; let user_id = req.user_id; let cmd = RequestWrapUpCommand { user_id, start_date: start, end_date: end, }; let id = generate::execute(&state.app_ctx.deps.wrapup.generate, cmd).await?; Ok(Json(WrapUpGeneratedResponse { id: id.value().to_string(), })) } #[utoipa::path( get, path = "/api/v1/wrapups", responses( (status = 200, body = WrapUpListResponse), (status = 401, description = "Unauthorized"), ), security(("bearer_auth" = [])) )] pub async fn get_list( State(state): State, user: AuthenticatedUser, ) -> Result, ApiError> { let records = list_wrapups::execute( &state.app_ctx.deps.wrapup.list_wrapups, ListWrapUpsQuery { user_id: Some(user.0.value()), }, ) .await?; Ok(Json(WrapUpListResponse { items: records.iter().map(record_to_dto).collect(), })) } #[utoipa::path( get, path = "/api/v1/wrapups/{id}", params(("id" = Uuid, Path, description = "Wrap-up ID")), responses( (status = 200, body = WrapUpStatusResponse), (status = 401, description = "Unauthorized"), (status = 404, description = "Not found"), ), security(("bearer_auth" = [])) )] pub async fn get_status( State(state): State, _user: AuthenticatedUser, Path(id): Path, ) -> Result, ApiError> { let record = get_wrapup::execute( &state.app_ctx.deps.wrapup.get_wrapup, WrapUpId::from_uuid(id), ) .await? .ok_or_else(|| DomainError::NotFound("wrap-up not found".into()))?; Ok(Json(record_to_dto(&record))) } #[utoipa::path( get, path = "/api/v1/wrapups/{id}/report", params(("id" = Uuid, Path, description = "Wrap-up ID")), responses( (status = 200, description = "Report JSON", content_type = "application/json"), (status = 202, description = "Still generating"), (status = 401, description = "Unauthorized"), (status = 404, description = "Not found"), ), security(("bearer_auth" = [])) )] pub async fn get_report( State(state): State, _user: AuthenticatedUser, Path(id): Path, ) -> impl IntoResponse { match get_wrapup::execute( &state.app_ctx.deps.wrapup.get_wrapup, WrapUpId::from_uuid(id), ) .await { Ok(Some(record)) if record.status == WrapUpStatus::Ready => match record.report { Some(ref report) => { let json = serde_json::to_string(report).unwrap_or_default(); (StatusCode::OK, [("content-type", "application/json")], json).into_response() } None => StatusCode::NOT_FOUND.into_response(), }, Ok(Some(_)) => StatusCode::ACCEPTED.into_response(), Ok(None) => StatusCode::NOT_FOUND.into_response(), Err(e) => crate::errors::domain_error_response(e), } } #[utoipa::path( delete, path = "/api/v1/wrapups/{id}", params(("id" = Uuid, Path, description = "Wrap-up ID")), responses( (status = 204, description = "Deleted"), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden — admin only"), (status = 404, description = "Not found"), ), security(("bearer_auth" = [])) )] pub async fn delete_wrapup_handler( State(state): State, _admin: AdminApiUser, Path(id): Path, ) -> Result { delete_wrapup::execute( &state.app_ctx.deps.wrapup.delete_wrapup, WrapUpId::from_uuid(id), ) .await?; Ok(StatusCode::NO_CONTENT) }