//! 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 / "Nasıl daha iyi…" | //! | POST | `/api/periods/{id}/close` | dönemi kapat: devir + birikime aktarılan (transaction) | //! | POST | `/api/periods/{id}/reopen` | **sahip:** kapalı dönemi düzeltmeye aç (`correcting`) | //! | POST | `/api/periods/{id}/to-savings` | kapanıştaki birikimi TL varlığına ekle (bkz. `routes/asset.rs`) | 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}, closing, 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, /// Kuruş #[ts(type = "number", optional)] savings_goal: Option, } #[derive(Deserialize, Validate, TS)] #[ts(export, rename = "PeriodUpdateRequest", optional_fields)] pub struct UpdateRequest { name: Option, start_date: Option, end_date: Option, #[ts(type = "number", optional)] savings_goal: Option, /// "Nasıl daha iyi yapabilirim?" (Markdown); boş metin → kaldır reflection_md: Option, version: Option, } #[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, /// "Nasıl daha iyi yapabilirim?" (Markdown) reflection_md: Option, /// Kapanışta birikime aktarılan (kuruş); kapanmamışsa `null` #[ts(type = "number | null")] to_savings: Option, /// Kapanış zamanı (ISO 8601) closed_at: Option, version: i32, weeks: Vec, } #[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 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, reflection_md: p.reflection_md, to_savings: p.to_savings, closed_at: p.closed_at.and_then(|d| d.try_to_rfc3339_string().ok()), 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::parse_str(id).map_err(|_| AppError::NotFound) } // ---------- Uç noktalar ---------- pub async fn list(State(s): State, auth: AuthUser) -> AppResult>> { 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, auth: AuthUser, ) -> AppResult>> { let p = period::current(&s.db, auth.household_id).await?; Ok(Json(p.map(PeriodDto::from))) } pub async fn create( State(s): State, auth: AuthUser, ValidJson(req): ValidJson, ) -> AppResult<(StatusCode, Json)> { 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, auth: AuthUser, Path(id): Path, ) -> AppResult> { let p = period::get(&s.db, auth.household_id, parse_id(&id)?).await?; Ok(Json(p.into())) } pub async fn update( State(s): State, auth: AuthUser, Path(id): Path, ValidJson(req): ValidJson, ) -> AppResult> { 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, reflection_md: req.reflection_md, version: req.version, }, ) .await?; Ok(Json(p.into())) } // ---------- Kapatma / yeniden açma ---------- #[derive(Deserialize, Validate, TS)] #[ts(export, rename = "PeriodCloseRequest", optional_fields)] pub struct CloseRequest { /// Birikime aktarılacak (kuruş); verilmezse gerçekleşen birikimin tamamı. Kalanı artı olarak devreder. #[ts(type = "number", optional)] to_savings: Option, version: Option, } #[derive(Serialize, TS)] #[ts(export, rename = "PeriodCloseResult")] pub struct CloseResultDto { period: PeriodDto, /// Değerlendirmenin 4. sorusu (negatifse açık) #[ts(type = "number")] realized_savings: i64, #[ts(type = "number")] to_savings: i64, /// Sonraki döneme devreden; yoksa `null` carryover: Option, /// Birikimlerde bu döneme bağlı kayıt var (yeniden kapatmada yeni tutara güncellendi). /// `false` ve `to_savings > 0` → arayüz "Birikime ekle" önerir. savings_linked: bool, } #[derive(Serialize, TS)] #[ts(export, rename = "Carryover")] struct CarryoverDto { /// `debt` (borç) · `surplus` (artı) #[ts(type = "\"debt\" | \"surplus\"")] kind: &'static str, #[ts(type = "number")] amount: i64, /// Sonraki dönem henüz açılmadıysa `null` (açılınca bağlanır) to_period_id: Option, } pub async fn close( State(s): State, auth: AuthUser, Path(id): Path, ValidJson(req): ValidJson, ) -> AppResult> { let c = closing::close( &s.db, auth.household_id, auth.user_id, parse_id(&id)?, req.to_savings, req.version, ) .await?; Ok(Json(CloseResultDto { period: c.period.into(), realized_savings: c.realized_savings, to_savings: c.to_savings, savings_linked: c.savings_linked, carryover: c.carryover.map(|x| CarryoverDto { kind: x.kind.as_str(), amount: x.amount, to_period_id: x.to_period_id.map(|u| u.to_string()), }), })) } pub async fn reopen( State(s): State, auth: AuthUser, Path(id): Path, ) -> AppResult> { auth.require_owner()?; let p = closing::reopen(&s.db, auth.household_id, auth.user_id, parse_id(&id)?).await?; Ok(Json(p.into())) }