Files
kakeibo/backend/tests/closing.rs
T
suphicakir 62e92f552a 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>
2026-11-12 12:20:09 +03:00

246 lines
8.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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;
}