Files
kakeibo/backend/tests/stats.rs
T
suphicakir 30136ed1d3 feat(panel): günlük harcama ve dönem istatistikleri uç noktaları
- GET /periods/{id}/daily: dönemin her günü için 4 grubun harcaması
- GET /stats/periods: son dönemlerin harcanabilir/harcanan tutarı + toplam birikim
- Faz 7 adım planı

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 23:37:05 +03:00

165 lines
5.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Panel verileri: günlük harcama ve dönem istatistikleri (toplam birikim dahil).
mod common;
use axum::http::StatusCode;
use common::{Session, TestApp};
use serde_json::{Value, json};
async fn open(app: &TestApp, s: &Session, start: &str, end: &str, goal: i64) -> String {
let (st, p) = app
.post(
"/periods",
s,
json!({ "start_date": start, "end_date": end, "savings_goal": goal }),
)
.await;
assert_eq!(st, StatusCode::CREATED, "{p}");
p["id"].as_str().unwrap().to_string()
}
async fn category(app: &TestApp, s: &Session, group: &str) -> String {
let (_, cats) = app.get("/categories", s).await;
cats.as_array()
.unwrap()
.iter()
.find(|c| c["group"] == group)
.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 income(app: &TestApp, s: &Session, pid: &str, amount: i64) {
let (st, b) = app
.post(
&format!("/periods/{pid}/plan-items"),
s,
json!({ "kind": "income", "name": "Maaş", "amount": amount }),
)
.await;
assert_eq!(st, StatusCode::CREATED, "{b}");
}
/// Kasım: gelir 10.000, harcama 3.000, 5.000 birikime → 2.000 artı Aralık'a.
/// Aralık: gelir 20.000, hedef 1.000 → harcanabilir 21.000.
async fn two_periods(app: &TestApp, s: &Session) -> (String, String) {
let needs = category(app, s, "needs").await;
let nov = open(app, s, "2026-11-01", "2026-12-01", 0).await;
income(app, s, &nov, 1_000_000).await;
spend(app, s, &nov, "2026-11-30", 300_000, &needs).await;
let (st, b) = app
.post(
&format!("/periods/{nov}/close"),
s,
json!({ "to_savings": 500_000 }),
)
.await;
assert_eq!(st, StatusCode::OK, "{b}");
let dec = open(app, s, "2026-12-01", "2027-01-01", 100_000).await;
income(app, s, &dec, 2_000_000).await;
(nov, dec)
}
#[tokio::test]
async fn daily_spending_by_group() {
let app = TestApp::new().await;
let s = app.register("Test", None).await;
let (_, dec) = two_periods(&app, &s).await;
let needs = category(&app, &s, "needs").await;
let wants = category(&app, &s, "wants").await;
spend(&app, &s, &dec, "2026-12-01", 100_000, &needs).await;
spend(&app, &s, &dec, "2026-12-01", 50_000, &wants).await;
spend(&app, &s, &dec, "2026-12-07", 20_000, &needs).await;
spend(&app, &s, &dec, "2026-12-31", 7_000, &needs).await;
let gone = spend(&app, &s, &dec, "2026-12-02", 99_000, &needs).await;
let (st, _) = app
.delete(&format!("/entries/{}", gone["id"].as_str().unwrap()), &s)
.await;
assert_eq!(st, StatusCode::NO_CONTENT);
let (st, days) = app.get(&format!("/periods/{dec}/daily"), &s).await;
assert_eq!(st, StatusCode::OK, "{days}");
let days = days.as_array().unwrap();
assert_eq!(days.len(), 31, "her gün, harcama olmasa da");
assert_eq!(days[0]["date"], "2026-12-01");
assert_eq!(
days[0]["spent"],
json!({ "needs": 100_000, "wants": 50_000, "culture": 0, "unexpected": 0 })
);
assert_eq!(days[1]["spent"]["needs"], 0, "silinen kayıt sayılmaz");
assert_eq!(days[6]["spent"]["needs"], 20_000);
assert_eq!(days[30]["date"], "2026-12-31");
assert_eq!(days[30]["spent"]["needs"], 7_000, "son gün dahil");
// Kasım'ın 30.11 harcaması Aralık'a karışmaz
let total: i64 = days
.iter()
.map(|d| d["spent"]["needs"].as_i64().unwrap())
.sum();
assert_eq!(total, 127_000);
// Başka hane göremez
let other = app.register("Diger", None).await;
let (st, _) = app.get(&format!("/periods/{dec}/daily"), &other).await;
assert_eq!(st, StatusCode::NOT_FOUND);
app.cleanup().await;
}
#[tokio::test]
async fn period_stats_and_total_savings() {
let app = TestApp::new().await;
let s = app.register("Test", None).await;
let (nov, dec) = two_periods(&app, &s).await;
let needs = category(&app, &s, "needs").await;
spend(&app, &s, &dec, "2026-12-05", 40_000, &needs).await;
let (st, b) = app.get("/stats/periods", &s).await;
assert_eq!(st, StatusCode::OK, "{b}");
assert_eq!(b["total_saved"], 500_000);
let list = b["periods"].as_array().unwrap();
assert_eq!(list.len(), 2);
// Eskiden yeniye
assert_eq!(list[0]["id"], nov.as_str());
assert_eq!(list[0]["status"], "closed");
assert_eq!(list[0]["spendable"], 1_000_000);
assert_eq!(list[0]["spent"], 300_000);
assert_eq!(list[0]["to_savings"], 500_000);
assert_eq!(list[0]["last_date"], "2026-11-30");
assert_eq!(list[1]["id"], dec.as_str());
assert_eq!(
list[1]["spendable"], 2_100_000,
"20.000 − 1.000 hedef + 2.000 artı"
);
assert_eq!(list[1]["spent"], 40_000);
assert_eq!(list[1]["to_savings"], Value::Null);
// limit: en yeni N dönem
let (_, b) = app.get("/stats/periods?limit=1", &s).await;
let list = b["periods"].as_array().unwrap();
assert_eq!(list.len(), 1);
assert_eq!(list[0]["id"], dec.as_str());
assert_eq!(
b["total_saved"], 500_000,
"toplam birikim limitten bağımsız"
);
// Başka hane: boş
let other = app.register("Diger", None).await;
let (_, b) = app.get("/stats/periods", &other).await;
assert_eq!(b["total_saved"], 0);
assert_eq!(b["periods"], json!([]));
app.cleanup().await;
}