feat(dönem): dönem açma, açık dönem, düzenleme ve haftalar (backend)
- /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>
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
//! 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;
|
||||
}
|
||||
Reference in New Issue
Block a user