b883530f76
- /api/periods: liste, açık dönem, aç, detay, düzenle - Kurallar: tek açık dönem, çakışma yok, bitiş hariç, en fazla 62 gün, kapalı dönem salt okunur - Tarih daraltılırken dışarıda harcama kalırsa engellenir - Haftalar Pazartesi başlangıçlı, kısmi haftalar ayrı (takvim yardımcıları) - Otomatik dönem adı: "Aralık 2026" / "15 Ara 2026 – 14 Oca 2027" - Testler: takvim (4), dönem kuralları (2), entegrasyon (5) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
245 lines
7.2 KiB
Rust
245 lines
7.2 KiB
Rust
//! Dönem kuralları (KAKEIBO_PROMPT §6).
|
||
|
||
mod common;
|
||
|
||
use axum::http::StatusCode;
|
||
use common::TestApp;
|
||
use mongodb::bson::{Document, doc};
|
||
use serde_json::{Value, json};
|
||
|
||
async fn open_december(app: &TestApp, s: &common::Session) -> Value {
|
||
let (st, p) = app
|
||
.post(
|
||
"/periods",
|
||
s,
|
||
json!({ "start_date": "2026-12-01", "end_date": "2027-01-01", "savings_goal": 500000 }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CREATED, "{p}");
|
||
p
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn open_period_returns_weeks_and_default_name() {
|
||
let app = TestApp::new().await;
|
||
let s = app.register("Test", None).await;
|
||
|
||
// Başta açık dönem yok → null
|
||
let (st, cur) = app.get("/periods/current", &s).await;
|
||
assert_eq!((st, cur), (StatusCode::OK, Value::Null));
|
||
|
||
let p = open_december(&app, &s).await;
|
||
assert_eq!(p["name"], "Aralık 2026");
|
||
assert_eq!(p["status"], "open");
|
||
assert_eq!(p["days"], 31);
|
||
assert_eq!(p["last_date"], "2026-12-31");
|
||
assert_eq!(p["savings_goal"], 500000);
|
||
let days: Vec<i64> = p["weeks"]
|
||
.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.map(|w| w["days"].as_i64().unwrap())
|
||
.collect();
|
||
assert_eq!(
|
||
days,
|
||
vec![6, 7, 7, 7, 4],
|
||
"Pazartesi başlangıçlı, kısmi haftalar ayrı"
|
||
);
|
||
|
||
let (_, cur) = app.get("/periods/current", &s).await;
|
||
assert_eq!(cur["id"], p["id"]);
|
||
|
||
app.cleanup().await;
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn only_one_open_period_and_no_overlap() {
|
||
let app = TestApp::new().await;
|
||
let s = app.register("Test", None).await;
|
||
let dec = open_december(&app, &s).await;
|
||
|
||
// İkinci açık dönem → 409
|
||
let (st, body) = app
|
||
.post(
|
||
"/periods",
|
||
&s,
|
||
json!({ "start_date": "2027-01-01", "end_date": "2027-02-01" }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CONFLICT);
|
||
assert!(
|
||
body["error"]["message"]
|
||
.as_str()
|
||
.unwrap()
|
||
.contains("Aralık 2026")
|
||
);
|
||
|
||
// Aralık'ı kapat (Faz 6'da sihirbaz yapacak) → çakışan yeni dönem yine 409
|
||
app.db
|
||
.collection::<Document>("periods")
|
||
.update_one(
|
||
doc! { "name": "Aralık 2026" },
|
||
doc! { "$set": { "status": "closed" } },
|
||
)
|
||
.await
|
||
.unwrap();
|
||
let (st, body) = app
|
||
.post(
|
||
"/periods",
|
||
&s,
|
||
json!({ "start_date": "2026-12-20", "end_date": "2027-01-20" }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CONFLICT, "{body}");
|
||
assert!(
|
||
body["error"]["message"]
|
||
.as_str()
|
||
.unwrap()
|
||
.contains("çakışıyor")
|
||
);
|
||
|
||
// Bitişik dönem (bitiş hariç olduğu için 1 Ocak serbest) → 201
|
||
let (st, jan) = app
|
||
.post(
|
||
"/periods",
|
||
&s,
|
||
json!({ "start_date": "2027-01-01", "end_date": "2027-02-01" }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CREATED, "{jan}");
|
||
assert_eq!(jan["name"], "Ocak 2027");
|
||
|
||
// Liste: en yeni başta
|
||
let (_, list) = app.get("/periods", &s).await;
|
||
let names: Vec<&str> = list
|
||
.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.map(|p| p["name"].as_str().unwrap())
|
||
.collect();
|
||
assert_eq!(names, vec!["Ocak 2027", "Aralık 2026"]);
|
||
|
||
// Kapalı dönem salt okunur
|
||
let (st, _) = app
|
||
.patch(
|
||
&format!("/periods/{}", dec["id"].as_str().unwrap()),
|
||
&s,
|
||
json!({ "savings_goal": 1 }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CONFLICT);
|
||
|
||
app.cleanup().await;
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn validation_errors() {
|
||
let app = TestApp::new().await;
|
||
let s = app.register("Test", None).await;
|
||
for (start, end) in [
|
||
("2026-12-10", "2026-12-01"), // ters
|
||
("2026-12-01", "2026-12-01"), // boş
|
||
("2026-02-30", "2026-03-30"), // geçersiz gün
|
||
("01.12.2026", "2027-01-01"), // biçim
|
||
("2026-12-01", "2027-03-01"), // 90 gün
|
||
] {
|
||
let (st, _) = app
|
||
.post(
|
||
"/periods",
|
||
&s,
|
||
json!({ "start_date": start, "end_date": end }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::BAD_REQUEST, "{start} → {end}");
|
||
}
|
||
let (st, _) = app
|
||
.post(
|
||
"/periods",
|
||
&s,
|
||
json!({ "start_date": "2026-12-01", "end_date": "2027-01-01", "savings_goal": -5 }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::BAD_REQUEST, "negatif birikim");
|
||
app.cleanup().await;
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn update_dates_name_goal_and_version() {
|
||
let app = TestApp::new().await;
|
||
let s = app.register("Test", None).await;
|
||
let p = open_december(&app, &s).await;
|
||
let path = format!("/periods/{}", p["id"].as_str().unwrap());
|
||
|
||
// Maaş 15'inde → dönemi kaydır; ad otomatik güncellenmez, istenirse verilir
|
||
let (st, upd) = app
|
||
.patch(
|
||
&path,
|
||
&s,
|
||
json!({ "start_date": "2026-12-15", "end_date": "2027-01-15", "name": "", "savings_goal": 750000, "version": 1 }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::OK, "{upd}");
|
||
assert_eq!(upd["name"], "15 Ara 2026 – 14 Oca 2027");
|
||
assert_eq!(upd["savings_goal"], 750000);
|
||
assert_eq!(upd["version"], 2);
|
||
|
||
// Eski sürümle → 409
|
||
let (st, _) = app
|
||
.patch(&path, &s, json!({ "name": "X", "version": 1 }))
|
||
.await;
|
||
assert_eq!(st, StatusCode::CONFLICT);
|
||
|
||
// Aralığın dışında kalacak bir harcama varsa daraltma engellenir
|
||
let hid = mongodb::bson::Uuid::parse_str(&s.household_id).unwrap();
|
||
let pid = mongodb::bson::Uuid::parse_str(p["id"].as_str().unwrap()).unwrap();
|
||
app.db
|
||
.collection::<Document>("entries")
|
||
.insert_one(doc! { "household_id": hid, "period_id": pid, "date": "2027-01-10", "deleted_at": null })
|
||
.await
|
||
.unwrap();
|
||
let (st, body) = app
|
||
.patch(&path, &s, json!({ "end_date": "2027-01-05" }))
|
||
.await;
|
||
assert_eq!(st, StatusCode::CONFLICT, "{body}");
|
||
assert!(
|
||
body["error"]["message"]
|
||
.as_str()
|
||
.unwrap()
|
||
.contains("1 harcama")
|
||
);
|
||
|
||
app.cleanup().await;
|
||
}
|
||
|
||
#[tokio::test]
|
||
async fn periods_are_isolated_between_households() {
|
||
let app = TestApp::new().await;
|
||
let a = app.register("A", None).await;
|
||
let b = app.register("B", None).await;
|
||
let p = open_december(&app, &a).await;
|
||
let path = format!("/periods/{}", p["id"].as_str().unwrap());
|
||
|
||
// B, A'nın dönemini göremez/değiştiremez; kendi açık dönemi yok
|
||
assert_eq!(app.get(&path, &b).await.0, StatusCode::NOT_FOUND);
|
||
assert_eq!(
|
||
app.patch(&path, &b, json!({ "name": "ele geçirildi" }))
|
||
.await
|
||
.0,
|
||
StatusCode::NOT_FOUND
|
||
);
|
||
assert_eq!(app.get("/periods/current", &b).await.1, Value::Null);
|
||
assert_eq!(app.get("/periods", &b).await.1, json!([]));
|
||
|
||
// A'nın açık dönemi B'nin dönem açmasını engellemez (açık dönem kuralı hane bazlı)
|
||
let (st, _) = app
|
||
.post(
|
||
"/periods",
|
||
&b,
|
||
json!({ "start_date": "2026-12-01", "end_date": "2027-01-01" }),
|
||
)
|
||
.await;
|
||
assert_eq!(st, StatusCode::CREATED);
|
||
|
||
app.cleanup().await;
|
||
}
|