7143613ab2
- 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>
263 lines
7.4 KiB
Rust
263 lines
7.4 KiB
Rust
//! 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(),
|
||
}))
|
||
}
|