48554e0ff6
- 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>
224 lines
7.2 KiB
Rust
224 lines
7.2 KiB
Rust
//! 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;
|
||
}
|