//! Dönem notları (KAKEIBO_PROMPT §8.6): başlık + Markdown metin, `planning` ya da `review`. use mongodb::{ Database, bson::{Bson, Document, Uuid, doc}, options::ReturnDocument, }; use crate::{ db::col, error::{AppError, AppResult}, models::{ common::{new_meta, now, scope}, note::{Note, NoteContext}, }, services::period, }; pub const MAX_BODY: usize = 20_000; fn coll(db: &Database) -> mongodb::Collection { db.collection(col::NOTES) } /// Dönemin notları (isteğe bağlı sayfa filtresiyle), en yeni değişen başta. pub async fn list( db: &Database, household_id: Uuid, period_id: Uuid, context: Option, ) -> AppResult> { period::get(db, household_id, period_id).await?; let mut f = scope(household_id); f.insert("period_id", period_id); if let Some(c) = context { f.insert("context", c.as_str()); } let mut cursor = coll(db).find(f).sort(doc! { "updated_at": -1 }).await?; let mut out = Vec::new(); while cursor.advance().await? { out.push(cursor.deserialize_current()?); } Ok(out) } pub async fn create( db: &Database, household_id: Uuid, user_id: Uuid, period_id: Uuid, context: NoteContext, title: &str, body_md: &str, ) -> AppResult { period::get_open(db, household_id, period_id).await?; let mut d = new_meta(household_id); let id = d.get("_id").cloned().unwrap_or(Bson::Null); d.insert("period_id", period_id); d.insert("context", context.as_str()); d.insert("title", clean_title(title)?); d.insert("body_md", clean_body(body_md)?); d.insert("created_by", user_id); db.collection::(col::NOTES).insert_one(d).await?; coll(db) .find_one(doc! { "_id": id }) .await? .ok_or_else(|| AppError::Internal("eklenen not okunamadı".into())) } pub async fn update( db: &Database, household_id: Uuid, id: Uuid, title: Option<&str>, body_md: Option<&str>, version: Option, ) -> AppResult { let current = get(db, household_id, id).await?; period::get_open(db, household_id, current.period_id).await?; if version.is_some_and(|v| v != current.version) { return Err(stale()); } let mut set = doc! { "updated_at": now() }; if let Some(t) = title { set.insert("title", clean_title(t)?); } if let Some(b) = body_md { set.insert("body_md", clean_body(b)?); } let mut f = scope(household_id); f.insert("_id", id); f.insert("version", current.version); coll(db) .find_one_and_update(f, doc! { "$set": set, "$inc": { "version": 1 } }) .return_document(ReturnDocument::After) .await? .ok_or_else(stale) } pub async fn delete(db: &Database, household_id: Uuid, id: Uuid) -> AppResult<()> { let current = get(db, household_id, id).await?; period::get_open(db, household_id, current.period_id).await?; let ts = now(); let mut f = scope(household_id); f.insert("_id", id); db.collection::(col::NOTES) .update_one( f, doc! { "$set": { "deleted_at": ts, "updated_at": ts }, "$inc": { "version": 1 } }, ) .await?; Ok(()) } async fn get(db: &Database, household_id: Uuid, id: Uuid) -> AppResult { let mut f = scope(household_id); f.insert("_id", id); coll(db).find_one(f).await?.ok_or(AppError::NotFound) } fn clean_title(t: &str) -> AppResult { let t = t.trim(); match t.chars().count() { 0 => Err(AppError::BadRequest("Not başlığı boş olamaz".into())), 1..=120 => Ok(t.to_string()), _ => Err(AppError::BadRequest( "Not başlığı en fazla 120 karakter olabilir".into(), )), } } fn clean_body(b: &str) -> AppResult { if b.chars().count() > MAX_BODY { return Err(AppError::BadRequest(format!( "Not metni en fazla {MAX_BODY} karakter olabilir" ))); } Ok(b.to_string()) } fn stale() -> AppError { AppError::Conflict("Not başka biri tarafından değiştirildi, sayfayı yenileyin".into()) }