//! Ay sonu değerlendirmesi (KAKEIBO_PROMPT §8.5, DATA_MODEL.md §5). //! //! Kakeibo'nun dört sorusu — hepsi sorgu anında hesaplanır, saklanmaz: //! 1. Ne kadar param vardı? `available` = gelir − sabit gider − devreden borç + devreden artı //! 2. Ne kadar biriktirmek istedim? `savings_goal` //! 3. Ne kadar harcadım? `spent_total` //! 4. Ne kadar biriktirdim? `realized_savings` = available − spent_total //! //! Ek olarak: grup bazlı planlanan/gerçekleşen, kategori bazlı harcama, haftalık dağılım. //! Kapalı dönemde de okunabilir. use mongodb::{ Database, bson::{Document, Uuid, doc}, }; use serde::Deserialize; use crate::{ db::col, error::AppResult, models::{common::scope, group::Group}, services::budget::{self, GroupBudget, Summary}, }; pub struct Review { pub summary: Summary, /// 1. soru pub available: i64, /// 4. soru (negatifse açık var → kapanışta borç olarak devreder) pub realized_savings: i64, /// Grup: aylık bütçe ve gerçekleşen (`Group::ALL` sırasıyla) pub groups: Vec, /// Harcama olan kategoriler, çoktan aza pub categories: Vec, } pub struct CategorySpent { pub category_id: Uuid, pub group: Group, pub spent: i64, pub count: i64, } pub async fn review(db: &Database, household_id: Uuid, period_id: Uuid) -> AppResult { // `summary` dönemin bu haneye ait olduğunu da denetler (yoksa 404) let summary = budget::summary(db, household_id, period_id).await?; let groups = budget::view(db, household_id, period_id).await?; let categories = spent_by_category(db, household_id, period_id).await?; let available = summary.income_total - summary.fixed_expense_total - summary.carryover_debt + summary.carryover_surplus; Ok(Review { realized_savings: available - summary.spent_total, available, summary, groups, categories, }) } async fn spent_by_category( db: &Database, household_id: Uuid, period_id: Uuid, ) -> AppResult> { #[derive(Deserialize)] struct Key { category_id: Uuid, group: Group, } #[derive(Deserialize)] struct Row { #[serde(rename = "_id")] key: Key, total: i64, count: i64, } let mut m = scope(household_id); m.insert("period_id", period_id); let pipeline = vec![ doc! { "$match": m }, doc! { "$group": { "_id": { "category_id": "$category_id", "group": "$group" }, "total": { "$sum": "$amount" }, "count": { "$sum": 1_i64 }, } }, // Eşit tutarda sıra her seferinde aynı olsun doc! { "$sort": { "total": -1, "_id.category_id": 1 } }, ]; let mut cursor = db .collection::(col::ENTRIES) .aggregate(pipeline) .with_type::() .await?; let mut out = Vec::new(); while cursor.advance().await? { let r = cursor.deserialize_current()?; out.push(CategorySpent { category_id: r.key.category_id, group: r.key.group, spent: r.total, count: r.count, }); } Ok(out) }