diff options
| author | soryu <soryu@soryu.co> | 2026-01-26 20:19:30 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-26 20:19:30 +0000 |
| commit | 04e1e8f0dd85d19917ac5ba0b73cba65ebac8976 (patch) | |
| tree | e52537dd2a33c10156f1378ffdc6803bc983482d /makima/src/server/handlers/contracts.rs | |
| parent | 6328477bc459eca0243b685553dbd75b925fdc8a (diff) | |
| download | soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.tar.gz soryu-04e1e8f0dd85d19917ac5ba0b73cba65ebac8976.zip | |
Add completion phases
Diffstat (limited to 'makima/src/server/handlers/contracts.rs')
| -rw-r--r-- | makima/src/server/handlers/contracts.rs | 130 |
1 files changed, 130 insertions, 0 deletions
diff --git a/makima/src/server/handlers/contracts.rs b/makima/src/server/handlers/contracts.rs index f16f33d..de3164c 100644 --- a/makima/src/server/handlers/contracts.rs +++ b/makima/src/server/handlers/contracts.rs @@ -6,6 +6,8 @@ use axum::{ response::IntoResponse, Json, }; +use serde::Deserialize; +use utoipa::ToSchema; use uuid::Uuid; use crate::db::models::{ @@ -1423,6 +1425,134 @@ pub async fn change_phase( } // ============================================================================= +// Deliverables +// ============================================================================= + +/// Request body for marking a deliverable complete +#[derive(Debug, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MarkDeliverableRequest { + /// The deliverable ID to mark as complete (e.g., 'plan-document', 'pull-request') + pub deliverable_id: String, + /// Phase the deliverable belongs to. Defaults to current contract phase if not specified. + pub phase: Option<String>, +} + +/// Mark a deliverable as complete for a contract phase. +#[utoipa::path( + post, + path = "/api/v1/contracts/{id}/deliverables/complete", + params( + ("id" = Uuid, Path, description = "Contract ID") + ), + request_body = MarkDeliverableRequest, + responses( + (status = 200, description = "Deliverable marked complete", body = serde_json::Value), + (status = 400, description = "Invalid deliverable ID", body = ApiError), + (status = 401, description = "Unauthorized", body = ApiError), + (status = 404, description = "Contract not found", body = ApiError), + (status = 503, description = "Database not configured", body = ApiError), + (status = 500, description = "Internal server error", body = ApiError), + ), + security( + ("bearer_auth" = []), + ("api_key" = []) + ), + tag = "Contracts" +)] +pub async fn mark_deliverable_complete( + State(state): State<SharedState>, + Authenticated(auth): Authenticated, + Path(id): Path<Uuid>, + Json(req): Json<MarkDeliverableRequest>, +) -> impl IntoResponse { + let Some(ref pool) = state.db_pool else { + return ( + StatusCode::SERVICE_UNAVAILABLE, + Json(ApiError::new("DB_UNAVAILABLE", "Database not configured")), + ) + .into_response(); + }; + + // Get contract + let contract = match repository::get_contract_for_owner(pool, id, auth.owner_id).await { + Ok(Some(c)) => c, + Ok(None) => { + return ( + StatusCode::NOT_FOUND, + Json(ApiError::new("NOT_FOUND", "Contract not found")), + ) + .into_response(); + } + Err(e) => { + tracing::error!("Failed to get contract {}: {}", id, e); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response(); + } + }; + + // Use specified phase or default to current contract phase + let target_phase = req.phase.unwrap_or_else(|| contract.phase.clone()); + + // Validate the deliverable ID exists for this phase/contract type + let phase_deliverables = crate::llm::get_phase_deliverables_for_type(&target_phase, &contract.contract_type); + let deliverable = phase_deliverables.deliverables.iter().find(|d| d.id == req.deliverable_id); + + if deliverable.is_none() { + let valid_ids: Vec<&str> = phase_deliverables.deliverables.iter().map(|d| d.id.as_str()).collect(); + return ( + StatusCode::BAD_REQUEST, + Json(ApiError::new( + "INVALID_DELIVERABLE", + format!( + "Invalid deliverable_id '{}' for {} phase (contract type: {}). Valid IDs: {:?}", + req.deliverable_id, target_phase, contract.contract_type, valid_ids + ), + )), + ) + .into_response(); + } + + // Check if already completed + if contract.is_deliverable_complete(&target_phase, &req.deliverable_id) { + return Json(serde_json::json!({ + "success": true, + "message": format!("Deliverable '{}' is already marked complete for {} phase", req.deliverable_id, target_phase), + "deliverableId": req.deliverable_id, + "phase": target_phase, + "alreadyComplete": true, + })) + .into_response(); + } + + // Mark the deliverable as complete + match repository::mark_deliverable_complete(pool, id, &target_phase, &req.deliverable_id).await { + Ok(updated_contract) => { + let completed = updated_contract.get_completed_deliverables(&target_phase); + Json(serde_json::json!({ + "success": true, + "message": format!("Marked deliverable '{}' as complete for {} phase", req.deliverable_id, target_phase), + "deliverableId": req.deliverable_id, + "phase": target_phase, + "completedDeliverables": completed, + })) + .into_response() + } + Err(e) => { + tracing::error!("Failed to mark deliverable complete for contract {}: {}", id, e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ApiError::new("DB_ERROR", e.to_string())), + ) + .into_response() + } + } +} + +// ============================================================================= // Events // ============================================================================= |
