//! Dönem kapanışı → birikim: onayla TL varlığına ekleme, bekleyenler, yeniden kapatmada //! bağlı kaydın güncellenmesi / silinmesi, birikimden çıkış varsa reddedilmesi. mod common; use axum::http::StatusCode; use common::{Session, TestApp}; use serde_json::{Value, json}; /// Ekim: gelir 10.000, harcama 3.000 → gerçekleşen 7.000; `to_savings` ile kapatılır async fn october(app: &TestApp, s: &Session, to_savings: i64) -> String { let (_, p) = app .post( "/periods", s, json!({ "start_date": "2026-10-01", "end_date": "2026-11-01" }), ) .await; let id = p["id"].as_str().unwrap().to_string(); app.post( &format!("/periods/{id}/plan-items"), s, json!({ "kind": "income", "name": "Maaş", "amount": 1_000_000 }), ) .await; let (_, cats) = app.get("/categories", s).await; let cat = cats[0]["id"].as_str().unwrap(); app.post( &format!("/periods/{id}/entries"), s, json!({ "date": "2026-10-10", "amount": 300_000, "category_id": cat }), ) .await; close(app, s, &id, to_savings).await; id } async fn close(app: &TestApp, s: &Session, id: &str, to_savings: i64) -> Value { let (st, b) = app .post( &format!("/periods/{id}/close"), s, json!({ "to_savings": to_savings }), ) .await; assert_eq!(st, StatusCode::OK, "{b}"); b } async fn reopen(app: &TestApp, s: &Session, id: &str) { let (st, b) = app .post(&format!("/periods/{id}/reopen"), s, json!({})) .await; assert_eq!(st, StatusCode::OK, "{b}"); } async fn cash(app: &TestApp, s: &Session) -> Value { let (_, list) = app.get("/assets", s).await; list.as_array() .unwrap() .iter() .find(|a| a["code"] == "TRY") .unwrap() .clone() } async fn cash_txs(app: &TestApp, s: &Session) -> Vec { let id = cash(app, s).await["id"].as_str().unwrap().to_string(); let (_, list) = app.get(&format!("/assets/{id}/transactions"), s).await; list.as_array().unwrap().clone() } #[tokio::test] async fn add_closing_savings_once() { let app = TestApp::new().await; let s = app.register("Test", None).await; let oct = october(&app, &s, 500_000).await; let (_, pending) = app.get("/savings/pending", &s).await; assert_eq!( pending, json!([{ "period_id": oct, "name": "Ekim 2026", "last_date": "2026-10-31", "to_savings": 500_000 }]) ); let (st, t) = app .post(&format!("/periods/{oct}/to-savings"), &s, json!({})) .await; assert_eq!(st, StatusCode::CREATED, "{t}"); assert_eq!(t["type"], "buy"); assert_eq!(t["total"], 500_000); assert_eq!(t["date"], "2026-10-31"); assert_eq!(t["note"], "Ekim 2026 kapanışı"); assert_eq!(t["source_period_id"], oct.as_str()); assert_eq!(cash(&app, &s).await["cost"], 500_000); let (st, b) = app .post(&format!("/periods/{oct}/to-savings"), &s, json!({})) .await; assert_eq!(st, StatusCode::CONFLICT, "{b}"); let (_, pending) = app.get("/savings/pending", &s).await; assert_eq!(pending, json!([])); // Açık dönem eklenemez; başka hane göremez let (_, nov) = app .post( "/periods", &s, json!({ "start_date": "2026-11-01", "end_date": "2026-12-01" }), ) .await; let (st, _) = app .post( &format!("/periods/{}/to-savings", nov["id"].as_str().unwrap()), &s, json!({}), ) .await; assert_eq!(st, StatusCode::BAD_REQUEST); let other = app.register("Diger", None).await; let (st, _) = app .post(&format!("/periods/{oct}/to-savings"), &other, json!({})) .await; assert_eq!(st, StatusCode::NOT_FOUND); let (_, op) = app.get("/savings/pending", &other).await; assert_eq!(op, json!([])); // Silinen kayıt → dönem yeniden "bekleyen" olur let (st, _) = app .delete( &format!("/asset-transactions/{}", t["id"].as_str().unwrap()), &s, ) .await; assert_eq!(st, StatusCode::NO_CONTENT); let (_, pending) = app.get("/savings/pending", &s).await; assert_eq!(pending.as_array().unwrap().len(), 1); app.cleanup().await; } #[tokio::test] async fn reclosing_updates_or_removes_linked_savings() { let app = TestApp::new().await; let s = app.register("Test", None).await; let oct = october(&app, &s, 500_000).await; app.post(&format!("/periods/{oct}/to-savings"), &s, json!({})) .await; // Yeni tutarla yeniden kapat → kayıt güncellenir reopen(&app, &s, &oct).await; let b = close(&app, &s, &oct, 400_000).await; assert_eq!(b["savings_linked"], true); let txs = cash_txs(&app, &s).await; assert_eq!(txs.len(), 1); assert_eq!(txs[0]["total"], 400_000); assert_eq!(txs[0]["version"], 2); assert_eq!(cash(&app, &s).await["cost"], 400_000); // Birikim 0 → kayıt silinir; bekleyen de yok reopen(&app, &s, &oct).await; let b = close(&app, &s, &oct, 0).await; assert_eq!(b["savings_linked"], false); assert!(cash_txs(&app, &s).await.is_empty()); let (_, pending) = app.get("/savings/pending", &s).await; assert_eq!(pending, json!([])); // Kayıt yokken yeniden kapat → otomatik eklenmez, bekleyenlere düşer reopen(&app, &s, &oct).await; let b = close(&app, &s, &oct, 600_000).await; assert_eq!(b["savings_linked"], false); assert!(cash_txs(&app, &s).await.is_empty()); let (_, pending) = app.get("/savings/pending", &s).await; assert_eq!(pending[0]["to_savings"], 600_000); app.cleanup().await; } #[tokio::test] async fn reclosing_is_refused_when_savings_were_spent() { let app = TestApp::new().await; let s = app.register("Test", None).await; let oct = october(&app, &s, 500_000).await; app.post(&format!("/periods/{oct}/to-savings"), &s, json!({})) .await; // Birikimden 4.500 TL çıkış let cash_id = cash(&app, &s).await["id"].as_str().unwrap().to_string(); let (st, _) = app .post( &format!("/assets/{cash_id}/transactions"), &s, json!({ "type": "sell", "date": "2026-11-05", "total": 450_000 }), ) .await; assert_eq!(st, StatusCode::CREATED); reopen(&app, &s, &oct).await; let (st, b) = app .post( &format!("/periods/{oct}/close"), &s, json!({ "to_savings": 300_000 }), ) .await; assert_eq!(st, StatusCode::CONFLICT, "{b}"); assert!( b["error"]["message"] .as_str() .unwrap() .contains("TL çıkışları"), "{b}" ); // Hiçbir şey değişmedi: dönem düzeltmede, kayıt 5.000 TL let (_, p) = app.get(&format!("/periods/{oct}"), &s).await; assert_eq!(p["status"], "correcting"); let buy = cash_txs(&app, &s) .await .into_iter() .find(|t| t["type"] == "buy") .unwrap(); assert_eq!(buy["total"], 500_000); // Yeterli tutarla kapanır let b = close(&app, &s, &oct, 450_000).await; assert_eq!(b["savings_linked"], true); app.cleanup().await; }