//! Plan kalemleri ve notlar. mod common; use axum::http::StatusCode; use common::{Session, TestApp}; use mongodb::bson::{Document, doc}; use serde_json::{Value, json}; async fn open_period(app: &TestApp, s: &Session, start: &str, end: &str) -> String { let (st, p) = app .post( "/periods", s, json!({ "start_date": start, "end_date": end }), ) .await; assert_eq!(st, StatusCode::CREATED, "{p}"); p["id"].as_str().unwrap().to_string() } async fn close_period(app: &TestApp, id: &str) { let uuid = mongodb::bson::Uuid::parse_str(id).unwrap(); app.db .collection::("periods") .update_one( doc! { "_id": uuid }, doc! { "$set": { "status": "closed" } }, ) .await .unwrap(); } fn names(list: &Value) -> Vec { list.as_array() .unwrap() .iter() .map(|i| { format!( "{}:{}", i["kind"].as_str().unwrap(), i["name"].as_str().unwrap() ) }) .collect() } #[tokio::test] async fn plan_items_crud_and_validation() { let app = TestApp::new().await; let s = app.register("Test", None).await; let pid = open_period(&app, &s, "2026-12-01", "2027-01-01").await; let items = format!("/periods/{pid}/plan-items"); // Gider önce eklense de liste gelirle başlar let (st, rent) = app .post( &items, &s, json!({ "kind": "expense", "name": "Kira", "amount": 1_500_000, "date": "2026-12-05" }), ) .await; assert_eq!(st, StatusCode::CREATED, "{rent}"); app.post( &items, &s, json!({ "kind": "income", "name": "Maaş", "amount": 4_500_000 }), ) .await; app.post( &items, &s, json!({ "kind": "expense", "name": "İnternet", "amount": 45_000 }), ) .await; let (_, list) = app.get(&items, &s).await; assert_eq!( names(&list), vec!["income:Maaş", "expense:Kira", "expense:İnternet"] ); assert_eq!(list[0]["date"], Value::Null); // Doğrulamalar for bad in [ json!({ "kind": "expense", "name": "X", "amount": 0 }), json!({ "kind": "expense", "name": " ", "amount": 100 }), json!({ "kind": "expense", "name": "X", "amount": 100, "date": "2027-01-01" }), // dönem dışı (bitiş hariç) json!({ "kind": "expense", "name": "X", "amount": 100, "date": "05.12.2026" }), json!({ "kind": "gift", "name": "X", "amount": 100 }), ] { let (st, _) = app.post(&items, &s, bad.clone()).await; assert_eq!(st, StatusCode::BAD_REQUEST, "{bad}"); } // Düzenle: tutar + tarihi kaldır; eski sürüm → 409 let rent_path = format!("/plan-items/{}", rent["id"].as_str().unwrap()); let (st, upd) = app .patch( &rent_path, &s, json!({ "amount": 1_600_000, "date": "", "version": 1 }), ) .await; assert_eq!(st, StatusCode::OK, "{upd}"); assert_eq!( (upd["amount"].as_i64(), &upd["date"]), (Some(1_600_000), &Value::Null) ); let (st, _) = app .patch(&rent_path, &s, json!({ "name": "Y", "version": 1 })) .await; assert_eq!(st, StatusCode::CONFLICT); // Sil → listeden çıkar assert_eq!(app.delete(&rent_path, &s).await.0, StatusCode::NO_CONTENT); let (_, list) = app.get(&items, &s).await; assert_eq!(names(&list), vec!["income:Maaş", "expense:İnternet"]); app.cleanup().await; } #[tokio::test] async fn copy_previous_shifts_dates_and_skips_duplicates() { let app = TestApp::new().await; let s = app.register("Test", None).await; let dec = open_period(&app, &s, "2026-12-01", "2027-01-01").await; let dec_items = format!("/periods/{dec}/plan-items"); app.post( &dec_items, &s, json!({ "kind": "income", "name": "Maaş", "amount": 4_500_000, "date": "2026-12-15" }), ) .await; app.post( &dec_items, &s, json!({ "kind": "expense", "name": "Kira", "amount": 1_500_000, "date": "2026-12-05" }), ) .await; app.post( &dec_items, &s, json!({ "kind": "expense", "name": "Aidat", "amount": 80_000 }), ) .await; app.post( &dec_items, &s, json!({ "kind": "expense", "name": "Fatura", "amount": 90_000, "date": "2026-12-31" }), ) .await; close_period(&app, &dec).await; let jan = open_period(&app, &s, "2027-01-01", "2027-02-01").await; let jan_items = format!("/periods/{jan}/plan-items"); // Kullanıcı kirayı elle zaten eklemiş (farklı tutarla) → kopyada atlanmalı app.post( &jan_items, &s, json!({ "kind": "expense", "name": "kira", "amount": 1_700_000 }), ) .await; let (st, res) = app .post(&format!("{jan_items}/copy-previous"), &s, json!({})) .await; assert_eq!(st, StatusCode::OK, "{res}"); assert_eq!(res["added"], 3); let (_, list) = app.get(&jan_items, &s).await; let by_name = |n: &str| { list.as_array() .unwrap() .iter() .find(|i| i["name"] == n) .unwrap() .clone() }; assert_eq!(by_name("Maaş")["date"], "2027-01-15", "göreli gün korunur"); assert_eq!(by_name("Aidat")["date"], Value::Null); assert_eq!(by_name("kira")["amount"], 1_700_000, "elle eklenen korunur"); assert_eq!(by_name("Fatura")["date"], "2027-01-31"); // Ocak → Şubat (28 gün): 31 Ocak'taki fatura dönem dışına taşar → son güne (28 Şubat) çekilir close_period(&app, &jan).await; let feb = open_period(&app, &s, "2027-02-01", "2027-03-01").await; let feb_items = format!("/periods/{feb}/plan-items"); app.post(&format!("{feb_items}/copy-previous"), &s, json!({})) .await; let (_, feb_list) = app.get(&feb_items, &s).await; let fatura = feb_list .as_array() .unwrap() .iter() .find(|i| i["name"] == "Fatura") .unwrap(); assert_eq!(fatura["date"], "2027-02-28"); // İkinci kez → hiçbir şey eklenmez (Ocak kapalı olduğu için Şubat'ta deniyoruz) let (_, res) = app .post(&format!("{feb_items}/copy-previous"), &s, json!({})) .await; assert_eq!(res["added"], 0); // İlk dönemde önceki yok → 400 let (st, _) = app .post(&format!("{dec_items}/copy-previous"), &s, json!({})) .await; assert!( st == StatusCode::BAD_REQUEST || st == StatusCode::CONFLICT, "{st}" ); app.cleanup().await; } #[tokio::test] async fn closed_period_is_read_only() { let app = TestApp::new().await; let s = app.register("Test", None).await; let pid = open_period(&app, &s, "2026-12-01", "2027-01-01").await; let (_, item) = app .post( &format!("/periods/{pid}/plan-items"), &s, json!({ "kind": "income", "name": "Maaş", "amount": 100 }), ) .await; let (_, note) = app .post( &format!("/periods/{pid}/notes"), &s, json!({ "context": "planning", "title": "Hedef" }), ) .await; close_period(&app, &pid).await; let item_path = format!("/plan-items/{}", item["id"].as_str().unwrap()); let note_path = format!("/notes/{}", note["id"].as_str().unwrap()); let checks = [ app.post( &format!("/periods/{pid}/plan-items"), &s, json!({ "kind": "income", "name": "X", "amount": 1 }), ) .await .0, app.patch(&item_path, &s, json!({ "amount": 5 })).await.0, app.delete(&item_path, &s).await.0, app.post( &format!("/periods/{pid}/notes"), &s, json!({ "context": "review", "title": "X" }), ) .await .0, app.patch(¬e_path, &s, json!({ "title": "Y" })).await.0, app.delete(¬e_path, &s).await.0, ]; assert!( checks.iter().all(|st| *st == StatusCode::CONFLICT), "{checks:?}" ); // Okuma serbest assert_eq!( app.get(&format!("/periods/{pid}/plan-items"), &s).await.0, StatusCode::OK ); assert_eq!( app.get(&format!("/periods/{pid}/notes"), &s).await.0, StatusCode::OK ); app.cleanup().await; } #[tokio::test] async fn notes_crud_and_context_filter() { let app = TestApp::new().await; let s = app.register("Test", None).await; let pid = open_period(&app, &s, "2026-12-01", "2027-01-01").await; let notes = format!("/periods/{pid}/notes"); let (st, n) = app .post(¬es, &s, json!({ "context": "planning", "title": " Tatil birikimi ", "body_md": "- Ocak'ta **5.000 ₺**" })) .await; assert_eq!(st, StatusCode::CREATED, "{n}"); assert_eq!(n["title"], "Tatil birikimi"); app.post(¬es, &s, json!({ "context": "review", "title": "Ders" })) .await; let (_, planning) = app.get(&format!("{notes}?context=planning"), &s).await; assert_eq!(planning.as_array().unwrap().len(), 1); let (_, all) = app.get(¬es, &s).await; assert_eq!(all.as_array().unwrap().len(), 2); let (st, _) = app .post(¬es, &s, json!({ "context": "planning", "title": "" })) .await; assert_eq!(st, StatusCode::BAD_REQUEST, "boş başlık"); let path = format!("/notes/{}", n["id"].as_str().unwrap()); let (st, upd) = app .patch(&path, &s, json!({ "body_md": "Güncel", "version": 1 })) .await; assert_eq!( (st, upd["body_md"].as_str()), (StatusCode::OK, Some("Güncel")) ); assert_eq!(app.delete(&path, &s).await.0, StatusCode::NO_CONTENT); let (_, planning) = app.get(&format!("{notes}?context=planning"), &s).await; assert_eq!(planning, json!([])); app.cleanup().await; } #[tokio::test] async fn plan_items_and_notes_are_isolated() { let app = TestApp::new().await; let a = app.register("A", None).await; let b = app.register("B", None).await; let pid = open_period(&app, &a, "2026-12-01", "2027-01-01").await; let (_, item) = app .post( &format!("/periods/{pid}/plan-items"), &a, json!({ "kind": "income", "name": "Maaş", "amount": 100 }), ) .await; let (_, note) = app .post( &format!("/periods/{pid}/notes"), &a, json!({ "context": "planning", "title": "Gizli" }), ) .await; let item_path = format!("/plan-items/{}", item["id"].as_str().unwrap()); let note_path = format!("/notes/{}", note["id"].as_str().unwrap()); let checks = [ app.get(&format!("/periods/{pid}/plan-items"), &b).await.0, app.post( &format!("/periods/{pid}/plan-items"), &b, json!({ "kind": "income", "name": "X", "amount": 1 }), ) .await .0, app.post( &format!("/periods/{pid}/plan-items/copy-previous"), &b, json!({}), ) .await .0, app.patch(&item_path, &b, json!({ "amount": 1 })).await.0, app.delete(&item_path, &b).await.0, app.get(&format!("/periods/{pid}/notes"), &b).await.0, app.patch(¬e_path, &b, json!({ "title": "X" })).await.0, app.delete(¬e_path, &b).await.0, ]; assert!( checks.iter().all(|st| *st == StatusCode::NOT_FOUND), "{checks:?}" ); // A'nın verisi değişmedi let (_, list) = app.get(&format!("/periods/{pid}/plan-items"), &a).await; assert_eq!(list[0]["amount"], 100); assert_eq!(list[0]["version"], 1); app.cleanup().await; }