feat(birikim): dönem kapanışındaki birikimi varlıklara ekleme
- Kapatma sonucunda onayla "Birikime ekle" (TL alımı, dönemin son günü) - Birikimler'de "Bekleyen birikimler" listesi - Yeniden kapatmada bağlı kayıt yeni tutara güncellenir (0 → silinir), TL çıkışları aşılırsa reddedilir - Panel "Toplam birikim" artık portföyün güncel değeri Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -371,3 +371,9 @@ Content-Type: application/json
|
||||
|
||||
### Elle kuru sil → otomatik kura dön
|
||||
DELETE {{baseUrl}}/rates/BURAYA-KUR-ID
|
||||
|
||||
### Kapanıp birikime henüz eklenmemiş dönemler
|
||||
GET {{baseUrl}}/savings/pending
|
||||
|
||||
### Dönemin kapanıştaki birikimini TL varlığına ekle (dönem başına bir kez)
|
||||
POST {{baseUrl}}/periods/BURAYA-DONEM-ID/to-savings
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
//! | POST | `/api/assets/{id}/transactions` | alım / satım ekle |
|
||||
//! | PATCH | `/api/asset-transactions/{id}` | işlemi düzenle |
|
||||
//! | DELETE | `/api/asset-transactions/{id}` | işlemi sil (soft) |
|
||||
//! | GET | `/api/savings/pending` | kapanıp birikime henüz eklenmemiş dönemler |
|
||||
//! | POST | `/api/periods/{id}/to-savings` | dönemin birikimini TL varlığına alım olarak ekle |
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
@@ -292,3 +294,48 @@ pub async fn delete_tx(
|
||||
asset::delete_tx(&s.db, auth.household_id, path_id(&id)?).await?;
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
#[derive(Serialize, TS)]
|
||||
#[ts(export, rename = "PendingSavings")]
|
||||
pub struct PendingDto {
|
||||
period_id: String,
|
||||
name: String,
|
||||
/// Dönemin son günü (eklenecek alımın tarihi)
|
||||
last_date: String,
|
||||
/// Kuruş
|
||||
#[ts(type = "number")]
|
||||
to_savings: i64,
|
||||
}
|
||||
|
||||
pub async fn pending(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
) -> AppResult<Json<Vec<PendingDto>>> {
|
||||
let list = asset::pending_period_savings(&s.db, auth.household_id).await?;
|
||||
Ok(Json(
|
||||
list.into_iter()
|
||||
.map(|p| {
|
||||
let end = crate::services::calendar::parse_date(&p.end_date)
|
||||
.expect("geçerli bitiş tarihi");
|
||||
PendingDto {
|
||||
period_id: p.id.to_string(),
|
||||
name: p.name,
|
||||
last_date: crate::services::calendar::format_date(
|
||||
end - time::Duration::days(1),
|
||||
),
|
||||
to_savings: p.to_savings.unwrap_or(0),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn add_period_savings(
|
||||
State(s): State<AppState>,
|
||||
auth: AuthUser,
|
||||
Path(id): Path<String>,
|
||||
) -> AppResult<(StatusCode, Json<TxDto>)> {
|
||||
let t =
|
||||
asset::add_period_savings(&s.db, auth.household_id, auth.user_id, path_id(&id)?).await?;
|
||||
Ok((StatusCode::CREATED, Json(t.into())))
|
||||
}
|
||||
|
||||
@@ -102,6 +102,8 @@ pub fn router(state: AppState) -> Router {
|
||||
.route("/entries/{id}/restore", post(entry::restore))
|
||||
.route("/assets", get(asset::list).post(asset::create))
|
||||
.route("/portfolio", get(rates::portfolio))
|
||||
.route("/savings/pending", get(asset::pending))
|
||||
.route("/periods/{id}/to-savings", post(asset::add_period_savings))
|
||||
.route("/rates", post(rates::create))
|
||||
.route("/rates/{id}", delete(rates::delete))
|
||||
.route("/assets/{id}", patch(asset::update))
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
//! | 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,
|
||||
@@ -223,6 +224,9 @@ pub struct CloseResultDto {
|
||||
to_savings: i64,
|
||||
/// Sonraki döneme devreden; yoksa `null`
|
||||
carryover: Option<CarryoverDto>,
|
||||
/// 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)]
|
||||
@@ -256,6 +260,7 @@ pub async fn close(
|
||||
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,
|
||||
|
||||
@@ -429,3 +429,193 @@ fn clean_note(n: Option<&str>) -> AppResult<Option<String>> {
|
||||
fn stale() -> AppError {
|
||||
AppError::Conflict("Kayıt başka biri tarafından değiştirildi, sayfayı yenileyin".into())
|
||||
}
|
||||
|
||||
// ---------- Dönem kapanışından birikim ----------
|
||||
|
||||
/// Kapanışta birikime aktarılan tutarın TL varlığına eklenmiş kaydı (dönem başına tek)
|
||||
pub async fn period_savings_tx(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
period_id: Uuid,
|
||||
) -> AppResult<Option<AssetTx>> {
|
||||
let mut f = scope(household_id);
|
||||
f.insert("source_period_id", period_id);
|
||||
Ok(txs(db).find_one(f).await?)
|
||||
}
|
||||
|
||||
async fn cash_asset(db: &Database, household_id: Uuid) -> AppResult<Asset> {
|
||||
let mut f = scope(household_id);
|
||||
f.insert("kind", "cash");
|
||||
assets(db)
|
||||
.find_one(f)
|
||||
.await?
|
||||
.ok_or_else(|| AppError::Internal("hanenin TL varlığı yok".into()))
|
||||
}
|
||||
|
||||
/// Kapalı dönemin `to_savings` tutarını TL varlığına **alım** olarak ekler (kullanıcı onayıyla).
|
||||
/// Tarih = dönemin son günü. Aynı dönem ikinci kez eklenemez.
|
||||
pub async fn add_period_savings(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
user_id: Uuid,
|
||||
period_id: Uuid,
|
||||
) -> AppResult<AssetTx> {
|
||||
let p = crate::services::period::get(db, household_id, period_id).await?;
|
||||
if p.status != crate::models::period::PeriodStatus::Closed {
|
||||
return Err(AppError::BadRequest(
|
||||
"Dönem kapanmadan birikime eklenemez".into(),
|
||||
));
|
||||
}
|
||||
let amount = p.to_savings.unwrap_or(0);
|
||||
if amount <= 0 {
|
||||
return Err(AppError::BadRequest(
|
||||
"Bu dönemde birikime aktarılan tutar yok".into(),
|
||||
));
|
||||
}
|
||||
if period_savings_tx(db, household_id, period_id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(AppError::Conflict(
|
||||
"Bu dönemin birikimi zaten eklendi".into(),
|
||||
));
|
||||
}
|
||||
let cash = cash_asset(db, household_id).await?;
|
||||
|
||||
let mut d = new_meta(household_id);
|
||||
let id = d.get("_id").cloned().unwrap_or(Bson::Null);
|
||||
d.insert("asset_id", cash.id);
|
||||
d.insert("type", TxType::Buy.as_str());
|
||||
d.insert("date", last_day(&p.end_date)?);
|
||||
d.insert("quantity_e6", amount * 10_000);
|
||||
d.insert("unit_price_e6", ONE_E6);
|
||||
d.insert("total", amount);
|
||||
d.insert("note", format!("{} kapanışı", p.name));
|
||||
d.insert("source_period_id", period_id);
|
||||
d.insert("created_by", user_id);
|
||||
db.collection::<Document>(col::ASSET_TRANSACTIONS)
|
||||
.insert_one(d)
|
||||
.await?;
|
||||
txs(db)
|
||||
.find_one(doc! { "_id": id })
|
||||
.await?
|
||||
.ok_or_else(|| AppError::Internal("eklenen işlem okunamadı".into()))
|
||||
}
|
||||
|
||||
/// Kapalı, birikime aktarılan tutarı olan ama henüz eklenmemiş dönemler (en yeni başta)
|
||||
pub async fn pending_period_savings(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
) -> AppResult<Vec<crate::models::period::Period>> {
|
||||
let linked: Vec<Uuid> = {
|
||||
let mut f = scope(household_id);
|
||||
f.insert("source_period_id", doc! { "$ne": null });
|
||||
let mut c = txs(db).find(f).await?;
|
||||
let mut v = Vec::new();
|
||||
while c.advance().await? {
|
||||
if let Some(p) = c.deserialize_current()?.source_period_id {
|
||||
v.push(p);
|
||||
}
|
||||
}
|
||||
v
|
||||
};
|
||||
Ok(crate::services::period::list(db, household_id)
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|p| {
|
||||
p.status == crate::models::period::PeriodStatus::Closed
|
||||
&& p.to_savings.unwrap_or(0) > 0
|
||||
&& !linked.contains(&p.id)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Yeniden kapatmada bağlı kayda yapılacak iş (transaction'dan önce hazırlanıp denetlenir)
|
||||
pub enum SavingsSync {
|
||||
Update { id: Uuid, amount: i64, date: String },
|
||||
Delete { id: Uuid },
|
||||
}
|
||||
|
||||
/// Bağlı kayıt varsa yeni tutar/tarihe göre işi hazırlar. Tutar 0 → silinir.
|
||||
/// TL'de sonraki satışlar eldeki miktarı aşacaksa reddedilir (kapatma da yapılmaz).
|
||||
pub async fn plan_savings_sync(
|
||||
db: &Database,
|
||||
household_id: Uuid,
|
||||
period_id: Uuid,
|
||||
amount: i64,
|
||||
end_date: &str,
|
||||
) -> AppResult<Option<SavingsSync>> {
|
||||
let Some(linked) = period_savings_tx(db, household_id, period_id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let date = last_day(end_date)?;
|
||||
let mut list: Vec<AssetTx> = all_txs(db, household_id, Some(linked.asset_id))
|
||||
.await?
|
||||
.into_iter()
|
||||
.filter(|x| x.id != linked.id)
|
||||
.collect();
|
||||
if amount > 0 {
|
||||
let mut changed = linked.clone();
|
||||
changed.date = date.clone();
|
||||
changed.quantity_e6 = amount * 10_000;
|
||||
changed.total = amount;
|
||||
let pos = list.partition_point(|x| {
|
||||
(x.date.as_str(), x.id.bytes()) <= (date.as_str(), linked.id.bytes())
|
||||
});
|
||||
list.insert(pos, changed);
|
||||
}
|
||||
if holding::holding(&moves(&list)).is_err() {
|
||||
return Err(AppError::Conflict(
|
||||
"Birikimden yapılan TL çıkışları yeni tutarı aşıyor; önce Birikimler'deki kayıtları düzeltin"
|
||||
.into(),
|
||||
));
|
||||
}
|
||||
Ok(Some(if amount > 0 {
|
||||
SavingsSync::Update {
|
||||
id: linked.id,
|
||||
amount,
|
||||
date,
|
||||
}
|
||||
} else {
|
||||
SavingsSync::Delete { id: linked.id }
|
||||
}))
|
||||
}
|
||||
|
||||
/// Hazırlanan işi kapatma transaction'ı içinde uygular
|
||||
pub async fn apply_savings_sync(
|
||||
db: &Database,
|
||||
session: &mut mongodb::ClientSession,
|
||||
household_id: Uuid,
|
||||
sync: &SavingsSync,
|
||||
) -> AppResult<()> {
|
||||
let ts = now();
|
||||
let (id, set) = match sync {
|
||||
SavingsSync::Update { id, amount, date } => (
|
||||
*id,
|
||||
doc! {
|
||||
"date": date,
|
||||
"quantity_e6": amount * 10_000,
|
||||
"unit_price_e6": ONE_E6,
|
||||
"total": amount,
|
||||
"updated_at": ts,
|
||||
},
|
||||
),
|
||||
SavingsSync::Delete { id } => (*id, doc! { "deleted_at": ts, "updated_at": ts }),
|
||||
};
|
||||
let mut f = scope(household_id);
|
||||
f.insert("_id", id);
|
||||
db.collection::<Document>(col::ASSET_TRANSACTIONS)
|
||||
.update_one(f, doc! { "$set": set, "$inc": { "version": 1 } })
|
||||
.session(session)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Hariç bitiş → son gün (dahil)
|
||||
fn last_day(end_date: &str) -> AppResult<String> {
|
||||
let end =
|
||||
parse_date(end_date).ok_or_else(|| AppError::Internal("dönem bitişi geçersiz".into()))?;
|
||||
Ok(crate::services::calendar::format_date(
|
||||
end - time::Duration::days(1),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ use crate::{
|
||||
common::{new_meta, now, scope},
|
||||
period::{Period, PeriodStatus},
|
||||
},
|
||||
services::{period, review},
|
||||
services::{asset, period, review},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -54,6 +54,8 @@ pub struct Closing {
|
||||
pub realized_savings: i64,
|
||||
pub to_savings: i64,
|
||||
pub carryover: Option<Carry>,
|
||||
/// Birikime eklenmiş kayıt var (yeniden kapatmada güncellendi)
|
||||
pub savings_linked: bool,
|
||||
}
|
||||
|
||||
/// Gerçekleşen birikimi birikim / devir olarak böler. `to_savings` verilmezse tamamı birikime.
|
||||
@@ -99,6 +101,9 @@ pub async fn close(
|
||||
.realized_savings;
|
||||
let (to_savings, carry) = settle(realized, to_savings)?;
|
||||
let next = next_period(db, household_id, &p).await?;
|
||||
// Yeniden kapatma: birikime eklenmiş kayıt varsa yeni tutara uyarlanır (önce denetlenir)
|
||||
let savings_sync =
|
||||
asset::plan_savings_sync(db, household_id, period_id, to_savings, &p.end_date).await?;
|
||||
let ts = now();
|
||||
|
||||
let mut session = db.client().start_session().await?;
|
||||
@@ -138,6 +143,10 @@ pub async fn close(
|
||||
.session(&mut session)
|
||||
.await?;
|
||||
|
||||
if let Some(sync) = &savings_sync {
|
||||
asset::apply_savings_sync(db, &mut session, household_id, sync).await?;
|
||||
}
|
||||
|
||||
if let Some((kind, amount)) = carry {
|
||||
let mut d = new_meta(household_id);
|
||||
d.insert("from_period_id", period_id);
|
||||
@@ -164,6 +173,7 @@ pub async fn close(
|
||||
}
|
||||
|
||||
Ok(Closing {
|
||||
savings_linked: matches!(savings_sync, Some(asset::SavingsSync::Update { .. })),
|
||||
period: period::get(db, household_id, period_id).await?,
|
||||
realized_savings: realized,
|
||||
to_savings,
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
//! Dönem kapanışı → birikim: onayla TL varlığına ekleme, bekleyenler, yeniden kapatmada
|
||||
//! bağlı kaydın güncellenmesi / silinmesi, birikimden çıkış varsa reddedilmesi.
|
||||
|
||||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{Session, TestApp};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// Ekim: gelir 10.000, harcama 3.000 → gerçekleşen 7.000; `to_savings` ile kapatılır
|
||||
async fn october(app: &TestApp, s: &Session, to_savings: i64) -> String {
|
||||
let (_, p) = app
|
||||
.post(
|
||||
"/periods",
|
||||
s,
|
||||
json!({ "start_date": "2026-10-01", "end_date": "2026-11-01" }),
|
||||
)
|
||||
.await;
|
||||
let id = p["id"].as_str().unwrap().to_string();
|
||||
app.post(
|
||||
&format!("/periods/{id}/plan-items"),
|
||||
s,
|
||||
json!({ "kind": "income", "name": "Maaş", "amount": 1_000_000 }),
|
||||
)
|
||||
.await;
|
||||
let (_, cats) = app.get("/categories", s).await;
|
||||
let cat = cats[0]["id"].as_str().unwrap();
|
||||
app.post(
|
||||
&format!("/periods/{id}/entries"),
|
||||
s,
|
||||
json!({ "date": "2026-10-10", "amount": 300_000, "category_id": cat }),
|
||||
)
|
||||
.await;
|
||||
close(app, s, &id, to_savings).await;
|
||||
id
|
||||
}
|
||||
|
||||
async fn close(app: &TestApp, s: &Session, id: &str, to_savings: i64) -> Value {
|
||||
let (st, b) = app
|
||||
.post(
|
||||
&format!("/periods/{id}/close"),
|
||||
s,
|
||||
json!({ "to_savings": to_savings }),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::OK, "{b}");
|
||||
b
|
||||
}
|
||||
|
||||
async fn reopen(app: &TestApp, s: &Session, id: &str) {
|
||||
let (st, b) = app
|
||||
.post(&format!("/periods/{id}/reopen"), s, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::OK, "{b}");
|
||||
}
|
||||
|
||||
async fn cash(app: &TestApp, s: &Session) -> Value {
|
||||
let (_, list) = app.get("/assets", s).await;
|
||||
list.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|a| a["code"] == "TRY")
|
||||
.unwrap()
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn cash_txs(app: &TestApp, s: &Session) -> Vec<Value> {
|
||||
let id = cash(app, s).await["id"].as_str().unwrap().to_string();
|
||||
let (_, list) = app.get(&format!("/assets/{id}/transactions"), s).await;
|
||||
list.as_array().unwrap().clone()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_closing_savings_once() {
|
||||
let app = TestApp::new().await;
|
||||
let s = app.register("Test", None).await;
|
||||
let oct = october(&app, &s, 500_000).await;
|
||||
|
||||
let (_, pending) = app.get("/savings/pending", &s).await;
|
||||
assert_eq!(
|
||||
pending,
|
||||
json!([{ "period_id": oct, "name": "Ekim 2026", "last_date": "2026-10-31", "to_savings": 500_000 }])
|
||||
);
|
||||
|
||||
let (st, t) = app
|
||||
.post(&format!("/periods/{oct}/to-savings"), &s, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CREATED, "{t}");
|
||||
assert_eq!(t["type"], "buy");
|
||||
assert_eq!(t["total"], 500_000);
|
||||
assert_eq!(t["date"], "2026-10-31");
|
||||
assert_eq!(t["note"], "Ekim 2026 kapanışı");
|
||||
assert_eq!(t["source_period_id"], oct.as_str());
|
||||
assert_eq!(cash(&app, &s).await["cost"], 500_000);
|
||||
|
||||
let (st, b) = app
|
||||
.post(&format!("/periods/{oct}/to-savings"), &s, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CONFLICT, "{b}");
|
||||
let (_, pending) = app.get("/savings/pending", &s).await;
|
||||
assert_eq!(pending, json!([]));
|
||||
|
||||
// Açık dönem eklenemez; başka hane göremez
|
||||
let (_, nov) = app
|
||||
.post(
|
||||
"/periods",
|
||||
&s,
|
||||
json!({ "start_date": "2026-11-01", "end_date": "2026-12-01" }),
|
||||
)
|
||||
.await;
|
||||
let (st, _) = app
|
||||
.post(
|
||||
&format!("/periods/{}/to-savings", nov["id"].as_str().unwrap()),
|
||||
&s,
|
||||
json!({}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::BAD_REQUEST);
|
||||
let other = app.register("Diger", None).await;
|
||||
let (st, _) = app
|
||||
.post(&format!("/periods/{oct}/to-savings"), &other, json!({}))
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::NOT_FOUND);
|
||||
let (_, op) = app.get("/savings/pending", &other).await;
|
||||
assert_eq!(op, json!([]));
|
||||
|
||||
// Silinen kayıt → dönem yeniden "bekleyen" olur
|
||||
let (st, _) = app
|
||||
.delete(
|
||||
&format!("/asset-transactions/{}", t["id"].as_str().unwrap()),
|
||||
&s,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::NO_CONTENT);
|
||||
let (_, pending) = app.get("/savings/pending", &s).await;
|
||||
assert_eq!(pending.as_array().unwrap().len(), 1);
|
||||
app.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reclosing_updates_or_removes_linked_savings() {
|
||||
let app = TestApp::new().await;
|
||||
let s = app.register("Test", None).await;
|
||||
let oct = october(&app, &s, 500_000).await;
|
||||
app.post(&format!("/periods/{oct}/to-savings"), &s, json!({}))
|
||||
.await;
|
||||
|
||||
// Yeni tutarla yeniden kapat → kayıt güncellenir
|
||||
reopen(&app, &s, &oct).await;
|
||||
let b = close(&app, &s, &oct, 400_000).await;
|
||||
assert_eq!(b["savings_linked"], true);
|
||||
let txs = cash_txs(&app, &s).await;
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert_eq!(txs[0]["total"], 400_000);
|
||||
assert_eq!(txs[0]["version"], 2);
|
||||
assert_eq!(cash(&app, &s).await["cost"], 400_000);
|
||||
|
||||
// Birikim 0 → kayıt silinir; bekleyen de yok
|
||||
reopen(&app, &s, &oct).await;
|
||||
let b = close(&app, &s, &oct, 0).await;
|
||||
assert_eq!(b["savings_linked"], false);
|
||||
assert!(cash_txs(&app, &s).await.is_empty());
|
||||
let (_, pending) = app.get("/savings/pending", &s).await;
|
||||
assert_eq!(pending, json!([]));
|
||||
|
||||
// Kayıt yokken yeniden kapat → otomatik eklenmez, bekleyenlere düşer
|
||||
reopen(&app, &s, &oct).await;
|
||||
let b = close(&app, &s, &oct, 600_000).await;
|
||||
assert_eq!(b["savings_linked"], false);
|
||||
assert!(cash_txs(&app, &s).await.is_empty());
|
||||
let (_, pending) = app.get("/savings/pending", &s).await;
|
||||
assert_eq!(pending[0]["to_savings"], 600_000);
|
||||
app.cleanup().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn reclosing_is_refused_when_savings_were_spent() {
|
||||
let app = TestApp::new().await;
|
||||
let s = app.register("Test", None).await;
|
||||
let oct = october(&app, &s, 500_000).await;
|
||||
app.post(&format!("/periods/{oct}/to-savings"), &s, json!({}))
|
||||
.await;
|
||||
// Birikimden 4.500 TL çıkış
|
||||
let cash_id = cash(&app, &s).await["id"].as_str().unwrap().to_string();
|
||||
let (st, _) = app
|
||||
.post(
|
||||
&format!("/assets/{cash_id}/transactions"),
|
||||
&s,
|
||||
json!({ "type": "sell", "date": "2026-11-05", "total": 450_000 }),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CREATED);
|
||||
|
||||
reopen(&app, &s, &oct).await;
|
||||
let (st, b) = app
|
||||
.post(
|
||||
&format!("/periods/{oct}/close"),
|
||||
&s,
|
||||
json!({ "to_savings": 300_000 }),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::CONFLICT, "{b}");
|
||||
assert!(
|
||||
b["error"]["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("TL çıkışları"),
|
||||
"{b}"
|
||||
);
|
||||
// Hiçbir şey değişmedi: dönem düzeltmede, kayıt 5.000 TL
|
||||
let (_, p) = app.get(&format!("/periods/{oct}"), &s).await;
|
||||
assert_eq!(p["status"], "correcting");
|
||||
let buy = cash_txs(&app, &s)
|
||||
.await
|
||||
.into_iter()
|
||||
.find(|t| t["type"] == "buy")
|
||||
.unwrap();
|
||||
assert_eq!(buy["total"], 500_000);
|
||||
// Yeterli tutarla kapanır
|
||||
let b = close(&app, &s, &oct, 450_000).await;
|
||||
assert_eq!(b["savings_linked"], true);
|
||||
app.cleanup().await;
|
||||
}
|
||||
Reference in New Issue
Block a user