feat(dönem): dönem kapatma, devir ve düzeltme için yeniden açma (backend)
- POST /periods/{id}/close: tek transaction; açık → borç, artı → birikim + devir
- Devir sonraki dönem açılınca ona bağlanır
- POST /periods/{id}/reopen (sahip): düzeltme modu, yeni dönem açık kalır
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -68,6 +68,8 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/periods", get(period::list).post(period::create))
|
||||
.route("/periods/current", get(period::current))
|
||||
.route("/periods/{id}", get(period::get).patch(period::update))
|
||||
.route("/periods/{id}/close", post(period::close))
|
||||
.route("/periods/{id}/reopen", post(period::reopen))
|
||||
.route(
|
||||
"/periods/{id}/plan-items",
|
||||
get(plan::list).post(plan::create),
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
//! | 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 |
|
||||
//! | 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`) |
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -25,6 +27,7 @@ use crate::{
|
||||
models::period::{Period, PeriodStatus},
|
||||
services::{
|
||||
calendar::{days_between, format_date, parse_date, weeks},
|
||||
closing,
|
||||
period::{self, NewPeriod, PeriodChanges},
|
||||
},
|
||||
state::AppState,
|
||||
@@ -77,6 +80,11 @@ pub struct PeriodDto {
|
||||
savings_goal: i64,
|
||||
/// "Nasıl daha iyi yapabilirim?" (Markdown)
|
||||
reflection_md: Option<String>,
|
||||
/// Kapanışta birikime aktarılan (kuruş); kapanmamışsa `null`
|
||||
#[ts(type = "number | null")]
|
||||
to_savings: Option<i64>,
|
||||
/// Kapanış zamanı (ISO 8601)
|
||||
closed_at: Option<String>,
|
||||
version: i32,
|
||||
weeks: Vec<WeekDto>,
|
||||
}
|
||||
@@ -107,6 +115,8 @@ impl From<Period> for PeriodDto {
|
||||
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()
|
||||
@@ -190,3 +200,76 @@ pub async fn update(
|
||||
.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<i64>,
|
||||
version: Option<i32>,
|
||||
}
|
||||
|
||||
#[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<CarryoverDto>,
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
}
|
||||
|
||||
pub async fn close(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
ValidJson(req): ValidJson<CloseRequest>,
|
||||
) -> AppResult<Json<CloseResultDto>> {
|
||||
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,
|
||||
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<AppState>,
|
||||
auth: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<Json<PeriodDto>> {
|
||||
auth.require_owner()?;
|
||||
let p = closing::reopen(&s.db, auth.household_id, auth.user_id, parse_id(&id)?).await?;
|
||||
Ok(Json(p.into()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user