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:
@@ -313,3 +313,14 @@ Content-Type: application/json
|
||||
{
|
||||
"reflection_md": "- Dışarıda yemeği haftada 1'e indir\n- Market listesiyle alışveriş"
|
||||
}
|
||||
|
||||
### Dönemi kapat: to_savings verilmezse gerçekleşen birikimin tamamı birikime; kalanı artı, açık varsa borç devreder
|
||||
POST {{baseUrl}}/periods/BURAYA-DONEM-ID/close
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"to_savings": 500000
|
||||
}
|
||||
|
||||
### Kapalı dönemi düzeltmeye aç (yalnızca hane sahibi) → status: correcting; düzeltince yeniden "close"
|
||||
POST {{baseUrl}}/periods/BURAYA-DONEM-ID/reopen
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! `periods` koleksiyonu (DATA_MODEL.md §3.4).
|
||||
|
||||
use mongodb::bson::Uuid;
|
||||
use mongodb::bson::{DateTime, Uuid};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
@@ -10,8 +10,12 @@ use ts_rs::TS;
|
||||
pub enum PeriodStatus {
|
||||
/// Girişler yapılabilir. Hanede en fazla bir tane.
|
||||
Open,
|
||||
/// Salt okunur (Faz 6: kapanış sihirbazı, sahip yeniden açabilir).
|
||||
/// Salt okunur. Sahip yeniden açarsa `Correcting` olur.
|
||||
Closed,
|
||||
/// Düzeltmede: kapanmış dönem sahip tarafından düzeltme için açıldı.
|
||||
/// Yazılabilir, ama "açık dönem" sayılmaz (yeni dönem açıkken de olabilir).
|
||||
/// "Yeniden kapat" ile devir yeniden hesaplanır.
|
||||
Correcting,
|
||||
}
|
||||
|
||||
impl PeriodStatus {
|
||||
@@ -19,8 +23,14 @@ impl PeriodStatus {
|
||||
match self {
|
||||
PeriodStatus::Open => "open",
|
||||
PeriodStatus::Closed => "closed",
|
||||
PeriodStatus::Correcting => "correcting",
|
||||
}
|
||||
}
|
||||
|
||||
/// Kayıt eklenip değiştirilebilir mi?
|
||||
pub fn is_writable(self) -> bool {
|
||||
self != PeriodStatus::Closed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@@ -38,5 +48,10 @@ pub struct Period {
|
||||
/// "Nasıl daha iyi yapabilirim?" (Markdown, Değerlendirme sayfası)
|
||||
#[serde(default)]
|
||||
pub reflection_md: Option<String>,
|
||||
/// Kapanışta birikime aktarılan tutar (kuruş). Faz 8'de varlık hareketine dönüşecek.
|
||||
#[serde(default)]
|
||||
pub to_savings: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub closed_at: Option<DateTime>,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
//! Dönem kapatma ve yeniden açma (KAKEIBO_PROMPT §6, DATA_MODEL.md §3.10, §6).
|
||||
//!
|
||||
//! Kapatma (tek transaction):
|
||||
//! 1. Dönem `closed` olur (`closed_at`, `closed_by`, `to_savings`).
|
||||
//! 2. Bu dönemden önceki devirler (yeniden kapatmada) soft delete edilir.
|
||||
//! 3. Gerçekleşen birikim (değerlendirmenin 4. sorusu) dağıtılır:
|
||||
//! - < 0 → açık **borç** olarak devreder (`debt`).
|
||||
//! - ≥ 0 → `to_savings` kadarı birikime (varsayılan: tamamı), kalanı **artı** olarak devreder (`surplus`).
|
||||
//! 4. Devir sonraki döneme bağlanır; sonraki dönem henüz yoksa `to_period_id: null` kalır
|
||||
//! ve o dönem açılınca bağlanır (`period::create`).
|
||||
//!
|
||||
//! Yeniden açma (yalnızca sahip): dönem `correcting` olur, yeni açık dönem varsa o da açık kalır.
|
||||
//! Mevcut devir, "yeniden kapat" denene kadar yerinde kalır (sonraki dönemin rakamları oynamaz).
|
||||
//! Ardından kapanmış dönem olan bir dönem açılamaz: sonraki devirler zincirleme bozulurdu.
|
||||
|
||||
use mongodb::{
|
||||
Database,
|
||||
bson::{Bson, Document, Uuid, doc},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
db::col,
|
||||
error::{AppError, AppResult},
|
||||
models::{
|
||||
common::{new_meta, now, scope},
|
||||
period::{Period, PeriodStatus},
|
||||
},
|
||||
services::{period, review},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CarryKind {
|
||||
Debt,
|
||||
Surplus,
|
||||
}
|
||||
|
||||
impl CarryKind {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
CarryKind::Debt => "debt",
|
||||
CarryKind::Surplus => "surplus",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Carry {
|
||||
pub kind: CarryKind,
|
||||
pub amount: i64,
|
||||
pub to_period_id: Option<Uuid>,
|
||||
}
|
||||
|
||||
pub struct Closing {
|
||||
pub period: Period,
|
||||
pub realized_savings: i64,
|
||||
pub to_savings: i64,
|
||||
pub carryover: Option<Carry>,
|
||||
}
|
||||
|
||||
/// Gerçekleşen birikimi birikim / devir olarak böler. `to_savings` verilmezse tamamı birikime.
|
||||
pub fn settle(
|
||||
realized: i64,
|
||||
to_savings: Option<i64>,
|
||||
) -> AppResult<(i64, Option<(CarryKind, i64)>)> {
|
||||
if realized < 0 {
|
||||
if to_savings.unwrap_or(0) != 0 {
|
||||
return Err(AppError::BadRequest(
|
||||
"Bu dönemde birikim yok; açık, borç olarak sonraki döneme devreder".into(),
|
||||
));
|
||||
}
|
||||
return Ok((0, Some((CarryKind::Debt, -realized))));
|
||||
}
|
||||
let t = to_savings.unwrap_or(realized);
|
||||
if !(0..=realized).contains(&t) {
|
||||
return Err(AppError::BadRequest(
|
||||
"Birikime aktarılacak tutar 0 ile gerçekleşen birikim arasında olmalı".into(),
|
||||
));
|
||||
}
|
||||
let surplus = realized - t;
|
||||
Ok((t, (surplus > 0).then_some((CarryKind::Surplus, surplus))))
|
||||
}
|
||||
|
||||
pub async fn close(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
user_id: Uuid,
|
||||
period_id: Uuid,
|
||||
to_savings: Option<i64>,
|
||||
version: Option<i32>,
|
||||
) -> AppResult<Closing> {
|
||||
let p = period::get(db, household_id, period_id).await?;
|
||||
if p.status == PeriodStatus::Closed {
|
||||
return Err(AppError::Conflict("Bu dönem zaten kapalı".into()));
|
||||
}
|
||||
if version.is_some_and(|v| v != p.version) {
|
||||
return Err(period::stale());
|
||||
}
|
||||
let realized = review::review(db, household_id, period_id)
|
||||
.await?
|
||||
.realized_savings;
|
||||
let (to_savings, carry) = settle(realized, to_savings)?;
|
||||
let next = next_period(db, household_id, &p).await?;
|
||||
let ts = now();
|
||||
|
||||
let mut session = db.client().start_session().await?;
|
||||
session.start_transaction().await?;
|
||||
let result = async {
|
||||
let mut f = scope(household_id);
|
||||
f.insert("_id", period_id);
|
||||
f.insert("version", p.version);
|
||||
let r = db
|
||||
.collection::<Document>(col::PERIODS)
|
||||
.update_one(
|
||||
f,
|
||||
doc! {
|
||||
"$set": {
|
||||
"status": PeriodStatus::Closed.as_str(),
|
||||
"closed_at": ts,
|
||||
"closed_by": user_id,
|
||||
"to_savings": to_savings,
|
||||
"updated_at": ts,
|
||||
},
|
||||
"$inc": { "version": 1 },
|
||||
},
|
||||
)
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
if r.matched_count == 0 {
|
||||
return Err(period::stale());
|
||||
}
|
||||
|
||||
let mut old = scope(household_id);
|
||||
old.insert("from_period_id", period_id);
|
||||
db.collection::<Document>(col::CARRYOVERS)
|
||||
.update_many(
|
||||
old,
|
||||
doc! { "$set": { "deleted_at": ts, "updated_at": ts }, "$inc": { "version": 1 } },
|
||||
)
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
|
||||
if let Some((kind, amount)) = carry {
|
||||
let mut d = new_meta(household_id);
|
||||
d.insert("from_period_id", period_id);
|
||||
d.insert(
|
||||
"to_period_id",
|
||||
next.as_ref().map_or(Bson::Null, |n| n.id.into()),
|
||||
);
|
||||
d.insert("kind", kind.as_str());
|
||||
d.insert("amount", amount);
|
||||
db.collection::<Document>(col::CARRYOVERS)
|
||||
.insert_one(d)
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
match result {
|
||||
Ok(()) => session.commit_transaction().await?,
|
||||
Err(e) => {
|
||||
let _ = session.abort_transaction().await;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Closing {
|
||||
period: period::get(db, household_id, period_id).await?,
|
||||
realized_savings: realized,
|
||||
to_savings,
|
||||
carryover: carry.map(|(kind, amount)| Carry {
|
||||
kind,
|
||||
amount,
|
||||
to_period_id: next.map(|n| n.id),
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
/// Kapalı dönemi düzeltme için açar (yalnızca sahip — rota denetler).
|
||||
pub async fn reopen(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
user_id: Uuid,
|
||||
period_id: Uuid,
|
||||
) -> AppResult<Period> {
|
||||
let p = period::get(db, household_id, period_id).await?;
|
||||
if p.status != PeriodStatus::Closed {
|
||||
return Err(AppError::Conflict(
|
||||
"Yalnızca kapalı bir dönem yeniden açılabilir".into(),
|
||||
));
|
||||
}
|
||||
let mut later = scope(household_id);
|
||||
later.insert("start_date", doc! { "$gte": &p.end_date });
|
||||
later.insert("status", doc! { "$ne": PeriodStatus::Open.as_str() });
|
||||
if db
|
||||
.collection::<Document>(col::PERIODS)
|
||||
.find_one(later)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(AppError::Conflict(
|
||||
"Bu dönemden sonra kapanmış bir dönem var. Yalnızca en son kapanan dönem yeniden açılabilir."
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
|
||||
let ts = now();
|
||||
let mut f = scope(household_id);
|
||||
f.insert("_id", period_id);
|
||||
f.insert("version", p.version);
|
||||
let r = db
|
||||
.collection::<Document>(col::PERIODS)
|
||||
.update_one(
|
||||
f,
|
||||
doc! {
|
||||
"$set": {
|
||||
"status": PeriodStatus::Correcting.as_str(),
|
||||
"reopened_at": ts,
|
||||
"reopened_by": user_id,
|
||||
"updated_at": ts,
|
||||
},
|
||||
"$inc": { "version": 1 },
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if r.matched_count == 0 {
|
||||
return Err(period::stale());
|
||||
}
|
||||
period::get(db, household_id, period_id).await
|
||||
}
|
||||
|
||||
/// Bu dönemden hemen sonra gelen dönem (varsa)
|
||||
async fn next_period(db: &Database, household_id: Uuid, p: &Period) -> AppResult<Option<Period>> {
|
||||
let mut f = scope(household_id);
|
||||
f.insert("start_date", doc! { "$gte": &p.end_date });
|
||||
Ok(db
|
||||
.collection::<Period>(col::PERIODS)
|
||||
.find_one(f)
|
||||
.sort(doc! { "start_date": 1 })
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn settle_rules() {
|
||||
// Artı: varsayılan tamamı birikime
|
||||
assert_eq!(settle(700, None).unwrap(), (700, None));
|
||||
assert_eq!(
|
||||
settle(700, Some(500)).unwrap(),
|
||||
(500, Some((CarryKind::Surplus, 200)))
|
||||
);
|
||||
assert_eq!(
|
||||
settle(700, Some(0)).unwrap(),
|
||||
(0, Some((CarryKind::Surplus, 700)))
|
||||
);
|
||||
assert!(settle(700, Some(701)).is_err());
|
||||
assert!(settle(700, Some(-1)).is_err());
|
||||
// Sıfır: devir yok
|
||||
assert_eq!(settle(0, None).unwrap(), (0, None));
|
||||
// Açık: borç; birikim istenemez
|
||||
assert_eq!(settle(-50, None).unwrap(), (0, Some((CarryKind::Debt, 50))));
|
||||
assert_eq!(
|
||||
settle(-50, Some(0)).unwrap(),
|
||||
(0, Some((CarryKind::Debt, 50)))
|
||||
);
|
||||
assert!(settle(-50, Some(10)).is_err());
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod account;
|
||||
pub mod budget;
|
||||
pub mod calendar;
|
||||
pub mod catalog;
|
||||
pub mod closing;
|
||||
pub mod entry;
|
||||
pub mod invite;
|
||||
pub mod money;
|
||||
|
||||
@@ -82,7 +82,7 @@ pub async fn get(db: &Database, household_id: Uuid, id: Uuid) -> AppResult<Perio
|
||||
/// Plan kalemleri, notlar ve (Faz 5) harcamalar bunu kullanır.
|
||||
pub async fn get_open(db: &Database, household_id: Uuid, id: Uuid) -> AppResult<Period> {
|
||||
let p = get(db, household_id, id).await?;
|
||||
if p.status == PeriodStatus::Closed {
|
||||
if !p.status.is_writable() {
|
||||
return Err(closed());
|
||||
}
|
||||
Ok(p)
|
||||
@@ -122,24 +122,51 @@ pub async fn create(db: &Database, household_id: Uuid, p: NewPeriod) -> AppResul
|
||||
d.insert("end_date", format_date(end));
|
||||
d.insert("status", PeriodStatus::Open.as_str());
|
||||
d.insert("savings_goal", savings_goal);
|
||||
d.insert("reflection_md", "");
|
||||
d.insert("reflection_md", Bson::Null);
|
||||
d.insert("to_savings", Bson::Null);
|
||||
d.insert(
|
||||
"previous_period_id",
|
||||
previous.map_or(Bson::Null, |p| p.id.into()),
|
||||
previous.as_ref().map_or(Bson::Null, |p| p.id.into()),
|
||||
);
|
||||
d.insert("closed_at", Bson::Null);
|
||||
d.insert("closed_by", Bson::Null);
|
||||
d.insert("reopened_at", Bson::Null);
|
||||
|
||||
match db.collection::<Document>(col::PERIODS).insert_one(d).await {
|
||||
Ok(_) => {}
|
||||
// Aynı anda iki istek: kısmi benzersiz indeks ikinci açık dönemi engeller
|
||||
Err(e) if is_duplicate_key(&e) => {
|
||||
return Err(AppError::Conflict(
|
||||
"Zaten açık bir dönem var. Sayfayı yenileyin.".into(),
|
||||
));
|
||||
// Tek transaction: dönem + önceki dönemden bekleyen devrin bu döneme bağlanması
|
||||
let mut session = db.client().start_session().await?;
|
||||
session.start_transaction().await?;
|
||||
let result = async {
|
||||
db.collection::<Document>(col::PERIODS)
|
||||
.insert_one(d)
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
if let Some(prev) = &previous {
|
||||
let mut f = scope(household_id);
|
||||
f.insert("from_period_id", prev.id);
|
||||
f.insert("to_period_id", Bson::Null);
|
||||
db.collection::<Document>(col::CARRYOVERS)
|
||||
.update_many(
|
||||
f,
|
||||
doc! { "$set": { "to_period_id": id.clone(), "updated_at": now() }, "$inc": { "version": 1 } },
|
||||
)
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
}
|
||||
Ok::<_, mongodb::error::Error>(())
|
||||
}
|
||||
.await;
|
||||
match result {
|
||||
Ok(()) => session.commit_transaction().await?,
|
||||
Err(e) => {
|
||||
let _ = session.abort_transaction().await;
|
||||
// Aynı anda iki istek: kısmi benzersiz indeks ikinci açık dönemi engeller
|
||||
if is_duplicate_key(&e) {
|
||||
return Err(AppError::Conflict(
|
||||
"Zaten açık bir dönem var. Sayfayı yenileyin.".into(),
|
||||
));
|
||||
}
|
||||
return Err(e.into());
|
||||
}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
|
||||
coll(db)
|
||||
@@ -155,7 +182,7 @@ pub async fn update(
|
||||
ch: PeriodChanges,
|
||||
) -> AppResult<Period> {
|
||||
let current = get(db, household_id, id).await?;
|
||||
if current.status == PeriodStatus::Closed {
|
||||
if !current.status.is_writable() {
|
||||
return Err(closed());
|
||||
}
|
||||
if ch.version.is_some_and(|v| v != current.version) {
|
||||
@@ -297,7 +324,7 @@ async fn ensure_entries_inside(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stale() -> AppError {
|
||||
pub fn stale() -> AppError {
|
||||
AppError::Conflict("Dönem başka biri tarafından değiştirildi, sayfayı yenileyin".into())
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
//! Dönem kapatma, devir ve yeniden açma (düzeltme modu).
|
||||
|
||||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{Session, TestApp};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
async fn open_period(app: &TestApp, s: &Session, start: &str, end: &str, goal: i64) -> String {
|
||||
let (st, p) = app
|
||||
.post(
|
||||
"/periods",
|
||||
s,
|
||||
json!({ "start_date": start, "end_date": end, "savings_goal": goal }),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CREATED, "{p}");
|
||||
p["id"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
async fn income(app: &TestApp, s: &Session, pid: &str, amount: i64) {
|
||||
let (st, b) = app
|
||||
.post(
|
||||
&format!("/periods/{pid}/plan-items"),
|
||||
s,
|
||||
json!({ "kind": "income", "name": "Maaş", "amount": amount }),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CREATED, "{b}");
|
||||
}
|
||||
|
||||
async fn needs_category(app: &TestApp, s: &Session) -> String {
|
||||
let (_, cats) = app.get("/categories", s).await;
|
||||
cats.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|c| c["group"] == "needs")
|
||||
.unwrap()["id"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn spend(app: &TestApp, s: &Session, pid: &str, date: &str, amount: i64) -> StatusCode {
|
||||
let cat = needs_category(app, s).await;
|
||||
app.post(
|
||||
&format!("/periods/{pid}/entries"),
|
||||
s,
|
||||
json!({ "date": date, "amount": amount, "category_id": cat }),
|
||||
)
|
||||
.await
|
||||
.0
|
||||
}
|
||||
|
||||
async fn close(app: &TestApp, s: &Session, pid: &str, body: Value) -> (StatusCode, Value) {
|
||||
app.post(&format!("/periods/{pid}/close"), s, body).await
|
||||
}
|
||||
|
||||
async fn summary(app: &TestApp, s: &Session, pid: &str) -> Value {
|
||||
app.get(&format!("/periods/{pid}/summary"), s).await.1
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn surplus_split_and_carryover_to_next_period() {
|
||||
let app = TestApp::new().await;
|
||||
let s = app.register("Test", None).await;
|
||||
let nov = open_period(&app, &s, "2026-11-01", "2026-12-01", 200_000).await;
|
||||
income(&app, &s, &nov, 1_000_000).await;
|
||||
assert_eq!(
|
||||
spend(&app, &s, &nov, "2026-11-10", 300_000).await,
|
||||
StatusCode::CREATED
|
||||
);
|
||||
|
||||
// Gerçekleşen birikim 700.000: 500.000 birikime, 200.000 artı olarak devreder
|
||||
let (st, c) = close(&app, &s, &nov, json!({ "to_savings": 500_000 })).await;
|
||||
assert_eq!(st, StatusCode::OK, "{c}");
|
||||
assert_eq!(c["realized_savings"], 700_000);
|
||||
assert_eq!(c["to_savings"], 500_000);
|
||||
assert_eq!(
|
||||
c["carryover"],
|
||||
json!({ "kind": "surplus", "amount": 200_000, "to_period_id": null })
|
||||
);
|
||||
assert_eq!(c["period"]["status"], "closed");
|
||||
assert_eq!(c["period"]["to_savings"], 500_000);
|
||||
assert!(c["period"]["closed_at"].is_string());
|
||||
|
||||
// Kapalı dönem salt okunur; açık dönem yok
|
||||
assert_eq!(
|
||||
spend(&app, &s, &nov, "2026-11-11", 1).await,
|
||||
StatusCode::CONFLICT
|
||||
);
|
||||
let (_, cur) = app.get("/periods/current", &s).await;
|
||||
assert_eq!(cur, Value::Null);
|
||||
let (st, _) = close(&app, &s, &nov, json!({})).await;
|
||||
assert_eq!(st, StatusCode::CONFLICT, "iki kez kapatılamaz");
|
||||
|
||||
// Sonraki dönem açılınca devir ona bağlanır → harcanabilir tutara eklenir
|
||||
let dec = open_period(&app, &s, "2026-12-01", "2027-01-01", 0).await;
|
||||
income(&app, &s, &dec, 1_000_000).await;
|
||||
let sum = summary(&app, &s, &dec).await;
|
||||
assert_eq!(
|
||||
(sum["carryover_surplus"].as_i64(), sum["spendable"].as_i64()),
|
||||
(Some(200_000), Some(1_200_000))
|
||||
);
|
||||
let (_, r) = app.get(&format!("/periods/{dec}/review"), &s).await;
|
||||
assert_eq!(r["available"], 1_200_000);
|
||||
|
||||
app.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deficit_carries_over_as_debt_and_validation() {
|
||||
let app = TestApp::new().await;
|
||||
let s = app.register("Test", None).await;
|
||||
let nov = open_period(&app, &s, "2026-11-01", "2026-12-01", 0).await;
|
||||
income(&app, &s, &nov, 100_000).await;
|
||||
spend(&app, &s, &nov, "2026-11-10", 150_000).await;
|
||||
|
||||
let (st, b) = close(&app, &s, &nov, json!({ "to_savings": 10 })).await;
|
||||
assert_eq!(st, StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
b["error"]["message"],
|
||||
"Bu dönemde birikim yok; açık, borç olarak sonraki döneme devreder"
|
||||
);
|
||||
let (st, _) = close(&app, &s, &nov, json!({ "version": 99 })).await;
|
||||
assert_eq!(st, StatusCode::CONFLICT, "eski sürüm");
|
||||
|
||||
let (st, c) = close(&app, &s, &nov, json!({})).await;
|
||||
assert_eq!(st, StatusCode::OK, "{c}");
|
||||
assert_eq!(c["carryover"]["kind"], "debt");
|
||||
assert_eq!(c["carryover"]["amount"], 50_000);
|
||||
|
||||
let dec = open_period(&app, &s, "2026-12-01", "2027-01-01", 0).await;
|
||||
let sum = summary(&app, &s, &dec).await;
|
||||
assert_eq!(sum["carryover_debt"], 50_000);
|
||||
assert_eq!(sum["spendable"], -50_000);
|
||||
|
||||
// Artıda: birikim tutarı sınırların dışında → 400
|
||||
income(&app, &s, &dec, 300_000).await;
|
||||
for bad in [-1, 250_001] {
|
||||
let (st, b) = close(&app, &s, &dec, json!({ "to_savings": bad })).await;
|
||||
assert_eq!(st, StatusCode::BAD_REQUEST, "{bad}: {b}");
|
||||
}
|
||||
// Varsayılan: tamamı birikime, devir yok
|
||||
let (_, c) = close(&app, &s, &dec, json!({})).await;
|
||||
assert_eq!(
|
||||
(c["to_savings"].as_i64(), &c["carryover"]),
|
||||
(Some(250_000), &Value::Null)
|
||||
);
|
||||
|
||||
app.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reopen_for_correction_while_next_period_is_open() {
|
||||
let app = TestApp::new().await;
|
||||
let owner = app.register("Sahip", None).await;
|
||||
let (_, inv) = app.post("/household/invites", &owner, json!({})).await;
|
||||
let member = app
|
||||
.register("Uye", Some(inv["code"].as_str().unwrap()))
|
||||
.await;
|
||||
|
||||
let nov = open_period(&app, &owner, "2026-11-01", "2026-12-01", 0).await;
|
||||
income(&app, &owner, &nov, 500_000).await;
|
||||
spend(&app, &owner, &nov, "2026-11-05", 100_000).await;
|
||||
close(&app, &owner, &nov, json!({ "to_savings": 0 })).await; // 400.000 artı
|
||||
let dec = open_period(&app, &owner, "2026-12-01", "2027-01-01", 0).await;
|
||||
assert_eq!(
|
||||
summary(&app, &owner, &dec).await["carryover_surplus"],
|
||||
400_000
|
||||
);
|
||||
|
||||
// Açık dönem yeniden açılamaz; üye yeniden açamaz
|
||||
let (st, _) = app
|
||||
.post(&format!("/periods/{dec}/reopen"), &owner, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CONFLICT);
|
||||
let (st, _) = app
|
||||
.post(&format!("/periods/{nov}/reopen"), &member, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::FORBIDDEN);
|
||||
|
||||
// Sahip: Kasım düzeltmeye açılır, Aralık açık kalır
|
||||
let (st, p) = app
|
||||
.post(&format!("/periods/{nov}/reopen"), &owner, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::OK, "{p}");
|
||||
assert_eq!(p["status"], "correcting");
|
||||
let (_, cur) = app.get("/periods/current", &owner).await;
|
||||
assert_eq!(cur["id"], dec.as_str());
|
||||
// Düzeltme: Kasım'a unutulan harcama eklenir (üye de ekleyebilir)
|
||||
assert_eq!(
|
||||
spend(&app, &member, &nov, "2026-11-20", 150_000).await,
|
||||
StatusCode::CREATED
|
||||
);
|
||||
// Yeniden kapatılana kadar Aralık'ın devri değişmez
|
||||
assert_eq!(
|
||||
summary(&app, &owner, &dec).await["carryover_surplus"],
|
||||
400_000
|
||||
);
|
||||
|
||||
// Yeniden kapat → devir yeniden hesaplanır ve doğrudan Aralık'a bağlanır
|
||||
let (st, c) = close(&app, &owner, &nov, json!({ "to_savings": 0 })).await;
|
||||
assert_eq!(st, StatusCode::OK, "{c}");
|
||||
assert_eq!(
|
||||
c["carryover"],
|
||||
json!({ "kind": "surplus", "amount": 250_000, "to_period_id": dec })
|
||||
);
|
||||
let sum = summary(&app, &owner, &dec).await;
|
||||
assert_eq!(
|
||||
sum["carryover_surplus"], 250_000,
|
||||
"eski devir silindi, yenisi geçerli"
|
||||
);
|
||||
|
||||
// Aralık kapanınca Kasım artık yeniden açılamaz (zincir bozulurdu)
|
||||
close(&app, &owner, &dec, json!({})).await;
|
||||
let (st, b) = app
|
||||
.post(&format!("/periods/{nov}/reopen"), &owner, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CONFLICT, "{b}");
|
||||
let (st, _) = app
|
||||
.post(&format!("/periods/{dec}/reopen"), &owner, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::OK);
|
||||
|
||||
app.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn closing_is_isolated_between_households() {
|
||||
let app = TestApp::new().await;
|
||||
let a = app.register("Ahane", None).await;
|
||||
let b = app.register("Bhane", None).await;
|
||||
let pid = open_period(&app, &a, "2026-11-01", "2026-12-01", 0).await;
|
||||
|
||||
let (st, _) = close(&app, &b, &pid, json!({})).await;
|
||||
assert_eq!(st, StatusCode::NOT_FOUND);
|
||||
close(&app, &a, &pid, json!({})).await;
|
||||
let (st, _) = app
|
||||
.post(&format!("/periods/{pid}/reopen"), &b, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::NOT_FOUND);
|
||||
|
||||
app.cleanup().await;
|
||||
}
|
||||
+7
-5
@@ -81,16 +81,17 @@ Renkler ve ikonlar frontend tasarım token'larında.
|
||||
```js
|
||||
{ _id, household_id, name /* "Aralık 2026" */,
|
||||
start_date: "2026-12-01", end_date: "2027-01-01", // bitiş HARİÇ
|
||||
status: "open" | "closed",
|
||||
status: "open" | "closed" | "correcting",
|
||||
savings_goal: 500000, // kuruş — birikim hedefi
|
||||
reflection_md: "...", // "Nasıl daha iyi yapabilirim?"
|
||||
closed_at, closed_by, reopened_at,
|
||||
to_savings: 450000, // kapanışta birikime aktarılan (Faz 8'de varlık hareketi)
|
||||
closed_at, closed_by, reopened_at, reopened_by,
|
||||
previous_period_id,
|
||||
created_at, updated_at, deleted_at, version }
|
||||
```
|
||||
- **Bitiş tarihi hariç:** `01.12.2026 – 01.01.2027` → 1–31 Aralık. Dönemler arka arkaya boşluksuz ve çakışmasız eklenir.
|
||||
- Hanede en fazla **bir** `open` dönem (servis kontrolü + kısmi benzersiz indeks).
|
||||
- Yeniden açılan dönemin durumu tekrar `open` olur. O sırada başka açık dönem varsa önce o kapatılmalı mı, yoksa geçici olarak iki açık dönem mi olsun? → **Açık konu, Faz 6'da netleşecek.**
|
||||
- **Yeniden açma (karar 2026-11-12, "düzeltme modu"):** sahip kapalı dönemi açınca durum `correcting` olur; yazılabilir ama "açık dönem" sayılmaz → yeni dönem açık kalabilir (tek `open` kuralı korunur). Mevcut devir "yeniden kapat"a kadar yerinde kalır; yeniden kapatınca eskisi soft delete, yenisi hesaplanır. Ardından kapanmış dönem varsa açılamaz (zincirleme devirler bozulmasın).
|
||||
- **Haftalar saklanmaz.** `start_date`/`end_date`'ten hesaplanır (Pazartesi başlangıçlı; ilk/son kısmi haftalar ayrı).
|
||||
|
||||
### 3.5 `categories` — Alt kategoriler
|
||||
@@ -143,7 +144,8 @@ Renkler ve ikonlar frontend tasarım token'larında.
|
||||
kind: "debt" | "surplus", amount: 120000,
|
||||
created_at, ... }
|
||||
```
|
||||
- Dönem kapanışında oluşur. Dönem yeniden açılırsa soft delete edilir ve kapanışta yeniden hesaplanır.
|
||||
- Dönem kapanışında oluşur. Sonraki dönem henüz yoksa `to_period_id: null`; o dönem açılınca (aynı transaction'da) bağlanır.
|
||||
- Yeniden kapatmada eskisi soft delete edilir, yenisi yazılır (düzeltme süresince eskisi geçerli kalır).
|
||||
|
||||
### 3.11 `notes`
|
||||
```js
|
||||
@@ -223,7 +225,7 @@ Renkler ve ikonlar frontend tasarım token'larında.
|
||||
## 6. Dönem Kapanışı (tek transaction)
|
||||
1. `periods.status = "closed"`, `closed_at`, `closed_by`
|
||||
2. `carryovers` kaydı (borç veya artı) → sonraki dönem
|
||||
3. (Onaylıysa) gerçekleşen birikim → `asset_transactions` (`TRY`, `buy`, `source_period_id`)
|
||||
3. Gerçekleşen birikimin `to_savings` kadarı birikime: şimdilik `periods.to_savings`; Faz 8'de → `asset_transactions` (`TRY`, `buy`, `source_period_id`)
|
||||
4. (İstenirse) yeni dönem: `plan_items` ve aylık `budgets` kopyalanarak açılır
|
||||
|
||||
Transaction'ın bir adımı hata verirse önceki adımlar da geri alınır, hiçbir şey değişmez.
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
# 🧠 Proje Hafızası
|
||||
|
||||
## ▶️ Sıradaki Adım
|
||||
**Faz 6 – Adım 2:** Backend dönem kapatma (`POST /periods/{id}/close`, tek transaction): `status=closed`, `closed_at/by`; gerçekleşen birikim < 0 → `carryovers{kind: debt}`, > 0 → kullanıcının `to_savings` kadarı birikime (Faz 8'e kadar dönemde saklanır), kalanı `surplus`; devrin `to_period_id`'si sonraki dönem açılınca bağlanır. Yeniden açma (sahip): devirler soft delete. **Kullanıcıya sorulacak:** başka açık dönem varken eski dönem nasıl yeniden açılır?
|
||||
**Faz 6 – Adım 3:** Frontend Değerlendirme sayfası (`/degerlendirme`, açık dönem; ileride `?donem=`): 4 soru kartı (`PeriodReview`), grup planlanan/gerçekleşen çubukları, en çok harcanan kategoriler (`useCategories(true)` ile ad), haftalık dağılım, "Nasıl daha iyi yapabilirim?" (Markdown editör → `PATCH reflection_md`), notlar `NotesSection context="review"` (yalnızca başlık listesi, tıklayınca açılır), "🔒 Dönemi Kapat" düğmesi (sihirbaz Adım 4).
|
||||
**Bekleyen (kullanıcıda):** Faz 5 – Adım 5 canlı yedek kurulumu (`docs/DEPLOY.md` §6.1–6.3) → çıktılar gelince Faz 5 ✅.
|
||||
|
||||
## 📌 Alınan Kararlar
|
||||
@@ -51,7 +51,7 @@
|
||||
- **Canlı yedekleme kurulumu bekliyor** (betikler hazır, `docs/DEPLOY.md` §6) → gerçek veri girişinden önce.
|
||||
- **Canlı MongoDB auth kapalı** (Dokploy replica set betiği keyFile kullanmıyor, özel Command'ı yok sayıyor). İç ağda, port kapalı. Ayrıntı: `docs/DEPLOY.md` §4.
|
||||
- Üye çıkarma / rol değiştirme / sahipliği devretme henüz yok (Ayarlar ekranıyla birlikte, Faz 2+).
|
||||
- Kapalı dönem yeniden açılırken başka açık dönem varsa davranış (Faz 6).
|
||||
- ~~Kapalı dönem yeniden açma~~ → çözüldü: düzeltme modu (`correcting`). Arayüzde düzeltmedeki dönem için Defter/Plan dönem seçimi Faz 6 Adım 5'te.
|
||||
- Altın kuru için veri kaynağı seçimi (Faz 8).
|
||||
- Aylık değerlendirme çıktı formatı kullanıcıdan gelecek (Faz 9).
|
||||
- Canlı MongoDB kurulum şekli (konteyner mi, doğrudan mı) Faz 3'te birlikte kontrol edilecek.
|
||||
@@ -108,3 +108,4 @@
|
||||
|
||||
### 2026-11-12 — Faz 6
|
||||
- **Adım 1:** Değerlendirme backend: `services/review.rs` (`budget::summary` + `budget::view` + kategori aggregation), `routes/review.rs` → `GET /periods/{id}/review` (`PeriodReview`: `available` = gelir − sabit − borç + artı, `savings_goal`, `spent_total`, `realized_savings` = available − harcanan, `savings_gap`; `groups` planned/spent, `categories` çoktan aza (+count), `weeks` 4 grubun toplamı). Kapalı dönemde okunur. `periods.reflection_md` (PATCH, ≤ 20.000 karakter, `""` → null, yalnızca açık dönem). `tests/review.rs` (3). Mutasyon: 4. soruya hedef düşmek / silinenleri saymak → yakalandı. Not: `TestApp::register` adı e-postaya çevirir → Türkçe karakterli ad kullanma.
|
||||
- **Adım 2:** Kapatma backend: `services/closing.rs` — `settle(realized, to_savings)` (< 0 → borç, birikim istenemez; ≥ 0 → varsayılan tamamı birikime, kalan artı), `close` (tek transaction: dönem `closed` + `closed_at/by` + `to_savings`, bu dönemden eski devirler soft delete, yeni devir; `to_period_id` = sonraki dönem ya da null), `reopen` (sahip; `closed` → **`correcting`**; ardından kapalı/düzeltmede dönem varsa 409). `PeriodStatus::Correcting` + `is_writable()` → `get_open`/`update` buna bakar; `current()` yalnız `open`. `period::create` artık transaction: önceki dönemin bağsız devrini bağlar. `POST /periods/{id}/close` (`PeriodCloseRequest`/`PeriodCloseResult`/`Carryover`), `POST /periods/{id}/reopen`. Period DTO: `to_savings`, `closed_at`. `tests/closing.rs` (4) + `settle` birim testi. Mutasyon 3/3 yakalandı; hata zorlanınca transaction geri aldı (dönem açık, sürüm aynı). Backend 113 test. Karar: yeniden açma = düzeltme modu (kullanıcı "a").
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@
|
||||
|
||||
## Faz 6 Adımları (taslak)
|
||||
- ✅ 1. Backend: değerlendirme (`GET /periods/{id}/review`: 4 soru, grup/kategori/hafta) + `reflection_md`
|
||||
- ⏳ 2. Backend: dönem kapatma (transaction: durum, devir, birikime aktarılan tutar) + yeni dönemde devrin bağlanması + yeniden açma (sahip)
|
||||
- ✅ 2. Backend: dönem kapatma (transaction: durum, devir, birikime aktarılan tutar) + yeni dönemde devrin bağlanması + yeniden açma (sahip)
|
||||
- ⏳ 3. Frontend: Değerlendirme sayfası (4 soru, planlanan/gerçekleşen, en çok harcanan kategoriler, haftalık dağılım, "Nasıl daha iyi yapabilirim?", notlar)
|
||||
- ⏳ 4. Frontend: dönem kapatma sihirbazı (+ yeni dönemi kopyalayarak açma)
|
||||
- ⏳ 5. Frontend: kapalı dönemler listesi + yeniden açma (sahip)
|
||||
|
||||
@@ -14,6 +14,8 @@ const period: Period = {
|
||||
status: 'open',
|
||||
savings_goal: 500_000,
|
||||
reflection_md: null,
|
||||
to_savings: null,
|
||||
closed_at: null,
|
||||
version: 3,
|
||||
weeks: [
|
||||
{ index: 1, start_date: '2026-12-01', last_date: '2026-12-06', days: 6 },
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Carryover = {
|
||||
/**
|
||||
* `debt` (borç) · `surplus` (artı)
|
||||
*/
|
||||
kind: "debt" | "surplus", amount: number,
|
||||
/**
|
||||
* Sonraki dönem henüz açılmadıysa `null` (açılınca bağlanır)
|
||||
*/
|
||||
to_period_id: string | null, };
|
||||
@@ -22,4 +22,12 @@ savings_goal: number,
|
||||
/**
|
||||
* "Nasıl daha iyi yapabilirim?" (Markdown)
|
||||
*/
|
||||
reflection_md: string | null, version: number, weeks: Array<PeriodWeek>, };
|
||||
reflection_md: string | null,
|
||||
/**
|
||||
* Kapanışta birikime aktarılan (kuruş); kapanmamışsa `null`
|
||||
*/
|
||||
to_savings: number | null,
|
||||
/**
|
||||
* Kapanış zamanı (ISO 8601)
|
||||
*/
|
||||
closed_at: string | null, version: number, weeks: Array<PeriodWeek>, };
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PeriodCloseRequest = {
|
||||
/**
|
||||
* Birikime aktarılacak (kuruş); verilmezse gerçekleşen birikimin tamamı. Kalanı artı olarak devreder.
|
||||
*/
|
||||
to_savings?: number, version?: number, };
|
||||
@@ -0,0 +1,13 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Carryover } from "./Carryover";
|
||||
import type { Period } from "./Period";
|
||||
|
||||
export type PeriodCloseResult = { period: Period,
|
||||
/**
|
||||
* Değerlendirmenin 4. sorusu (negatifse açık)
|
||||
*/
|
||||
realized_savings: number, to_savings: number,
|
||||
/**
|
||||
* Sonraki döneme devreden; yoksa `null`
|
||||
*/
|
||||
carryover: Carryover | null, };
|
||||
@@ -1,3 +1,3 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type PeriodStatus = "open" | "closed";
|
||||
export type PeriodStatus = "open" | "closed" | "correcting";
|
||||
|
||||
Reference in New Issue
Block a user