Files
kakeibo/backend/tests/debt_plan.rs
T
suphicakir f0d285a9ba feat(plan): devreden borç planın parçası, borçlu ayda %45 kuralı
- Devreden borç: elle girilen ya da dönem başından önceki son ay sonu ölçümü
- Borçlu ay: harcanabilir = gelir × oran (vars. %45), kalan borca, artan birikime
- Birikim hedefi ve Kakeibo devri borçlu ayda uygulanmaz; kapanış devir üretmez
- Plan düzenleme ve sihirbazda devreden borç alanı + oran; canlı önizleme
- Plan'daki eski borç bölümü ve taksit önerisi kaldırıldı (veri yerinde)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 06:27:12 +03:00

235 lines
7.1 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.
//! Devreden borç planın parçası (docs/KAKEIBO_PANEL_ISTEKLERI.md A): %45 kuralı, kaynak
//! (ölçüm / elle), borç bitince normal düzen, borçlu ayda Kakeibo devri yok.
mod common;
use axum::http::StatusCode;
use common::{Session, TestApp};
use serde_json::{Value, json};
async fn december(app: &TestApp, s: &Session) -> String {
let (_, p) = app
.post(
"/periods",
s,
json!({ "start_date": "2026-12-01", "end_date": "2027-01-01", "savings_goal": 500_000 }),
)
.await;
let id = p["id"].as_str().unwrap().to_string();
// A5 kontrol: gelir 79.509,64 · sabit 23.006,90
for (kind, name, amount) in [
("income", "Maaş", 7_950_964),
("expense", "Kira", 2_000_000),
("expense", "Taksitler", 300_690),
] {
app.post(
&format!("/periods/{id}/plan-items"),
s,
json!({ "kind": kind, "name": name, "amount": amount }),
)
.await;
}
id
}
async fn summary(app: &TestApp, s: &Session, pid: &str) -> Value {
let (st, b) = app.get(&format!("/periods/{pid}/summary"), s).await;
assert_eq!(st, StatusCode::OK, "{b}");
b
}
#[tokio::test]
async fn debt_month_uses_45_percent_rule() {
let app = TestApp::new().await;
let s = app.register("Sahip", None).await;
let pid = december(&app, &s).await;
// Borç yokken normal Kakeibo: gelir − sabit − birikim
let x = summary(&app, &s, &pid).await;
assert_eq!(x["debt_mode"], false);
assert_eq!(x["carried_debt_source"], Value::Null);
assert_eq!(x["spendable"], 7_950_964 - 2_300_690 - 500_000);
// Kasım sonu ölçümü (toplam 57.100) → otomatik devreden borç
app.post(
"/debt/snapshots",
&s,
json!({ "date": "2026-11-30", "kmh": 3_000_000, "card_balance": 2_710_000, "note": "maaş sonrası" }),
)
.await;
let x = summary(&app, &s, &pid).await;
assert_eq!(x["debt_mode"], true);
assert_eq!(
(
x["carried_debt"].as_i64(),
x["carried_debt_source"].as_str()
),
(Some(5_710_000), Some("auto"))
);
assert_eq!(x["carried_debt_date"], "2026-11-30");
assert_eq!(x["spendable_rate_bp"], 4500);
assert_eq!(
x["spendable"], 3_577_934,
"35.779,34 — birikim hedefi girmez"
);
assert_eq!(x["debt_payment"], 2_072_340, "20.723,40");
assert_eq!(x["closing_debt_target"], 3_637_660, "36.376,60");
assert_eq!(x["debt_fixed_too_high"], false);
assert_eq!(
x["unallocated"], 3_577_934,
"bütçe dağıtımı yeni harcanabilire göre"
);
// Elle düzeltme → manual; 10.000 → borca 10.000, birikime 10.723,40
let (st, _) = app
.put(
&format!("/periods/{pid}/opening-debt"),
&s,
json!({ "amount": 1_000_000 }),
)
.await;
assert_eq!(st, StatusCode::NO_CONTENT);
let x = summary(&app, &s, &pid).await;
assert_eq!(x["carried_debt_source"], "manual");
assert_eq!(
x["carried_debt_auto"], 5_710_000,
"ölçüm değeri elle düzeltmede de görünür"
);
assert_eq!(x["carried_debt_date"], "2026-11-30");
assert_eq!(
(x["debt_payment"].as_i64(), x["debt_to_savings"].as_i64()),
(Some(1_000_000), Some(1_072_340))
);
// Elle 0 → borç bitti, normal düzen (A6)
app.put(
&format!("/periods/{pid}/opening-debt"),
&s,
json!({ "amount": 0 }),
)
.await;
let x = summary(&app, &s, &pid).await;
assert_eq!(x["debt_mode"], false);
assert_eq!(x["spendable"], 7_950_964 - 2_300_690 - 500_000);
// Oran ayarlanabilir (%10–%90)
app.put(
&format!("/periods/{pid}/opening-debt"),
&s,
json!({ "amount": null }),
)
.await;
let (_, p) = app.get(&format!("/periods/{pid}"), &s).await;
let (st, p2) = app
.patch(
&format!("/periods/{pid}"),
&s,
json!({ "spendable_rate_bp": 5000, "version": p["version"] }),
)
.await;
assert_eq!(st, StatusCode::OK, "{p2}");
assert_eq!(p2["spendable_rate_bp"], 5000);
assert_eq!(summary(&app, &s, &pid).await["spendable"], 3_975_482);
let (st, _) = app
.patch(
&format!("/periods/{pid}"),
&s,
json!({ "spendable_rate_bp": 9500 }),
)
.await;
assert_eq!(st, StatusCode::BAD_REQUEST);
app.cleanup().await;
}
#[tokio::test]
async fn net_positive_measurement_means_no_debt_and_fixed_too_high_warns() {
let app = TestApp::new().await;
let s = app.register("Sahip", None).await;
let pid = december(&app, &s).await;
// Artı bakiye borçtan büyük → borç 0 → normal düzen
app.post(
"/debt/snapshots",
&s,
json!({ "date": "2026-11-30", "card_balance": 100_000, "positive_balance": 300_000 }),
)
.await;
let x = summary(&app, &s, &pid).await;
assert_eq!(
(x["debt_mode"].as_bool(), x["carried_debt"].as_i64()),
(Some(false), Some(0))
);
// Sabitler çok yüksek: harcanabilir = gelir − sabit, borca 0, uyarı
app.put(
&format!("/periods/{pid}/opening-debt"),
&s,
json!({ "amount": 5_710_000 }),
)
.await;
app.post(
&format!("/periods/{pid}/plan-items"),
&s,
json!({ "kind": "expense", "name": "Büyük ödeme", "amount": 3_000_000 }),
)
.await;
let x = summary(&app, &s, &pid).await;
assert_eq!(x["debt_fixed_too_high"], true);
assert_eq!(x["debt_payment"], 0);
assert_eq!(x["spendable"], 7_950_964 - 5_300_690);
assert_eq!(x["closing_debt_target"], 5_710_000);
app.cleanup().await;
}
#[tokio::test]
async fn closing_a_debt_month_creates_no_kakeibo_carryover() {
let app = TestApp::new().await;
let s = app.register("Sahip", None).await;
let (_, p) = app
.post(
"/periods",
&s,
json!({ "start_date": "2026-11-01", "end_date": "2026-12-01", "savings_goal": 0 }),
)
.await;
let nov = p["id"].as_str().unwrap().to_string();
app.post(
&format!("/periods/{nov}/plan-items"),
&s,
json!({ "kind": "income", "name": "Maaş", "amount": 1_000_000 }),
)
.await;
app.put(
&format!("/periods/{nov}/opening-debt"),
&s,
json!({ "amount": 5_000_000 }),
)
.await;
let (st, c) = app
.post(
&format!("/periods/{nov}/close"),
&s,
json!({ "to_savings": 200_000 }),
)
.await;
assert_eq!(st, StatusCode::OK, "{c}");
assert_eq!(c["to_savings"], 0, "borçlu ayda birikim aktarılmaz");
assert!(c["carryover"].is_null(), "Kakeibo devri yok: {c}");
let (_, p) = app
.post(
"/periods",
&s,
json!({ "start_date": "2026-12-01", "end_date": "2027-01-01" }),
)
.await;
let x = summary(&app, &s, p["id"].as_str().unwrap()).await;
assert_eq!(
(
x["carryover_debt"].as_i64(),
x["carryover_surplus"].as_i64()
),
(Some(0), Some(0))
);
app.cleanup().await;
}