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:
@@ -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