2003b8fad7
- /periods/{id}/entries (from/to filtresi), /entries/{id}, /entries/{id}/restore
- Grup kategoriden türetilir; kategori/ödeme türü hane ve aktiflik kontrolü
- Yalnızca açık dönem; soft delete + geri al; sürüm çakışmasında 409
- 6 entegrasyon testi (izolasyon, kapalı dönem, doğrulama, önceki dönem önerisi)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
204 lines
5.5 KiB
Rust
204 lines
5.5 KiB
Rust
//! Günlük defter uç noktaları (hanenin tüm üyeleri):
|
||
//!
|
||
//! | Yöntem | Yol | İş |
|
||
//! |---|---|---|
|
||
//! | GET | `/api/periods/{id}/entries?from=&to=` | dönemin kayıtları (tarih aralığı isteğe bağlı, ikisi de dahil) |
|
||
//! | POST | `/api/periods/{id}/entries` | harcama ekle |
|
||
//! | PATCH | `/api/entries/{id}` | düzenle |
|
||
//! | DELETE | `/api/entries/{id}` | sil (soft) |
|
||
//! | POST | `/api/entries/{id}/restore` | silmeyi geri al |
|
||
|
||
use axum::{
|
||
Json,
|
||
extract::{Path, Query, State},
|
||
http::StatusCode,
|
||
};
|
||
use mongodb::bson::Uuid;
|
||
use serde::{Deserialize, Serialize};
|
||
use ts_rs::TS;
|
||
use validator::Validate;
|
||
|
||
use crate::{
|
||
auth::AuthUser,
|
||
error::{AppError, AppResult},
|
||
extract::ValidJson,
|
||
models::{entry::Entry, group::Group},
|
||
services::entry::{self, EntryChanges, EntryFilter, NewEntry},
|
||
state::AppState,
|
||
};
|
||
|
||
#[derive(Deserialize, Validate, TS)]
|
||
#[ts(export, rename = "EntryCreateRequest", optional_fields)]
|
||
pub struct CreateRequest {
|
||
/// `"YYYY-MM-DD"`, dönem içinde
|
||
date: String,
|
||
/// Kuruş, > 0
|
||
#[ts(type = "number")]
|
||
amount: i64,
|
||
/// Grup kategoriden alınır
|
||
category_id: String,
|
||
payment_method_id: Option<String>,
|
||
description: Option<String>,
|
||
}
|
||
|
||
#[derive(Deserialize, Validate, TS)]
|
||
#[ts(export, rename = "EntryUpdateRequest", optional_fields)]
|
||
pub struct UpdateRequest {
|
||
date: Option<String>,
|
||
#[ts(type = "number", optional)]
|
||
amount: Option<i64>,
|
||
category_id: Option<String>,
|
||
/// Boş metin → ödeme türünü kaldır
|
||
payment_method_id: Option<String>,
|
||
/// Boş metin → açıklamayı kaldır
|
||
description: Option<String>,
|
||
version: Option<i32>,
|
||
}
|
||
|
||
#[derive(Deserialize)]
|
||
pub struct ListQuery {
|
||
from: Option<String>,
|
||
to: Option<String>,
|
||
}
|
||
|
||
#[derive(Serialize, TS)]
|
||
#[ts(export, rename = "Entry")]
|
||
pub struct EntryDto {
|
||
id: String,
|
||
period_id: String,
|
||
date: String,
|
||
/// Kuruş
|
||
#[ts(type = "number")]
|
||
amount: i64,
|
||
group: Group,
|
||
category_id: String,
|
||
payment_method_id: Option<String>,
|
||
description: Option<String>,
|
||
created_by: String,
|
||
version: i32,
|
||
}
|
||
|
||
impl From<Entry> for EntryDto {
|
||
fn from(e: Entry) -> Self {
|
||
Self {
|
||
id: e.id.to_string(),
|
||
period_id: e.period_id.to_string(),
|
||
date: e.date,
|
||
amount: e.amount,
|
||
group: e.group,
|
||
category_id: e.category_id.to_string(),
|
||
payment_method_id: e.payment_method_id.map(|u| u.to_string()),
|
||
description: e.description,
|
||
created_by: e.created_by.to_string(),
|
||
version: e.version,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Adresteki kimlik → geçersizse 404
|
||
fn path_id(id: &str) -> AppResult<Uuid> {
|
||
Uuid::parse_str(id).map_err(|_| AppError::NotFound)
|
||
}
|
||
|
||
/// Gövdedeki kimlik → geçersizse 400 (ör. kategori seçilmemiş)
|
||
fn body_id(id: &str, what: &str) -> AppResult<Uuid> {
|
||
Uuid::parse_str(id.trim()).map_err(|_| AppError::BadRequest(format!("{what} geçersiz")))
|
||
}
|
||
|
||
pub async fn list(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(period_id): Path<String>,
|
||
Query(q): Query<ListQuery>,
|
||
) -> AppResult<Json<Vec<EntryDto>>> {
|
||
let items = entry::list(
|
||
&s.db,
|
||
auth.household_id,
|
||
path_id(&period_id)?,
|
||
EntryFilter {
|
||
from: q.from,
|
||
to: q.to,
|
||
},
|
||
)
|
||
.await?;
|
||
Ok(Json(items.into_iter().map(EntryDto::from).collect()))
|
||
}
|
||
|
||
pub async fn create(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(period_id): Path<String>,
|
||
ValidJson(req): ValidJson<CreateRequest>,
|
||
) -> AppResult<(StatusCode, Json<EntryDto>)> {
|
||
let payment_method_id = match req.payment_method_id.as_deref().map(str::trim) {
|
||
None | Some("") => None,
|
||
Some(id) => Some(body_id(id, "Ödeme türü")?),
|
||
};
|
||
let e = entry::create(
|
||
&s.db,
|
||
auth.household_id,
|
||
auth.user_id,
|
||
path_id(&period_id)?,
|
||
NewEntry {
|
||
date: req.date,
|
||
amount: req.amount,
|
||
category_id: body_id(&req.category_id, "Kategori")?,
|
||
payment_method_id,
|
||
description: req.description,
|
||
},
|
||
)
|
||
.await?;
|
||
Ok((StatusCode::CREATED, Json(e.into())))
|
||
}
|
||
|
||
pub async fn update(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(id): Path<String>,
|
||
ValidJson(req): ValidJson<UpdateRequest>,
|
||
) -> AppResult<Json<EntryDto>> {
|
||
let payment_method_id = match req.payment_method_id.as_deref().map(str::trim) {
|
||
None => None,
|
||
Some("") => Some(None),
|
||
Some(pm) => Some(Some(body_id(pm, "Ödeme türü")?)),
|
||
};
|
||
let category_id = req
|
||
.category_id
|
||
.as_deref()
|
||
.map(|c| body_id(c, "Kategori"))
|
||
.transpose()?;
|
||
let e = entry::update(
|
||
&s.db,
|
||
auth.household_id,
|
||
path_id(&id)?,
|
||
EntryChanges {
|
||
date: req.date,
|
||
amount: req.amount,
|
||
category_id,
|
||
payment_method_id,
|
||
description: req.description,
|
||
version: req.version,
|
||
},
|
||
)
|
||
.await?;
|
||
Ok(Json(e.into()))
|
||
}
|
||
|
||
pub async fn delete(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(id): Path<String>,
|
||
) -> AppResult<StatusCode> {
|
||
entry::delete(&s.db, auth.household_id, path_id(&id)?).await?;
|
||
Ok(StatusCode::NO_CONTENT)
|
||
}
|
||
|
||
pub async fn restore(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(id): Path<String>,
|
||
) -> AppResult<Json<EntryDto>> {
|
||
let e = entry::restore(&s.db, auth.household_id, path_id(&id)?).await?;
|
||
Ok(Json(e.into()))
|
||
}
|