8ec1fd6f25
- GET /periods/{id}/review: param, hedef, harcanan, biriken; grup, kategori ve hafta bazında planlanan/gerçekleşen
- Dönemde "Nasıl daha iyi yapabilirim?" metni (reflection_md)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
242 lines
8.0 KiB
Rust
242 lines
8.0 KiB
Rust
//! Ay sonu değerlendirmesi: dört soru, grup/kategori/hafta dökümü, "Nasıl daha iyi yapabilirim?".
|
||
|
||
mod common;
|
||
|
||
use axum::http::StatusCode;
|
||
use common::{Session, TestApp};
|
||
use mongodb::bson::{Document, Uuid, doc};
|
||
use serde_json::{Value, json};
|
||
|
||
async fn open_december(app: &TestApp, s: &Session, savings_goal: i64) -> String {
|
||
let (st, p) = app
|
||
.post(
|
||
"/periods",
|
||
s,
|
||
json!({ "start_date": "2026-12-01", "end_date": "2027-01-01", "savings_goal": savings_goal }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CREATED, "{p}");
|
||
p["id"].as_str().unwrap().to_string()
|
||
}
|
||
|
||
/// Gruptaki n'inci (0'dan) örnek kategori
|
||
async fn category(app: &TestApp, s: &Session, group: &str, n: usize) -> String {
|
||
let (_, cats) = app.get("/categories", s).await;
|
||
cats.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.filter(|c| c["group"] == group)
|
||
.nth(n)
|
||
.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 plan(app: &TestApp, s: &Session, pid: &str, kind: &str, amount: i64) {
|
||
let (st, b) = app
|
||
.post(
|
||
&format!("/periods/{pid}/plan-items"),
|
||
s,
|
||
json!({ "kind": kind, "name": format!("{kind}-{amount}"), "amount": amount }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CREATED, "{b}");
|
||
}
|
||
|
||
fn column(list: &Value, key: &str) -> Vec<i64> {
|
||
list.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.map(|x| x[key].as_i64().unwrap())
|
||
.collect()
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn four_questions_and_breakdowns() {
|
||
let app = TestApp::new().await;
|
||
let s = app.register("Test", None).await;
|
||
let pid = open_december(&app, &s, 500_000).await;
|
||
plan(&app, &s, &pid, "income", 4_800_000).await;
|
||
plan(&app, &s, &pid, "expense", 1_545_000).await;
|
||
app.db
|
||
.collection::<Document>("carryovers")
|
||
.insert_one(doc! {
|
||
"household_id": Uuid::parse_str(&s.household_id).unwrap(),
|
||
"to_period_id": Uuid::parse_str(&pid).unwrap(),
|
||
"kind": "debt", "amount": 100_000_i64, "deleted_at": null,
|
||
})
|
||
.await
|
||
.unwrap();
|
||
app.put(
|
||
&format!("/periods/{pid}/budget"),
|
||
&s,
|
||
json!({ "needs": 1_000_000, "wants": 500_000, "culture": 200_000, "unexpected": 100_000 }),
|
||
)
|
||
.await;
|
||
|
||
let (market, rent) = (
|
||
category(&app, &s, "needs", 0).await,
|
||
category(&app, &s, "needs", 1).await,
|
||
);
|
||
let wants = category(&app, &s, "wants", 0).await;
|
||
let culture = category(&app, &s, "culture", 0).await;
|
||
// Aralık 2026 haftaları: 1–6 · 7–13 · 14–20 · 21–27 · 28–31
|
||
spend(&app, &s, &pid, "2026-12-01", 100_000, &market).await;
|
||
spend(&app, &s, &pid, "2026-12-08", 50_000, &market).await;
|
||
spend(&app, &s, &pid, "2026-12-08", 300_000, &rent).await;
|
||
spend(&app, &s, &pid, "2026-12-15", 40_000, &wants).await;
|
||
spend(&app, &s, &pid, "2026-12-31", 10_000, &culture).await;
|
||
let gone = spend(&app, &s, &pid, "2026-12-15", 99_999, &wants).await;
|
||
app.delete(&format!("/entries/{}", gone["id"].as_str().unwrap()), &s)
|
||
.await;
|
||
|
||
let (st, r) = app.get(&format!("/periods/{pid}/review"), &s).await;
|
||
assert_eq!(st, StatusCode::OK, "{r}");
|
||
// 1: 4.800.000 − 1.545.000 − 100.000 = 3.155.000 · 3: 500.000 · 4: 3.155.000 − 500.000
|
||
for (k, v) in [
|
||
("available", 3_155_000),
|
||
("savings_goal", 500_000),
|
||
("spent_total", 500_000),
|
||
("realized_savings", 2_655_000),
|
||
("savings_gap", 2_155_000),
|
||
("carryover_debt", 100_000),
|
||
] {
|
||
assert_eq!(r[k], v, "{k}: {r}");
|
||
}
|
||
assert_eq!(
|
||
r["groups"],
|
||
json!([
|
||
{ "group": "needs", "planned": 1_000_000, "spent": 450_000 },
|
||
{ "group": "wants", "planned": 500_000, "spent": 40_000 },
|
||
{ "group": "culture", "planned": 200_000, "spent": 10_000 },
|
||
{ "group": "unexpected", "planned": 100_000, "spent": 0 },
|
||
])
|
||
);
|
||
// Kategoriler çoktan aza; silinen kayıt sayılmaz
|
||
let cats = r["categories"].as_array().unwrap();
|
||
assert_eq!(
|
||
column(&r["categories"], "spent"),
|
||
vec![300_000, 150_000, 40_000, 10_000]
|
||
);
|
||
assert_eq!(
|
||
(
|
||
cats[0]["category_id"].as_str(),
|
||
cats[1]["category_id"].as_str()
|
||
),
|
||
(Some(rent.as_str()), Some(market.as_str()))
|
||
);
|
||
assert_eq!(
|
||
(cats[1]["count"].as_i64(), cats[1]["group"].as_str()),
|
||
(Some(2), Some("needs"))
|
||
);
|
||
// Haftalar: 4 grubun toplamı; planlananların toplamı aylık bütçelerin toplamı
|
||
assert_eq!(
|
||
column(&r["weeks"], "spent"),
|
||
vec![100_000, 350_000, 40_000, 0, 10_000]
|
||
);
|
||
assert_eq!(
|
||
column(&r["weeks"], "planned").iter().sum::<i64>(),
|
||
1_800_000
|
||
);
|
||
assert_eq!(r["weeks"][4]["last_date"], "2026-12-31");
|
||
|
||
app.cleanup().await;
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn overspending_gives_negative_savings() {
|
||
let app = TestApp::new().await;
|
||
let s = app.register("Test", None).await;
|
||
let pid = open_december(&app, &s, 100_000).await;
|
||
plan(&app, &s, &pid, "income", 200_000).await;
|
||
let needs = category(&app, &s, "needs", 0).await;
|
||
spend(&app, &s, &pid, "2026-12-10", 250_000, &needs).await;
|
||
|
||
let (_, r) = app.get(&format!("/periods/{pid}/review"), &s).await;
|
||
assert_eq!(
|
||
(
|
||
r["available"].as_i64(),
|
||
r["realized_savings"].as_i64(),
|
||
r["savings_gap"].as_i64()
|
||
),
|
||
(Some(200_000), Some(-50_000), Some(-150_000))
|
||
);
|
||
// Hiç bütçe yoksa: gruplar 0 planlı, haftalar yine dönem takviminden
|
||
assert_eq!(column(&r["groups"], "planned"), vec![0; 4]);
|
||
assert_eq!(r["weeks"].as_array().unwrap().len(), 5);
|
||
|
||
app.cleanup().await;
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn reflection_text_and_access_rules() {
|
||
let app = TestApp::new().await;
|
||
let s = app.register("Test", None).await;
|
||
let other = app.register("Diger", None).await;
|
||
let pid = open_december(&app, &s, 0).await;
|
||
let url = format!("/periods/{pid}");
|
||
|
||
let (st, p) = app
|
||
.patch(
|
||
&url,
|
||
&s,
|
||
json!({ "reflection_md": " **Dışarıda yemeği** azalt. " }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::OK, "{p}");
|
||
assert_eq!(p["reflection_md"], "**Dışarıda yemeği** azalt.");
|
||
let (_, p) = app.get(&url, &s).await;
|
||
assert_eq!(p["reflection_md"], "**Dışarıda yemeği** azalt.");
|
||
|
||
// Türkçe karakterler tek sayılır; sınırın üstü 400
|
||
let (st, _) = app
|
||
.patch(&url, &s, json!({ "reflection_md": "ş".repeat(20_000) }))
|
||
.await;
|
||
assert_eq!(st, StatusCode::OK);
|
||
let (st, b) = app
|
||
.patch(&url, &s, json!({ "reflection_md": "a".repeat(20_001) }))
|
||
.await;
|
||
assert_eq!(st, StatusCode::BAD_REQUEST);
|
||
assert_eq!(
|
||
b["error"]["message"],
|
||
"Metin en fazla 20.000 karakter olabilir"
|
||
);
|
||
|
||
// Boş metin → kaldırılır
|
||
let (_, p) = app.patch(&url, &s, json!({ "reflection_md": "" })).await;
|
||
assert_eq!(p["reflection_md"], Value::Null);
|
||
|
||
// Başka hane göremez
|
||
let (st, _) = app.get(&format!("{url}/review"), &other).await;
|
||
assert_eq!(st, StatusCode::NOT_FOUND);
|
||
|
||
// Kapalı dönem: değerlendirme okunur, metin değiştirilemez
|
||
app.db
|
||
.collection::<Document>("periods")
|
||
.update_one(
|
||
doc! { "_id": Uuid::parse_str(&pid).unwrap() },
|
||
doc! { "$set": { "status": "closed" } },
|
||
)
|
||
.await
|
||
.unwrap();
|
||
let (st, _) = app.get(&format!("{url}/review"), &s).await;
|
||
assert_eq!(st, StatusCode::OK);
|
||
let (st, _) = app.patch(&url, &s, json!({ "reflection_md": "geç" })).await;
|
||
assert_eq!(st, StatusCode::CONFLICT);
|
||
|
||
app.cleanup().await;
|
||
}
|