feat(raporlar): harcama kırılımı uç noktası ve dönem istatistiklerinde birikim oranı verileri
- GET /reports/breakdown: grup, tüm kategoriler, ödeme türü ve ay kırılımı; dönem/tarih/grup/kategori/ödeme türü filtreleri - /stats/periods: gelir, sabit gider, devirler, gerçekleşen birikim - Faz 9 planı kullanıcı kararlarıyla güncellendi (ekstre içe aktarma, JSON geri dönüş noktası, aylık rapor) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
//! Raporlar: harcama kırılımı (grup, tüm kategoriler, ödeme türü, ay) ve filtreler.
|
||||
|
||||
mod common;
|
||||
|
||||
use axum::http::StatusCode;
|
||||
use common::{Session, TestApp};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
async fn id_by_name(app: &TestApp, s: &Session, path: &str, name: &str) -> String {
|
||||
let (_, list) = app.get(path, s).await;
|
||||
list.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.find(|c| c["name"] == name)
|
||||
.unwrap()["id"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn spend(
|
||||
app: &TestApp,
|
||||
s: &Session,
|
||||
pid: &str,
|
||||
date: &str,
|
||||
amount: i64,
|
||||
cat: &str,
|
||||
pm: Option<&str>,
|
||||
) -> Value {
|
||||
let mut body = json!({ "date": date, "amount": amount, "category_id": cat });
|
||||
if let Some(pm) = pm {
|
||||
body["payment_method_id"] = json!(pm);
|
||||
}
|
||||
let (st, e) = app.post(&format!("/periods/{pid}/entries"), s, body).await;
|
||||
assert_eq!(st, StatusCode::CREATED, "{e}");
|
||||
e
|
||||
}
|
||||
|
||||
async fn report(app: &TestApp, s: &Session, query: &str) -> Value {
|
||||
let (st, b) = app.get(&format!("/reports/breakdown{query}"), s).await;
|
||||
assert_eq!(st, StatusCode::OK, "{query} → {b}");
|
||||
b
|
||||
}
|
||||
|
||||
fn totals(list: &Value, key: &str) -> Vec<(String, i64)> {
|
||||
list.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|r| {
|
||||
(
|
||||
r[key].as_str().unwrap_or("-").to_string(),
|
||||
r["total"].as_i64().unwrap(),
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn breakdown_with_filters() {
|
||||
let app = TestApp::new().await;
|
||||
let s = app.register("Test", None).await;
|
||||
let market = id_by_name(&app, &s, "/categories", "Market").await;
|
||||
let giyim = id_by_name(&app, &s, "/categories", "Giyim").await;
|
||||
let kitap = id_by_name(&app, &s, "/categories", "Kitap").await;
|
||||
let nakit = id_by_name(&app, &s, "/payment-methods", "Nakit").await;
|
||||
let kart = id_by_name(&app, &s, "/payment-methods", "Kredi Kartı").await;
|
||||
|
||||
let (_, oct) = app
|
||||
.post(
|
||||
"/periods",
|
||||
&s,
|
||||
json!({ "start_date": "2026-10-01", "end_date": "2026-11-01" }),
|
||||
)
|
||||
.await;
|
||||
let oct = oct["id"].as_str().unwrap().to_string();
|
||||
spend(&app, &s, &oct, "2026-10-05", 100_000, &market, Some(&nakit)).await;
|
||||
spend(&app, &s, &oct, "2026-10-20", 50_000, &giyim, None).await;
|
||||
app.post(&format!("/periods/{oct}/close"), &s, json!({}))
|
||||
.await;
|
||||
let (_, nov) = app
|
||||
.post(
|
||||
"/periods",
|
||||
&s,
|
||||
json!({ "start_date": "2026-11-01", "end_date": "2026-12-01" }),
|
||||
)
|
||||
.await;
|
||||
let nov = nov["id"].as_str().unwrap().to_string();
|
||||
spend(&app, &s, &nov, "2026-11-02", 30_000, &market, Some(&kart)).await;
|
||||
spend(&app, &s, &nov, "2026-11-03", 20_000, &kitap, Some(&nakit)).await;
|
||||
spend(&app, &s, &nov, "2026-11-04", 10_000, &market, Some(&nakit)).await;
|
||||
let gone = spend(&app, &s, &nov, "2026-11-05", 99_000, &kitap, None).await;
|
||||
app.delete(&format!("/entries/{}", gone["id"].as_str().unwrap()), &s)
|
||||
.await;
|
||||
|
||||
// Filtresiz: tüm zamanlar
|
||||
let r = report(&app, &s, "").await;
|
||||
assert_eq!(r["total"], 210_000);
|
||||
assert_eq!(r["count"], 5, "silinen sayılmaz");
|
||||
assert_eq!(
|
||||
totals(&r["groups"], "group"),
|
||||
[
|
||||
("needs".into(), 140_000),
|
||||
("wants".into(), 50_000),
|
||||
("culture".into(), 20_000),
|
||||
("unexpected".into(), 0)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
totals(&r["categories"], "category_id"),
|
||||
[
|
||||
(market.clone(), 140_000),
|
||||
(giyim.clone(), 50_000),
|
||||
(kitap.clone(), 20_000)
|
||||
]
|
||||
);
|
||||
assert_eq!(r["categories"][0]["count"], 3);
|
||||
assert_eq!(r["categories"][0]["group"], "needs");
|
||||
assert_eq!(
|
||||
totals(&r["payment_methods"], "payment_method_id"),
|
||||
[
|
||||
(nakit.clone(), 130_000),
|
||||
("-".into(), 50_000),
|
||||
(kart.clone(), 30_000)
|
||||
],
|
||||
"ödeme türü girilmemiş → null"
|
||||
);
|
||||
assert_eq!(
|
||||
totals(&r["months"], "month"),
|
||||
[("2026-10".into(), 150_000), ("2026-11".into(), 60_000)]
|
||||
);
|
||||
|
||||
// Dönem
|
||||
assert_eq!(
|
||||
report(&app, &s, &format!("?period_id={nov}")).await["total"],
|
||||
60_000
|
||||
);
|
||||
// Tarih aralığı iki dönemi keser (ikisi de dahil)
|
||||
let r = report(&app, &s, "?from=2026-10-15&to=2026-11-03").await;
|
||||
assert_eq!(
|
||||
(r["total"].as_i64(), r["count"].as_i64()),
|
||||
(Some(100_000), Some(3))
|
||||
);
|
||||
// Grup, kategori, ödeme türü (+ girilmemiş)
|
||||
assert_eq!(report(&app, &s, "?group=needs").await["total"], 140_000);
|
||||
assert_eq!(
|
||||
report(&app, &s, &format!("?category_id={kitap}")).await["total"],
|
||||
20_000
|
||||
);
|
||||
assert_eq!(
|
||||
report(&app, &s, &format!("?payment_method_id={nakit}")).await["total"],
|
||||
130_000
|
||||
);
|
||||
assert_eq!(
|
||||
report(&app, &s, "?payment_method_id=none").await["total"],
|
||||
50_000
|
||||
);
|
||||
// Birleşik
|
||||
let r = report(
|
||||
&app,
|
||||
&s,
|
||||
&format!("?period_id={nov}&group=needs&payment_method_id={nakit}"),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
(r["total"].as_i64(), r["count"].as_i64()),
|
||||
(Some(10_000), Some(1))
|
||||
);
|
||||
|
||||
// Hatalı filtreler
|
||||
for q in [
|
||||
"?from=2026-11-10&to=2026-11-01",
|
||||
"?from=2026-02-30",
|
||||
"?group=xyz",
|
||||
"?category_id=abc",
|
||||
] {
|
||||
let (st, b) = app.get(&format!("/reports/breakdown{q}"), &s).await;
|
||||
assert_eq!(st, StatusCode::BAD_REQUEST, "{q} → {b}");
|
||||
}
|
||||
|
||||
// Başka hane: kendi verisi (boş); bu hanenin dönemi → 404
|
||||
let other = app.register("Diger", None).await;
|
||||
let r = report(&app, &other, "").await;
|
||||
assert_eq!(r["total"], 0);
|
||||
assert_eq!(r["groups"].as_array().unwrap().len(), 4);
|
||||
assert_eq!(r["categories"], json!([]));
|
||||
let (st, _) = app
|
||||
.get(&format!("/reports/breakdown?period_id={nov}"), &other)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::NOT_FOUND);
|
||||
app.cleanup().await;
|
||||
}
|
||||
@@ -151,6 +151,17 @@ async fn period_stats_and_total_savings() {
|
||||
assert_eq!(list[1]["spent"], 42_500);
|
||||
assert_eq!(list[1]["spent_by_group"]["needs"], 40_000);
|
||||
assert_eq!(list[1]["spent_by_group"]["culture"], 2_500);
|
||||
assert_eq!(list[1]["income_total"], 2_000_000);
|
||||
assert_eq!(list[1]["savings_goal"], 100_000);
|
||||
assert_eq!(list[1]["carryover_surplus"], 200_000);
|
||||
assert_eq!(
|
||||
list[0]["realized_savings"], 700_000,
|
||||
"Kasım: 10.000 − 3.000"
|
||||
);
|
||||
assert_eq!(
|
||||
list[1]["realized_savings"], 2_157_500,
|
||||
"20.000 + 2.000 artı − 425 harcanan (hedef düşülmez)"
|
||||
);
|
||||
assert_eq!(list[1]["to_savings"], Value::Null);
|
||||
|
||||
// limit: en yeni N dönem
|
||||
|
||||
Reference in New Issue
Block a user