Files
kakeibo/backend/src/routes/budget.rs
T
suphicakir 7143613ab2 feat(defter): özet ve bütçeye gerçekleşen harcama eklendi (backend)
- Dönem özeti: spent_total ve remaining (harcanabilir − harcanan)
- Bütçe görünümü: grup ve hafta bazında spent
- Değerler saklanmaz, kayıtlardan anlık toplanır; silinenler sayılmaz

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 11:12:51 +03:00

263 lines
7.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<budget::GroupAmounts> for GroupAmounts {
fn from(a: budget::GroupAmounts) -> Self {
Self {
needs: a[0],
wants: a[1],
culture: a[2],
unexpected: a[3],
}
}
}
impl From<GroupAmounts> 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<WeekBudgetDto>,
}
#[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<GroupBudget> 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<i64>,
}
#[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<Method>,
}
#[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<String>,
amounts: GroupAmounts,
}
fn parse_id(id: &str) -> AppResult<Uuid> {
Uuid::parse_str(id).map_err(|_| AppError::NotFound)
}
pub async fn summary(
State(s): State<AppState>,
auth: AuthUser,
Path(id): Path<String>,
) -> AppResult<Json<SummaryDto>> {
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<AppState>,
auth: AuthUser,
Path(id): Path<String>,
) -> AppResult<Json<Vec<GroupBudgetDto>>> {
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<AppState>,
auth: AuthUser,
Path(id): Path<String>,
ValidJson(req): ValidJson<GroupAmounts>,
) -> AppResult<Json<Vec<GroupBudgetDto>>> {
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<AppState>,
auth: AuthUser,
Path(id): Path<String>,
ValidJson(req): ValidJson<WeekRequest>,
) -> AppResult<Json<Vec<GroupBudgetDto>>> {
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<AppState>,
auth: AuthUser,
Path(id): Path<String>,
Query(q): Query<SuggestQuery>,
) -> AppResult<Json<SuggestionDto>> {
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(),
}))
}