feat(dönem): dönem açma, açık dönem, düzenleme ve haftalar (backend)
- /api/periods: liste, açık dönem, aç, detay, düzenle - Kurallar: tek açık dönem, çakışma yok, bitiş hariç, en fazla 62 gün, kapalı dönem salt okunur - Tarih daraltılırken dışarıda harcama kalırsa engellenir - Haftalar Pazartesi başlangıçlı, kısmi haftalar ayrı (takvim yardımcıları) - Otomatik dönem adı: "Aralık 2026" / "15 Ara 2026 – 14 Oca 2027" - Testler: takvim (4), dönem kuralları (2), entegrasyon (5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ mod auth;
|
||||
mod catalog;
|
||||
mod health;
|
||||
mod household;
|
||||
mod period;
|
||||
mod spa;
|
||||
|
||||
use axum::{
|
||||
@@ -59,6 +60,9 @@ pub fn router(state: AppState) -> Router {
|
||||
"/payment-methods/{id}",
|
||||
patch(catalog::update_payment_method),
|
||||
)
|
||||
.route("/periods", get(period::list).post(period::create))
|
||||
.route("/periods/current", get(period::current))
|
||||
.route("/periods/{id}", get(period::get).patch(period::update))
|
||||
// Bilinmeyen /api/... → JSON 404 (arayüzün index.html'i dönmesin)
|
||||
.fallback(|| async { AppError::NotFound });
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
//! Dönem uç noktaları (hanenin tüm üyeleri):
|
||||
//!
|
||||
//! | Yöntem | Yol | İş |
|
||||
//! |---|---|---|
|
||||
//! | GET | `/api/periods` | tüm dönemler (en yeni başta) |
|
||||
//! | GET | `/api/periods/current` | açık dönem ya da `null` |
|
||||
//! | POST | `/api/periods` | dönem aç |
|
||||
//! | GET | `/api/periods/{id}` | dönem + haftalar |
|
||||
//! | PATCH | `/api/periods/{id}` | ad / tarihler / birikim hedefi |
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
};
|
||||
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::period::{Period, PeriodStatus},
|
||||
services::{
|
||||
calendar::{days_between, format_date, parse_date, weeks},
|
||||
period::{self, NewPeriod, PeriodChanges},
|
||||
},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
// ---------- İstek / yanıt ----------
|
||||
|
||||
#[derive(Deserialize, Validate, TS)]
|
||||
#[ts(export, rename = "PeriodCreateRequest", optional_fields)]
|
||||
pub struct CreateRequest {
|
||||
/// `"YYYY-MM-DD"`, dahil
|
||||
start_date: String,
|
||||
/// `"YYYY-MM-DD"`, hariç (ör. Aralık için `2027-01-01`)
|
||||
end_date: String,
|
||||
/// Boşsa otomatik ("Aralık 2026")
|
||||
name: Option<String>,
|
||||
/// Kuruş
|
||||
#[ts(type = "number", optional)]
|
||||
savings_goal: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Validate, TS)]
|
||||
#[ts(export, rename = "PeriodUpdateRequest", optional_fields)]
|
||||
pub struct UpdateRequest {
|
||||
name: Option<String>,
|
||||
start_date: Option<String>,
|
||||
end_date: Option<String>,
|
||||
#[ts(type = "number", optional)]
|
||||
savings_goal: Option<i64>,
|
||||
version: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "Period")]
|
||||
pub struct PeriodDto {
|
||||
id: String,
|
||||
name: String,
|
||||
/// Dahil
|
||||
start_date: String,
|
||||
/// Hariç (veritabanındaki değer)
|
||||
end_date: String,
|
||||
/// Son gün, dahil (gösterim için)
|
||||
last_date: String,
|
||||
days: u32,
|
||||
status: PeriodStatus,
|
||||
/// Kuruş
|
||||
#[ts(type = "number")]
|
||||
savings_goal: i64,
|
||||
version: i32,
|
||||
weeks: Vec<WeekDto>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "PeriodWeek")]
|
||||
struct WeekDto {
|
||||
/// 1'den başlar
|
||||
index: u32,
|
||||
start_date: String,
|
||||
/// Dahil
|
||||
last_date: String,
|
||||
days: u32,
|
||||
}
|
||||
|
||||
impl From<Period> for PeriodDto {
|
||||
fn from(p: Period) -> Self {
|
||||
// Veritabanındaki tarihler her zaman doğrulanmış biçimde yazılır
|
||||
let start = parse_date(&p.start_date).expect("geçerli başlangıç tarihi");
|
||||
let end = parse_date(&p.end_date).expect("geçerli bitiş tarihi");
|
||||
Self {
|
||||
id: p.id.to_string(),
|
||||
name: p.name,
|
||||
last_date: format_date(end - time::Duration::days(1)),
|
||||
days: days_between(start, end) as u32,
|
||||
start_date: p.start_date,
|
||||
end_date: p.end_date,
|
||||
status: p.status,
|
||||
savings_goal: p.savings_goal,
|
||||
version: p.version,
|
||||
weeks: weeks(start, end)
|
||||
.into_iter()
|
||||
.map(|w| WeekDto {
|
||||
index: w.index,
|
||||
start_date: format_date(w.start),
|
||||
last_date: format_date(w.last),
|
||||
days: w.days,
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_id(id: &str) -> AppResult<Uuid> {
|
||||
Uuid::parse_str(id).map_err(|_| AppError::NotFound)
|
||||
}
|
||||
|
||||
// ---------- Uç noktalar ----------
|
||||
|
||||
pub async fn list(State(s): State<AppState>, auth: AuthUser) -> AppResult<Json<Vec<PeriodDto>>> {
|
||||
let items = period::list(&s.db, auth.household_id).await?;
|
||||
Ok(Json(items.into_iter().map(PeriodDto::from).collect()))
|
||||
}
|
||||
|
||||
pub async fn current(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
) -> AppResult<Json<Option<PeriodDto>>> {
|
||||
let p = period::current(&s.db, auth.household_id).await?;
|
||||
Ok(Json(p.map(PeriodDto::from)))
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
ValidJson(req): ValidJson<CreateRequest>,
|
||||
) -> AppResult<(StatusCode, Json<PeriodDto>)> {
|
||||
let p = period::create(
|
||||
&s.db,
|
||||
auth.household_id,
|
||||
NewPeriod {
|
||||
start_date: req.start_date,
|
||||
end_date: req.end_date,
|
||||
name: req.name,
|
||||
savings_goal: req.savings_goal,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok((StatusCode::CREATED, Json(p.into())))
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<Json<PeriodDto>> {
|
||||
let p = period::get(&s.db, auth.household_id, parse_id(&id)?).await?;
|
||||
Ok(Json(p.into()))
|
||||
}
|
||||
|
||||
pub async fn update(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
ValidJson(req): ValidJson<UpdateRequest>,
|
||||
) -> AppResult<Json<PeriodDto>> {
|
||||
let p = period::update(
|
||||
&s.db,
|
||||
auth.household_id,
|
||||
parse_id(&id)?,
|
||||
PeriodChanges {
|
||||
name: req.name,
|
||||
start_date: req.start_date,
|
||||
end_date: req.end_date,
|
||||
savings_goal: req.savings_goal,
|
||||
version: req.version,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(p.into()))
|
||||
}
|
||||
Reference in New Issue
Block a user