summaryrefslogtreecommitdiff
path: root/makima/src/server/handlers/contracts.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/server/handlers/contracts.rs')
-rw-r--r--makima/src/server/handlers/contracts.rs130
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
// =============================================================================