Files
kakeibo/backend/src/routes/entry.rs
T
suphicakir 2003b8fad7 feat(defter): harcama kayıtları — ekle, düzenle, sil, geri al (backend)
- /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>
2026-11-12 09:07:51 +03:00

204 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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()))
}