diff --git a/src/api/identity_api.rs b/src/api/identity_api.rs index 822ddac..cbe5d23 100644 --- a/src/api/identity_api.rs +++ b/src/api/identity_api.rs @@ -15,10 +15,14 @@ pub fn identity_routes() -> Router { .route("/api/v1/people", get(list_people)) .route("/api/v1/people/search", post(search_people)) .route("/api/v1/people/candidates", get(list_candidates)) + .route("/api/v1/people/{identity_id}/confirm-candidate", post(confirm_candidate)) + .route("/api/v1/people/{identity_id}/reject-candidate", post(reject_candidate)) .route("/api/v1/files", get(list_files)) .route("/api/v1/files/{uuid}", get(get_file_detail)) } +// ... (Keep existing functions) ... + // --- People / Identity Endpoints --- #[derive(Debug, Deserialize)] @@ -156,6 +160,50 @@ async fn list_candidates( })) } +// --- Candidate Workflow Endpoints --- + +#[derive(Debug, Deserialize)] +pub struct ConfirmCandidateRequest { + pub pre_chunk_id: i64, +} + +#[derive(Debug, Serialize)] +pub struct ConfirmCandidateResponse { + pub success: bool, + pub message: String, +} + +async fn confirm_candidate( + State(state): State, + Path(identity_id_str): Path, + Json(req): Json, +) -> Result, (StatusCode, String)> { + let identity_id = Uuid::parse_str(&identity_id_str) + .map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?; + + state.db.confirm_candidate(req.pre_chunk_id, identity_id).await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json(ConfirmCandidateResponse { + success: true, + message: "Candidate confirmed and linked to identity".to_string(), + })) +} + +async fn reject_candidate( + State(state): State, + Path(_identity_id_str): Path, // Unused, but consistent with route + Json(req): Json, +) -> Result, (StatusCode, String)> { + state.db.reject_candidate(req.pre_chunk_id).await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json(ConfirmCandidateResponse { + success: true, + message: "Candidate rejected".to_string(), + })) +} + // --- Files Endpoints --- #[derive(Debug, Deserialize)] diff --git a/src/core/db/postgres_db.rs b/src/core/db/postgres_db.rs index d7ba38e..babb424 100644 --- a/src/core/db/postgres_db.rs +++ b/src/core/db/postgres_db.rs @@ -1877,6 +1877,64 @@ impl PostgresDb { Ok(rows) } + pub async fn confirm_candidate( + &self, + pre_chunk_id: i64, + identity_id: Uuid, + ) -> Result<()> { + // 1. Update the pre_chunk to link it to the identity + sqlx::query( + "UPDATE pre_chunks SET identity_id = $1 WHERE id = $2" + ) + .bind(identity_id) + .bind(pre_chunk_id) + .execute(&self.pool) + .await?; + + // 2. Ensure a link exists in file_identities table + // We need the file_uuid from the pre_chunk + let file_uuid: Option = sqlx::query_scalar( + "SELECT file_uuid FROM pre_chunks WHERE id = $1" + ) + .bind(pre_chunk_id) + .fetch_optional(&self.pool) + .await?; + + if let Some(f_uuid) = file_uuid { + // Check if relationship exists + let exists: bool = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM file_identities WHERE file_uuid = $1 AND identity_id = $2)" + ) + .bind(f_uuid) + .bind(identity_id) + .fetch_one(&self.pool) + .await?; + + if !exists { + sqlx::query( + "INSERT INTO file_identities (file_uuid, identity_id, status) VALUES ($1, $2, 'detected')" + ) + .bind(f_uuid) + .bind(identity_id) + .execute(&self.pool) + .await?; + } + } + + Ok(()) + } + + pub async fn reject_candidate(&self, pre_chunk_id: i64) -> Result<()> { + // Just ensure it is NULL (or maybe we mark it as ignored in metadata? For now, just NULL) + sqlx::query( + "UPDATE pre_chunks SET identity_id = NULL WHERE id = $1" + ) + .bind(pre_chunk_id) + .execute(&self.pool) + .await?; + Ok(()) + } + pub async fn store_chunk(&self, chunk: &Chunk) -> Result<()> { let table = schema::table_name("chunks"); let content_with_rule = serde_json::json!({