//! Dönem kuralları (KAKEIBO_PROMPT §6). mod common; use axum::http::StatusCode; use common::TestApp; use mongodb::bson::{Document, doc}; use serde_json::{Value, json}; async fn open_december(app: &TestApp, s: &common::Session) -> Value { let (st, p) = app .post( "/periods", s, json!({ "start_date": "2026-12-01", "end_date": "2027-01-01", "savings_goal": 500000 }), ) .await; assert_eq!(st, StatusCode::CREATED, "{p}"); p } #[tokio::test] async fn open_period_returns_weeks_and_default_name() { let app = TestApp::new().await; let s = app.register("Test", None).await; // Başta açık dönem yok → null let (st, cur) = app.get("/periods/current", &s).await; assert_eq!((st, cur), (StatusCode::OK, Value::Null)); let p = open_december(&app, &s).await; assert_eq!(p["name"], "Aralık 2026"); assert_eq!(p["status"], "open"); assert_eq!(p["days"], 31); assert_eq!(p["last_date"], "2026-12-31"); assert_eq!(p["savings_goal"], 500000); let days: Vec = p["weeks"] .as_array() .unwrap() .iter() .map(|w| w["days"].as_i64().unwrap()) .collect(); assert_eq!( days, vec![6, 7, 7, 7, 4], "Pazartesi başlangıçlı, kısmi haftalar ayrı" ); let (_, cur) = app.get("/periods/current", &s).await; assert_eq!(cur["id"], p["id"]); app.cleanup().await; } #[tokio::test] async fn only_one_open_period_and_no_overlap() { let app = TestApp::new().await; let s = app.register("Test", None).await; let dec = open_december(&app, &s).await; // İkinci açık dönem → 409 let (st, body) = app .post( "/periods", &s, json!({ "start_date": "2027-01-01", "end_date": "2027-02-01" }), ) .await; assert_eq!(st, StatusCode::CONFLICT); assert!( body["error"]["message"] .as_str() .unwrap() .contains("Aralık 2026") ); // Aralık'ı kapat (Faz 6'da sihirbaz yapacak) → çakışan yeni dönem yine 409 app.db .collection::("periods") .update_one( doc! { "name": "Aralık 2026" }, doc! { "$set": { "status": "closed" } }, ) .await .unwrap(); let (st, body) = app .post( "/periods", &s, json!({ "start_date": "2026-12-20", "end_date": "2027-01-20" }), ) .await; assert_eq!(st, StatusCode::CONFLICT, "{body}"); assert!( body["error"]["message"] .as_str() .unwrap() .contains("çakışıyor") ); // Bitişik dönem (bitiş hariç olduğu için 1 Ocak serbest) → 201 let (st, jan) = app .post( "/periods", &s, json!({ "start_date": "2027-01-01", "end_date": "2027-02-01" }), ) .await; assert_eq!(st, StatusCode::CREATED, "{jan}"); assert_eq!(jan["name"], "Ocak 2027"); // Liste: en yeni başta let (_, list) = app.get("/periods", &s).await; let names: Vec<&str> = list .as_array() .unwrap() .iter() .map(|p| p["name"].as_str().unwrap()) .collect(); assert_eq!(names, vec!["Ocak 2027", "Aralık 2026"]); // Kapalı dönem salt okunur let (st, _) = app .patch( &format!("/periods/{}", dec["id"].as_str().unwrap()), &s, json!({ "savings_goal": 1 }), ) .await; assert_eq!(st, StatusCode::CONFLICT); app.cleanup().await; } #[tokio::test] async fn validation_errors() { let app = TestApp::new().await; let s = app.register("Test", None).await; for (start, end) in [ ("2026-12-10", "2026-12-01"), // ters ("2026-12-01", "2026-12-01"), // boş ("2026-02-30", "2026-03-30"), // geçersiz gün ("01.12.2026", "2027-01-01"), // biçim ("2026-12-01", "2027-03-01"), // 90 gün ] { let (st, _) = app .post( "/periods", &s, json!({ "start_date": start, "end_date": end }), ) .await; assert_eq!(st, StatusCode::BAD_REQUEST, "{start} → {end}"); } let (st, _) = app .post( "/periods", &s, json!({ "start_date": "2026-12-01", "end_date": "2027-01-01", "savings_goal": -5 }), ) .await; assert_eq!(st, StatusCode::BAD_REQUEST, "negatif birikim"); app.cleanup().await; } #[tokio::test] async fn update_dates_name_goal_and_version() { let app = TestApp::new().await; let s = app.register("Test", None).await; let p = open_december(&app, &s).await; let path = format!("/periods/{}", p["id"].as_str().unwrap()); // Maaş 15'inde → dönemi kaydır; ad otomatik güncellenmez, istenirse verilir let (st, upd) = app .patch( &path, &s, json!({ "start_date": "2026-12-15", "end_date": "2027-01-15", "name": "", "savings_goal": 750000, "version": 1 }), ) .await; assert_eq!(st, StatusCode::OK, "{upd}"); assert_eq!(upd["name"], "15 Ara 2026 – 14 Oca 2027"); assert_eq!(upd["savings_goal"], 750000); assert_eq!(upd["version"], 2); // Eski sürümle → 409 let (st, _) = app .patch(&path, &s, json!({ "name": "X", "version": 1 })) .await; assert_eq!(st, StatusCode::CONFLICT); // Aralığın dışında kalacak bir harcama varsa daraltma engellenir let hid = mongodb::bson::Uuid::parse_str(&s.household_id).unwrap(); let pid = mongodb::bson::Uuid::parse_str(p["id"].as_str().unwrap()).unwrap(); app.db .collection::("entries") .insert_one(doc! { "household_id": hid, "period_id": pid, "date": "2027-01-10", "deleted_at": null }) .await .unwrap(); let (st, body) = app .patch(&path, &s, json!({ "end_date": "2027-01-05" })) .await; assert_eq!(st, StatusCode::CONFLICT, "{body}"); assert!( body["error"]["message"] .as_str() .unwrap() .contains("1 harcama") ); app.cleanup().await; } #[tokio::test] async fn periods_are_isolated_between_households() { let app = TestApp::new().await; let a = app.register("A", None).await; let b = app.register("B", None).await; let p = open_december(&app, &a).await; let path = format!("/periods/{}", p["id"].as_str().unwrap()); // B, A'nın dönemini göremez/değiştiremez; kendi açık dönemi yok assert_eq!(app.get(&path, &b).await.0, StatusCode::NOT_FOUND); assert_eq!( app.patch(&path, &b, json!({ "name": "ele geçirildi" })) .await .0, StatusCode::NOT_FOUND ); assert_eq!(app.get("/periods/current", &b).await.1, Value::Null); assert_eq!(app.get("/periods", &b).await.1, json!([])); // A'nın açık dönemi B'nin dönem açmasını engellemez (açık dönem kuralı hane bazlı) let (st, _) = app .post( "/periods", &b, json!({ "start_date": "2026-12-01", "end_date": "2027-01-01" }), ) .await; assert_eq!(st, StatusCode::CREATED); app.cleanup().await; }