use sqlx::PgPool;
use uuid::Uuid;
use crate::db::repositories::memos as repo;
use crate::domain::memos::Memo;
use crate::error::{AppError, AppResult};
use super::documents::normalize_limit;
use super::drift;
pub const MEMO_STATUSES: [&str; 2] = ["draft", "final"];
pub async fn create_memo(
pool: &PgPool,
memo_type: &str,
title: &str,
body_markdown: &str,
status: Option<&str>,
) -> AppResult<Memo> {
let memo_type = memo_type.trim();
let title = title.trim();
let status = status.unwrap_or("draft").trim();
if memo_type.is_empty() {
return Err(AppError::Validation("memo_type must not be empty".into()));
}
if title.is_empty() {
return Err(AppError::Validation("title must not be empty".into()));
}
if body_markdown.trim().is_empty() {
return Err(AppError::Validation("body_markdown must not be empty".into()));
}
validate_status(status)?;
Ok(repo::create(
pool,
repo::NewMemo {
title,
memo_type,
body_markdown,
status,
},
)
.await?)
}
pub async fn update_memo(
pool: &PgPool,
id: Uuid,
title: Option<&str>,
memo_type: Option<&str>,
body_markdown: Option<&str>,
status: Option<&str>,
) -> AppResult<Memo> {
let current = get_memo(pool, id).await?;
let title = title.map(str::trim).unwrap_or(¤t.title);
let memo_type = memo_type.map(str::trim).unwrap_or(¤t.memo_type);
let body_markdown = body_markdown
.map(str::trim)
.unwrap_or(¤t.body_markdown);
let status = status.map(str::trim).unwrap_or(¤t.status);
if title.is_empty() {
return Err(AppError::Validation("title must not be empty".into()));
}
if memo_type.is_empty() {
return Err(AppError::Validation("memo_type must not be empty".into()));
}
if body_markdown.is_empty() {
return Err(AppError::Validation("body_markdown must not be empty".into()));
}
validate_status(status)?;
repo::update(
pool,
id,
repo::MemoUpdate {
title,
memo_type,
body_markdown,
status,
},
)
.await?
.ok_or_else(|| AppError::NotFound(format!("memo {id} not found")))
}
pub async fn list_memos(pool: &PgPool, limit: Option<i64>) -> AppResult<Vec<Memo>> {
Ok(repo::list(pool, normalize_limit(limit)).await?)
}
pub async fn get_memo(pool: &PgPool, id: Uuid) -> AppResult<Memo> {
repo::get(pool, id)
.await?
.ok_or_else(|| AppError::NotFound(format!("memo {id} not found")))
}
pub async fn create_memo_from_drift(
pool: &PgPool,
drift_signal_id: Uuid,
) -> AppResult<Memo> {
let signal = drift::get_drift_signal(pool, drift_signal_id).await?;
let title = format!("Drift memo: {}", signal.summary);
let body = format!(
"# {title}\n\n\
## Summary\n{summary}\n\n\
## Drift signal\n- Type: {drift_type}\n- Severity: {severity}\n- Target: {target_type} ({target_id})\n\n\
## Explanation\n{explanation}\n\n\
## Relevant decision\n_TODO: describe the affected decision._\n\n\
## Recommended next actions\n_TODO: list concrete actions._\n\n\
## Unknowns\n_TODO: list missing information or open questions._\n",
title = title,
summary = signal.summary,
drift_type = signal.drift_type,
severity = signal.severity,
target_type = signal.target_entity_type,
target_id = signal.target_entity_id,
explanation = signal.explanation,
);
create_memo(pool, "drift_memo", &title, &body, Some("draft")).await
}
fn validate_status(status: &str) -> AppResult<()> {
if MEMO_STATUSES.contains(&status) {
Ok(())
} else {
Err(AppError::Validation(format!(
"invalid memo status `{status}`; expected one of: {}",
MEMO_STATUSES.join(", ")
)))
}
}