//! Ay sonu değerlendirmesi: dört soru, grup/kategori/hafta dökümü, "Nasıl daha iyi yapabilirim?". mod common; use axum::http::StatusCode; use common::{Session, TestApp}; use mongodb::bson::{Document, Uuid, doc}; use serde_json::{Value, json}; async fn open_december(app: &TestApp, s: &Session, savings_goal: i64) -> String { let (st, p) = app .post( "/periods", s, json!({ "start_date": "2026-12-01", "end_date": "2027-01-01", "savings_goal": savings_goal }), ) .await; assert_eq!(st, StatusCode::CREATED, "{p}"); p["id"].as_str().unwrap().to_string() } /// Gruptaki n'inci (0'dan) örnek kategori async fn category(app: &TestApp, s: &Session, group: &str, n: usize) -> String { let (_, cats) = app.get("/categories", s).await; cats.as_array() .unwrap() .iter() .filter(|c| c["group"] == group) .nth(n) .unwrap()["id"] .as_str() .unwrap() .to_string() } async fn spend(app: &TestApp, s: &Session, pid: &str, date: &str, amount: i64, cat: &str) -> Value { let (st, e) = app .post( &format!("/periods/{pid}/entries"), s, json!({ "date": date, "amount": amount, "category_id": cat }), ) .await; assert_eq!(st, StatusCode::CREATED, "{e}"); e } async fn plan(app: &TestApp, s: &Session, pid: &str, kind: &str, amount: i64) { let (st, b) = app .post( &format!("/periods/{pid}/plan-items"), s, json!({ "kind": kind, "name": format!("{kind}-{amount}"), "amount": amount }), ) .await; assert_eq!(st, StatusCode::CREATED, "{b}"); } fn column(list: &Value, key: &str) -> Vec { list.as_array() .unwrap() .iter() .map(|x| x[key].as_i64().unwrap()) .collect() } #[tokio::test] async fn four_questions_and_breakdowns() { let app = TestApp::new().await; let s = app.register("Test", None).await; let pid = open_december(&app, &s, 500_000).await; plan(&app, &s, &pid, "income", 4_800_000).await; plan(&app, &s, &pid, "expense", 1_545_000).await; app.db .collection::("carryovers") .insert_one(doc! { "household_id": Uuid::parse_str(&s.household_id).unwrap(), "to_period_id": Uuid::parse_str(&pid).unwrap(), "kind": "debt", "amount": 100_000_i64, "deleted_at": null, }) .await .unwrap(); app.put( &format!("/periods/{pid}/budget"), &s, json!({ "needs": 1_000_000, "wants": 500_000, "culture": 200_000, "unexpected": 100_000 }), ) .await; let (market, rent) = ( category(&app, &s, "needs", 0).await, category(&app, &s, "needs", 1).await, ); let wants = category(&app, &s, "wants", 0).await; let culture = category(&app, &s, "culture", 0).await; // Aralık 2026 haftaları: 1–6 · 7–13 · 14–20 · 21–27 · 28–31 spend(&app, &s, &pid, "2026-12-01", 100_000, &market).await; spend(&app, &s, &pid, "2026-12-08", 50_000, &market).await; spend(&app, &s, &pid, "2026-12-08", 300_000, &rent).await; spend(&app, &s, &pid, "2026-12-15", 40_000, &wants).await; spend(&app, &s, &pid, "2026-12-31", 10_000, &culture).await; let gone = spend(&app, &s, &pid, "2026-12-15", 99_999, &wants).await; app.delete(&format!("/entries/{}", gone["id"].as_str().unwrap()), &s) .await; let (st, r) = app.get(&format!("/periods/{pid}/review"), &s).await; assert_eq!(st, StatusCode::OK, "{r}"); // 1: 4.800.000 − 1.545.000 − 100.000 = 3.155.000 · 3: 500.000 · 4: 3.155.000 − 500.000 for (k, v) in [ ("available", 3_155_000), ("savings_goal", 500_000), ("spent_total", 500_000), ("realized_savings", 2_655_000), ("savings_gap", 2_155_000), ("carryover_debt", 100_000), ] { assert_eq!(r[k], v, "{k}: {r}"); } assert_eq!( r["groups"], json!([ { "group": "needs", "planned": 1_000_000, "spent": 450_000 }, { "group": "wants", "planned": 500_000, "spent": 40_000 }, { "group": "culture", "planned": 200_000, "spent": 10_000 }, { "group": "unexpected", "planned": 100_000, "spent": 0 }, ]) ); // Kategoriler çoktan aza; silinen kayıt sayılmaz let cats = r["categories"].as_array().unwrap(); assert_eq!( column(&r["categories"], "spent"), vec![300_000, 150_000, 40_000, 10_000] ); assert_eq!( ( cats[0]["category_id"].as_str(), cats[1]["category_id"].as_str() ), (Some(rent.as_str()), Some(market.as_str())) ); assert_eq!( (cats[1]["count"].as_i64(), cats[1]["group"].as_str()), (Some(2), Some("needs")) ); // Haftalar: 4 grubun toplamı; planlananların toplamı aylık bütçelerin toplamı assert_eq!( column(&r["weeks"], "spent"), vec![100_000, 350_000, 40_000, 0, 10_000] ); assert_eq!( column(&r["weeks"], "planned").iter().sum::(), 1_800_000 ); assert_eq!(r["weeks"][4]["last_date"], "2026-12-31"); app.cleanup().await; } #[tokio::test] async fn overspending_gives_negative_savings() { let app = TestApp::new().await; let s = app.register("Test", None).await; let pid = open_december(&app, &s, 100_000).await; plan(&app, &s, &pid, "income", 200_000).await; let needs = category(&app, &s, "needs", 0).await; spend(&app, &s, &pid, "2026-12-10", 250_000, &needs).await; let (_, r) = app.get(&format!("/periods/{pid}/review"), &s).await; assert_eq!( ( r["available"].as_i64(), r["realized_savings"].as_i64(), r["savings_gap"].as_i64() ), (Some(200_000), Some(-50_000), Some(-150_000)) ); // Hiç bütçe yoksa: gruplar 0 planlı, haftalar yine dönem takviminden assert_eq!(column(&r["groups"], "planned"), vec![0; 4]); assert_eq!(r["weeks"].as_array().unwrap().len(), 5); app.cleanup().await; } #[tokio::test] async fn reflection_text_and_access_rules() { let app = TestApp::new().await; let s = app.register("Test", None).await; let other = app.register("Diger", None).await; let pid = open_december(&app, &s, 0).await; let url = format!("/periods/{pid}"); let (st, p) = app .patch( &url, &s, json!({ "reflection_md": " **Dışarıda yemeği** azalt. " }), ) .await; assert_eq!(st, StatusCode::OK, "{p}"); assert_eq!(p["reflection_md"], "**Dışarıda yemeği** azalt."); let (_, p) = app.get(&url, &s).await; assert_eq!(p["reflection_md"], "**Dışarıda yemeği** azalt."); // Türkçe karakterler tek sayılır; sınırın üstü 400 let (st, _) = app .patch(&url, &s, json!({ "reflection_md": "ş".repeat(20_000) })) .await; assert_eq!(st, StatusCode::OK); let (st, b) = app .patch(&url, &s, json!({ "reflection_md": "a".repeat(20_001) })) .await; assert_eq!(st, StatusCode::BAD_REQUEST); assert_eq!( b["error"]["message"], "Metin en fazla 20.000 karakter olabilir" ); // Boş metin → kaldırılır let (_, p) = app.patch(&url, &s, json!({ "reflection_md": "" })).await; assert_eq!(p["reflection_md"], Value::Null); // Başka hane göremez let (st, _) = app.get(&format!("{url}/review"), &other).await; assert_eq!(st, StatusCode::NOT_FOUND); // Kapalı dönem: değerlendirme okunur, metin değiştirilemez app.db .collection::("periods") .update_one( doc! { "_id": Uuid::parse_str(&pid).unwrap() }, doc! { "$set": { "status": "closed" } }, ) .await .unwrap(); let (st, _) = app.get(&format!("{url}/review"), &s).await; assert_eq!(st, StatusCode::OK); let (st, _) = app.patch(&url, &s, json!({ "reflection_md": "geç" })).await; assert_eq!(st, StatusCode::CONFLICT); app.cleanup().await; }