use sqlx::PgPool;
use uuid::Uuid;
use crate::db::repositories::{
actions as actions_repo, assumptions as assumptions_repo, decisions as decisions_repo,
drift as repo, evidence as evidence_repo,
};
use crate::domain::drift::DriftSignal;
use crate::domain::{DriftType, EntityType, Severity};
use crate::error::{AppError, AppResult};
use super::documents::normalize_limit;
pub struct NewDriftSignalInput {
pub drift_type: DriftType,
pub target_entity_id: Uuid,
pub target_entity_type: EntityType,
pub summary: String,
pub severity: Severity,
pub explanation: String,
}
pub async fn create_drift_signal(
pool: &PgPool,
input: NewDriftSignalInput,
) -> AppResult<DriftSignal> {
if input.summary.trim().is_empty() {
return Err(AppError::Validation("summary must not be empty".into()));
}
if input.explanation.trim().is_empty() {
return Err(AppError::Validation("explanation must not be empty".into()));
}
ensure_target_exists(pool, input.target_entity_type, input.target_entity_id).await?;
Ok(repo::create(
pool,
crate::db::repositories::drift::NewDriftSignal {
drift_type: input.drift_type.as_str(),
target_entity_id: input.target_entity_id,
target_entity_type: input.target_entity_type.as_str(),
summary: input.summary.trim(),
severity: input.severity.as_str(),
explanation: input.explanation.trim(),
},
)
.await?)
}
pub async fn list_drift_signals(
pool: &PgPool,
status: Option<&str>,
limit: Option<i64>,
) -> AppResult<Vec<DriftSignal>> {
Ok(repo::list(pool, status, normalize_limit(limit)).await?)
}
pub async fn list_open_drift_signals(
pool: &PgPool,
limit: Option<i64>,
) -> AppResult<Vec<DriftSignal>> {
Ok(repo::list(pool, Some("open"), normalize_limit(limit)).await?)
}
pub async fn get_drift_signal(pool: &PgPool, id: Uuid) -> AppResult<DriftSignal> {
repo::get(pool, id)
.await?
.ok_or_else(|| AppError::NotFound(format!("drift signal {id} not found")))
}
pub async fn accept_drift_signal(pool: &PgPool, id: Uuid) -> AppResult<DriftSignal> {
let signal = set_status(pool, id, "accepted").await?;
if signal.target_entity_type == EntityType::Decision.as_str() {
let _ = decisions_repo::update_status(pool, signal.target_entity_id, "under_review").await;
}
Ok(signal)
}
pub async fn dismiss_drift_signal(pool: &PgPool, id: Uuid) -> AppResult<DriftSignal> {
set_status(pool, id, "dismissed").await
}
async fn set_status(pool: &PgPool, id: Uuid, status: &str) -> AppResult<DriftSignal> {
repo::update_status(pool, id, status)
.await?
.ok_or_else(|| AppError::NotFound(format!("drift signal {id} not found")))
}
async fn ensure_target_exists(
pool: &PgPool,
entity_type: EntityType,
entity_id: Uuid,
) -> AppResult<()> {
let exists = match entity_type {
EntityType::Decision => decisions_repo::get(pool, entity_id).await?.is_some(),
EntityType::Assumption => assumptions_repo::get(pool, entity_id).await?.is_some(),
EntityType::Action => actions_repo::get(pool, entity_id).await?.is_some(),
EntityType::Evidence => evidence_repo::get(pool, entity_id).await?.is_some(),
EntityType::DriftSignal | EntityType::Memo => true,
};
if exists {
Ok(())
} else {
Err(AppError::NotFound(format!(
"{} {entity_id} not found",
entity_type.as_str()
)))
}
}