//! Contract types API handler. use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, Json, }; use serde::Serialize; use utoipa::ToSchema; use uuid::Uuid; use crate::db::models::{ ContractTypeTemplateRecord, ContractTypeTemplateSummary, CreateTemplateRequest, UpdateTemplateRequest, }; use crate::db::repository; use crate::llm::templates; use crate::llm::templates::ContractTypeTemplate; use crate::server::auth::{Authenticated, MaybeAuthenticated}; use crate::server::state::SharedState; // ============================================================================= // Contract Type Templates (Workflow Definitions) // ============================================================================= /// Response for listing contract types #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ListContractTypesResponse { pub contract_types: Vec, } /// Response for a single custom template #[derive(Debug, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct TemplateResponse { pub template: ContractTypeTemplateSummary, } /// List all available contract type templates (built-in + custom) #[utoipa::path( get, path = "/api/v1/contract-types", responses( (status = 200, description = "Contract types retrieved successfully", body = ListContractTypesResponse) ), tag = "templates" )] pub async fn list_contract_types( State(state): State, MaybeAuthenticated(auth): MaybeAuthenticated, ) -> impl IntoResponse { // Start with built-in types let mut contract_types = templates::all_contract_types(); // If authenticated, also fetch custom templates for this owner if let Some(user) = auth { if let Some(ref pool) = state.db_pool { if let Ok(custom_templates) = repository::list_templates_for_owner(pool, user.owner_id).await { for template in custom_templates { contract_types.push(template_record_to_api(&template)); } } } } ( StatusCode::OK, Json(ListContractTypesResponse { contract_types }), ) .into_response() } /// Create a new custom contract type template #[utoipa::path( post, path = "/api/v1/contract-types", request_body = CreateTemplateRequest, responses( (status = 201, description = "Template created successfully", body = TemplateResponse), (status = 400, description = "Invalid request"), (status = 401, description = "Unauthorized"), (status = 409, description = "Template with this name already exists") ), tag = "templates" )] pub async fn create_template( State(state): State, Authenticated(auth): Authenticated, Json(req): Json, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": "Database not configured" })), ) .into_response(); }; // Validate request if req.name.trim().is_empty() { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "code": "INVALID_REQUEST", "message": "Template name cannot be empty" })), ) .into_response(); } if req.phases.is_empty() { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "code": "INVALID_REQUEST", "message": "Template must have at least one phase" })), ) .into_response(); } // Validate default_phase is in the phases list if !req.phases.iter().any(|p| p.id == req.default_phase) { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "code": "INVALID_REQUEST", "message": format!("Default phase '{}' is not in the phases list", req.default_phase) })), ) .into_response(); } // Check that template name doesn't conflict with built-in types let builtin_names = ["simple", "specification", "execute"]; if builtin_names.contains(&req.name.to_lowercase().as_str()) { return ( StatusCode::CONFLICT, Json(serde_json::json!({ "code": "NAME_CONFLICT", "message": "Cannot create a template with the same name as a built-in type" })), ) .into_response(); } match repository::create_template_for_owner(pool, auth.owner_id, req).await { Ok(template) => ( StatusCode::CREATED, Json(serde_json::json!({ "template": template_record_to_summary(&template) })), ) .into_response(), Err(e) => { // Check for unique constraint violation let error_str = e.to_string(); if error_str.contains("unique") || error_str.contains("duplicate") { return ( StatusCode::CONFLICT, Json(serde_json::json!({ "code": "NAME_CONFLICT", "message": "A template with this name already exists" })), ) .into_response(); } ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": format!("Failed to create template: {}", e) })), ) .into_response() } } } /// Get a specific contract type template by ID #[utoipa::path( get, path = "/api/v1/contract-types/{id}", params( ("id" = Uuid, Path, description = "Template ID") ), responses( (status = 200, description = "Template retrieved successfully", body = TemplateResponse), (status = 401, description = "Unauthorized"), (status = 404, description = "Template not found") ), tag = "templates" )] pub async fn get_template( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": "Database not configured" })), ) .into_response(); }; match repository::get_template_for_owner(pool, id, auth.owner_id).await { Ok(Some(template)) => ( StatusCode::OK, Json(serde_json::json!({ "template": template_record_to_summary(&template) })), ) .into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "code": "NOT_FOUND", "message": "Template not found" })), ) .into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": format!("Failed to get template: {}", e) })), ) .into_response(), } } /// Update a contract type template #[utoipa::path( put, path = "/api/v1/contract-types/{id}", params( ("id" = Uuid, Path, description = "Template ID") ), request_body = UpdateTemplateRequest, responses( (status = 200, description = "Template updated successfully", body = TemplateResponse), (status = 400, description = "Invalid request"), (status = 401, description = "Unauthorized"), (status = 404, description = "Template not found"), (status = 409, description = "Version conflict") ), tag = "templates" )] pub async fn update_template( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": "Database not configured" })), ) .into_response(); }; // Validate phases if provided if let Some(ref phases) = req.phases { if phases.is_empty() { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "code": "INVALID_REQUEST", "message": "Template must have at least one phase" })), ) .into_response(); } // If default_phase is also provided, validate it's in the phases if let Some(ref default_phase) = req.default_phase { if !phases.iter().any(|p| &p.id == default_phase) { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "code": "INVALID_REQUEST", "message": format!("Default phase '{}' is not in the phases list", default_phase) })), ) .into_response(); } } } // Check that template name doesn't conflict with built-in types if let Some(ref name) = req.name { let builtin_names = ["simple", "specification", "execute"]; if builtin_names.contains(&name.to_lowercase().as_str()) { return ( StatusCode::CONFLICT, Json(serde_json::json!({ "code": "NAME_CONFLICT", "message": "Cannot rename template to a built-in type name" })), ) .into_response(); } } match repository::update_template_for_owner(pool, id, auth.owner_id, req).await { Ok(Some(template)) => ( StatusCode::OK, Json(serde_json::json!({ "template": template_record_to_summary(&template) })), ) .into_response(), Ok(None) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "code": "NOT_FOUND", "message": "Template not found" })), ) .into_response(), Err(repository::RepositoryError::VersionConflict { expected, actual }) => ( StatusCode::CONFLICT, Json(serde_json::json!({ "code": "VERSION_CONFLICT", "message": format!("Version conflict: expected {}, found {}", expected, actual), "expectedVersion": expected, "actualVersion": actual })), ) .into_response(), Err(e) => { let error_str = e.to_string(); if error_str.contains("unique") || error_str.contains("duplicate") { return ( StatusCode::CONFLICT, Json(serde_json::json!({ "code": "NAME_CONFLICT", "message": "A template with this name already exists" })), ) .into_response(); } ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": format!("Failed to update template: {}", e) })), ) .into_response() } } } /// Delete a contract type template #[utoipa::path( delete, path = "/api/v1/contract-types/{id}", params( ("id" = Uuid, Path, description = "Template ID") ), responses( (status = 204, description = "Template deleted successfully"), (status = 401, description = "Unauthorized"), (status = 404, description = "Template not found") ), tag = "templates" )] pub async fn delete_template( State(state): State, Authenticated(auth): Authenticated, Path(id): Path, ) -> impl IntoResponse { let Some(ref pool) = state.db_pool else { return ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": "Database not configured" })), ) .into_response(); }; match repository::delete_template_for_owner(pool, id, auth.owner_id).await { Ok(true) => StatusCode::NO_CONTENT.into_response(), Ok(false) => ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "code": "NOT_FOUND", "message": "Template not found" })), ) .into_response(), Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "code": "DB_ERROR", "message": format!("Failed to delete template: {}", e) })), ) .into_response(), } } // ============================================================================= // Helper Functions // ============================================================================= /// Convert a database template record to the API template format fn template_record_to_api(template: &ContractTypeTemplateRecord) -> ContractTypeTemplate { ContractTypeTemplate { id: template.id.to_string(), name: template.name.clone(), description: template.description.clone().unwrap_or_default(), phases: template.phases.iter().map(|p| p.id.clone()).collect(), default_phase: template.default_phase.clone(), is_builtin: false, } } /// Convert a database template record to the summary format fn template_record_to_summary(template: &ContractTypeTemplateRecord) -> ContractTypeTemplateSummary { ContractTypeTemplateSummary { id: template.id, name: template.name.clone(), description: template.description.clone(), phases: template.phases.clone(), default_phase: template.default_phase.clone(), is_builtin: false, version: template.version, created_at: template.created_at, } }