use uuid::Uuid; use crate::db::models::{ContactCandidate, NewContactCandidate}; use crate::db::{Db, Page, Pagination, db_error}; use crate::error::AppResult; pub async fn upsert(db: &Db, input: &NewContactCandidate) -> AppResult { let client = db.client().await?; let row = client .query_one( "INSERT INTO contact_candidates ( run_id, interviewee_id, origin_id, crawl_page_id, contact_type, raw_value, normalized_value, proposed_relationship_kind, proposed_label, evidence, confidence, ai_call_id ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12) ON CONFLICT (run_id, interviewee_id, origin_id, contact_type, normalized_value) DO UPDATE SET raw_value = EXCLUDED.raw_value, proposed_relationship_kind = COALESCE(EXCLUDED.proposed_relationship_kind, contact_candidates.proposed_relationship_kind), proposed_label = COALESCE(EXCLUDED.proposed_label, contact_candidates.proposed_label), evidence = CASE WHEN EXCLUDED.confidence >= contact_candidates.confidence THEN EXCLUDED.evidence ELSE contact_candidates.evidence END, confidence = GREATEST(contact_candidates.confidence, EXCLUDED.confidence), ai_call_id = COALESCE(EXCLUDED.ai_call_id, contact_candidates.ai_call_id) RETURNING *", &[ &input.run_id, &input.interviewee_id, &input.origin_id, &input.crawl_page_id, &input.contact_type, &input.raw_value, &input.normalized_value, &input.proposed_relationship_kind, &input.proposed_label, &input.evidence, &input.confidence, &input.ai_call_id, ], ) .await .map_err(db_error)?; ContactCandidate::from_row(&row).map_err(db_error) } pub async fn get(db: &Db, id: Uuid) -> AppResult> { let client = db.client().await?; client .query_opt("SELECT * FROM contact_candidates WHERE id = $1", &[&id]) .await .map_err(db_error)? .map(|row| ContactCandidate::from_row(&row)) .transpose() .map_err(db_error) } pub async fn list_for_review( db: &Db, run_id: Option, interviewee_id: Option, status: Option<&str>, pagination: Pagination, ) -> AppResult> { let client = db.client().await?; let predicate = "($1::uuid IS NULL OR run_id = $1) AND ($2::uuid IS NULL OR interviewee_id = $2) AND ($3::text IS NULL OR status = $3)"; let total_sql = format!("SELECT count(*)::bigint FROM contact_candidates WHERE {predicate}"); let total: i64 = client .query_one(&total_sql, &[&run_id, &interviewee_id, &status]) .await .map_err(db_error)? .get(0); let list_sql = format!( "SELECT * FROM contact_candidates WHERE {predicate} ORDER BY CASE status WHEN 'needs_review' THEN 0 WHEN 'pending' THEN 1 ELSE 2 END, confidence DESC, created_at LIMIT $4 OFFSET $5" ); let rows = client .query( &list_sql, &[ &run_id, &interviewee_id, &status, &pagination.limit, &pagination.offset, ], ) .await .map_err(db_error)?; let items = rows .iter() .map(ContactCandidate::from_row) .collect::, _>>() .map_err(db_error)?; Ok(Page::new(items, total, pagination)) } pub async fn decide( db: &Db, id: Uuid, status: &str, accepted_contact_id: Option, rejection_reason: Option<&str>, ) -> AppResult> { let client = db.client().await?; client .query_opt( "UPDATE contact_candidates SET status = $2, accepted_contact_id = $3, rejection_reason = $4 WHERE id = $1 RETURNING *", &[&id, &status, &accepted_contact_id, &rejection_reason], ) .await .map_err(db_error)? .map(|row| ContactCandidate::from_row(&row)) .transpose() .map_err(db_error) }