//! 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; }