//! Bütçe ve özet uç noktaları: //! //! | Yöntem | Yol | İş | //! |---|---|---| //! | GET | `/api/periods/{id}/summary` | gelir/gider/birikim/devir → harcanabilir, harcanan, kalan | //! | GET | `/api/periods/{id}/budget` | 4 grup: aylık + haftalık döküm, her birinde harcanan | //! | PUT | `/api/periods/{id}/budget` | 4 grubun aylık tutarları | //! | PUT | `/api/periods/{id}/budget/weeks` | bir haftayı elle ayarla / otomatiğe döndür | //! | GET | `/api/periods/{id}/budget/suggestion?method=…` | `default` · `previous` · `avg3` | use axum::{ Json, extract::{Path, Query, State}, }; use mongodb::bson::Uuid; use serde::{Deserialize, Serialize}; use ts_rs::TS; use validator::Validate; use crate::{ auth::AuthUser, error::{AppError, AppResult}, extract::ValidJson, models::group::Group, services::budget::{self, GroupBudget, SuggestMethod}, state::AppState, }; /// Gruplara göre tutarlar (kuruş) #[derive(Serialize, Deserialize, Validate, TS, Clone, Copy)] #[ts(export)] pub struct GroupAmounts { #[ts(type = "number")] needs: i64, #[ts(type = "number")] wants: i64, #[ts(type = "number")] culture: i64, #[ts(type = "number")] unexpected: i64, } impl From for GroupAmounts { fn from(a: budget::GroupAmounts) -> Self { Self { needs: a[0], wants: a[1], culture: a[2], unexpected: a[3], } } } impl From for budget::GroupAmounts { fn from(a: GroupAmounts) -> Self { [a.needs, a.wants, a.culture, a.unexpected] } } #[derive(Serialize, TS)] #[ts(export, rename = "PeriodSummary")] pub struct SummaryDto { #[ts(type = "number")] income_total: i64, #[ts(type = "number")] fixed_expense_total: i64, #[ts(type = "number")] savings_goal: i64, /// Önceki dönemden devreden borç #[ts(type = "number")] carryover_debt: i64, /// Önceki dönemden devreden artı bakiye #[ts(type = "number")] carryover_surplus: i64, /// gelir − sabit gider − birikim − borç + artı #[ts(type = "number")] spendable: i64, /// 4 grubun aylık bütçe toplamı #[ts(type = "number")] budget_total: i64, /// harcanabilir − dağıtılan (negatifse fazla dağıtılmış) #[ts(type = "number")] unallocated: i64, /// Dönemde gerçekleşen harcama #[ts(type = "number")] spent_total: i64, /// Kalan harcanabilir: harcanabilir − gerçekleşen (negatifse aşılmış) #[ts(type = "number")] remaining: i64, } #[derive(Serialize, TS)] #[ts(export, rename = "GroupBudget")] pub struct GroupBudgetDto { group: Group, #[ts(type = "number")] monthly: i64, /// Dönem boyunca gerçekleşen harcama #[ts(type = "number")] spent: i64, /// Elle girilen haftaların toplamı aylık tutarı aşıyor weekly_over: bool, weeks: Vec, } #[derive(Serialize, TS)] #[ts(export, rename = "WeekBudget")] struct WeekBudgetDto { index: u32, start_date: String, last_date: String, days: u32, #[ts(type = "number")] amount: i64, /// Elle girildi (otomatik değil) is_manual: bool, /// Bu haftada gerçekleşen harcama #[ts(type = "number")] spent: i64, } impl From for GroupBudgetDto { fn from(g: GroupBudget) -> Self { Self { group: g.group, monthly: g.monthly, spent: g.spent, weekly_over: g.weekly_over, weeks: g .weeks .into_iter() .map(|w| WeekBudgetDto { index: w.index, start_date: w.start_date, last_date: w.last_date, days: w.days, amount: w.amount, is_manual: w.is_manual, spent: w.spent, }) .collect(), } } } #[derive(Deserialize, Validate, TS)] #[ts(export, rename = "BudgetWeekRequest")] pub struct WeekRequest { group: Group, /// Haftanın ilk günü (`PeriodWeek.start_date`) week_start: String, /// Kuruş; `null` → otomatiğe döndür #[ts(type = "number | null")] amount: Option, } #[derive(Deserialize, Clone, Copy, TS)] #[serde(rename_all = "lowercase")] #[ts(export, rename = "SuggestMethod")] pub enum Method { Default, Previous, Avg3, } #[derive(Deserialize)] pub struct SuggestQuery { method: Option, } #[derive(Serialize, TS)] #[ts(export, rename = "BudgetSuggestion")] pub struct SuggestionDto { /// `false` → bu yöntem için veri yok (`message` nedenini söyler) available: bool, message: Option, amounts: GroupAmounts, } fn parse_id(id: &str) -> AppResult { Uuid::parse_str(id).map_err(|_| AppError::NotFound) } pub async fn summary( State(s): State, auth: AuthUser, Path(id): Path, ) -> AppResult> { let x = budget::summary(&s.db, auth.household_id, parse_id(&id)?).await?; Ok(Json(SummaryDto { income_total: x.income_total, fixed_expense_total: x.fixed_expense_total, savings_goal: x.savings_goal, carryover_debt: x.carryover_debt, carryover_surplus: x.carryover_surplus, spendable: x.spendable, budget_total: x.budget_total, unallocated: x.spendable - x.budget_total, spent_total: x.spent_total, remaining: x.spendable - x.spent_total, })) } pub async fn get( State(s): State, auth: AuthUser, Path(id): Path, ) -> AppResult>> { let groups = budget::view(&s.db, auth.household_id, parse_id(&id)?).await?; Ok(Json(groups.into_iter().map(GroupBudgetDto::from).collect())) } pub async fn set_monthly( State(s): State, auth: AuthUser, Path(id): Path, ValidJson(req): ValidJson, ) -> AppResult>> { let pid = parse_id(&id)?; budget::set_monthly(&s.db, auth.household_id, pid, req.into()).await?; let groups = budget::view(&s.db, auth.household_id, pid).await?; Ok(Json(groups.into_iter().map(GroupBudgetDto::from).collect())) } pub async fn set_week( State(s): State, auth: AuthUser, Path(id): Path, ValidJson(req): ValidJson, ) -> AppResult>> { let pid = parse_id(&id)?; budget::set_week( &s.db, auth.household_id, pid, req.group, &req.week_start, req.amount, ) .await?; let groups = budget::view(&s.db, auth.household_id, pid).await?; Ok(Json(groups.into_iter().map(GroupBudgetDto::from).collect())) } pub async fn suggestion( State(s): State, auth: AuthUser, Path(id): Path, Query(q): Query, ) -> AppResult> { let method = match q.method.unwrap_or(Method::Default) { Method::Default => SuggestMethod::Default, Method::Previous => SuggestMethod::Previous, Method::Avg3 => SuggestMethod::Avg3, }; let x = budget::suggest(&s.db, auth.household_id, parse_id(&id)?, method).await?; Ok(Json(SuggestionDto { available: x.available, message: x.message, amounts: x.amounts.into(), })) }