906f05f62f
- Backend: ts-rs ile DTO'lardan TypeScript tipleri (frontend/src/types, npm run types) - Hata ve health yanıtları tipli yapılara çevrildi - lib/api.ts: tek API istemcisi, Türkçe ApiError, ağ hatası mesajı - lib/query.ts: QueryClient (4xx tekrar denenmez, 401 → oturum kapanır) - lib/auth.ts: useMe, useLogin, useRegister, useLogout - Vitest: 7 API istemcisi testi (toplam 29) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
237 lines
6.2 KiB
Rust
237 lines
6.2 KiB
Rust
//! Kategori ve ödeme türü uç noktaları (hanenin tüm üyeleri kullanabilir):
|
||
//!
|
||
//! | Yöntem | Yol | İş |
|
||
//! |---|---|---|
|
||
//! | GET | `/api/categories?include_inactive=true` | listele |
|
||
//! | POST | `/api/categories` | ekle |
|
||
//! | PATCH | `/api/categories/{id}` | ad / emoji / aktiflik değiştir |
|
||
//! | PUT | `/api/categories/order` | sırala |
|
||
//!
|
||
//! `/api/payment-methods` aynı yapıdadır (yalnızca `group` alanı yoktur).
|
||
|
||
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::group::Group,
|
||
services::catalog::{self, Catalog, Changes, Item},
|
||
state::AppState,
|
||
};
|
||
|
||
// ---------- İstek / yanıt ----------
|
||
|
||
#[derive(Deserialize, Default)]
|
||
pub struct ListQuery {
|
||
#[serde(default)]
|
||
include_inactive: bool,
|
||
}
|
||
|
||
#[derive(Deserialize, Validate, TS)]
|
||
#[ts(export, rename = "CatalogCreateRequest", optional_fields)]
|
||
pub struct CreateRequest {
|
||
group: Option<Group>,
|
||
#[validate(length(max = 40, message = "Ad en fazla 40 karakter olabilir"))]
|
||
name: String,
|
||
#[validate(length(max = 16, message = "Emoji çok uzun"))]
|
||
emoji: Option<String>,
|
||
}
|
||
|
||
#[derive(Deserialize, Validate, TS)]
|
||
#[ts(export, rename = "CatalogUpdateRequest", optional_fields)]
|
||
pub struct UpdateRequest {
|
||
#[validate(length(max = 40, message = "Ad en fazla 40 karakter olabilir"))]
|
||
name: Option<String>,
|
||
#[validate(length(max = 16, message = "Emoji çok uzun"))]
|
||
emoji: Option<String>,
|
||
is_active: Option<bool>,
|
||
version: Option<i32>,
|
||
}
|
||
|
||
#[derive(Deserialize, Validate, TS)]
|
||
#[ts(export, rename = "CatalogOrderRequest", optional_fields)]
|
||
pub struct OrderRequest {
|
||
group: Option<Group>,
|
||
#[validate(length(max = 200, message = "Liste çok uzun"))]
|
||
ids: Vec<String>,
|
||
}
|
||
|
||
#[derive(Serialize, TS)]
|
||
#[ts(export, rename = "CatalogItem")]
|
||
pub struct ItemDto {
|
||
id: String,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
#[ts(optional)]
|
||
group: Option<Group>,
|
||
name: String,
|
||
emoji: Option<String>,
|
||
sort_order: i32,
|
||
is_active: bool,
|
||
version: i32,
|
||
}
|
||
|
||
impl From<Item> for ItemDto {
|
||
fn from(i: Item) -> Self {
|
||
Self {
|
||
id: i.id.to_string(),
|
||
group: i.group,
|
||
name: i.name,
|
||
emoji: i.emoji,
|
||
sort_order: i.sort_order,
|
||
is_active: i.is_active,
|
||
version: i.version,
|
||
}
|
||
}
|
||
}
|
||
|
||
fn dtos(items: Vec<Item>) -> Json<Vec<ItemDto>> {
|
||
Json(items.into_iter().map(ItemDto::from).collect())
|
||
}
|
||
|
||
/// URL'deki kimlik bozuksa "bulunamadı" (başka hanenin kaydıyla aynı yanıt).
|
||
fn parse_id(id: &str) -> AppResult<Uuid> {
|
||
Uuid::parse_str(id).map_err(|_| AppError::NotFound)
|
||
}
|
||
|
||
// ---------- Ortak işleyiciler ----------
|
||
|
||
async fn list(
|
||
c: Catalog,
|
||
state: AppState,
|
||
auth: AuthUser,
|
||
q: ListQuery,
|
||
) -> AppResult<Json<Vec<ItemDto>>> {
|
||
let items = catalog::list(&state.db, c, auth.household_id, q.include_inactive).await?;
|
||
Ok(dtos(items))
|
||
}
|
||
|
||
async fn create(
|
||
c: Catalog,
|
||
state: AppState,
|
||
auth: AuthUser,
|
||
req: CreateRequest,
|
||
) -> AppResult<(StatusCode, Json<ItemDto>)> {
|
||
let item = catalog::create(
|
||
&state.db,
|
||
c,
|
||
auth.household_id,
|
||
req.group,
|
||
req.name,
|
||
req.emoji,
|
||
)
|
||
.await?;
|
||
Ok((StatusCode::CREATED, Json(item.into())))
|
||
}
|
||
|
||
async fn update(
|
||
c: Catalog,
|
||
state: AppState,
|
||
auth: AuthUser,
|
||
id: String,
|
||
req: UpdateRequest,
|
||
) -> AppResult<Json<ItemDto>> {
|
||
let changes = Changes {
|
||
name: req.name,
|
||
emoji: req.emoji,
|
||
is_active: req.is_active,
|
||
version: req.version,
|
||
};
|
||
let item = catalog::update(&state.db, c, auth.household_id, parse_id(&id)?, changes).await?;
|
||
Ok(Json(item.into()))
|
||
}
|
||
|
||
async fn reorder(
|
||
c: Catalog,
|
||
state: AppState,
|
||
auth: AuthUser,
|
||
req: OrderRequest,
|
||
) -> AppResult<Json<Vec<ItemDto>>> {
|
||
let ids = req
|
||
.ids
|
||
.iter()
|
||
.map(Uuid::parse_str)
|
||
.collect::<Result<Vec<_>, _>>()
|
||
.map_err(|_| AppError::BadRequest("Sıralama listesinde geçersiz kimlik var".into()))?;
|
||
let items = catalog::reorder(&state.db, c, auth.household_id, req.group, ids).await?;
|
||
Ok(dtos(items))
|
||
}
|
||
|
||
// ---------- Kategoriler ----------
|
||
|
||
pub async fn list_categories(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Query(q): Query<ListQuery>,
|
||
) -> AppResult<Json<Vec<ItemDto>>> {
|
||
list(Catalog::Categories, s, auth, q).await
|
||
}
|
||
|
||
pub async fn create_category(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
ValidJson(req): ValidJson<CreateRequest>,
|
||
) -> AppResult<(StatusCode, Json<ItemDto>)> {
|
||
create(Catalog::Categories, s, auth, req).await
|
||
}
|
||
|
||
pub async fn update_category(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(id): Path<String>,
|
||
ValidJson(req): ValidJson<UpdateRequest>,
|
||
) -> AppResult<Json<ItemDto>> {
|
||
update(Catalog::Categories, s, auth, id, req).await
|
||
}
|
||
|
||
pub async fn reorder_categories(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
ValidJson(req): ValidJson<OrderRequest>,
|
||
) -> AppResult<Json<Vec<ItemDto>>> {
|
||
reorder(Catalog::Categories, s, auth, req).await
|
||
}
|
||
|
||
// ---------- Ödeme türleri ----------
|
||
|
||
pub async fn list_payment_methods(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Query(q): Query<ListQuery>,
|
||
) -> AppResult<Json<Vec<ItemDto>>> {
|
||
list(Catalog::PaymentMethods, s, auth, q).await
|
||
}
|
||
|
||
pub async fn create_payment_method(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
ValidJson(req): ValidJson<CreateRequest>,
|
||
) -> AppResult<(StatusCode, Json<ItemDto>)> {
|
||
create(Catalog::PaymentMethods, s, auth, req).await
|
||
}
|
||
|
||
pub async fn update_payment_method(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
Path(id): Path<String>,
|
||
ValidJson(req): ValidJson<UpdateRequest>,
|
||
) -> AppResult<Json<ItemDto>> {
|
||
update(Catalog::PaymentMethods, s, auth, id, req).await
|
||
}
|
||
|
||
pub async fn reorder_payment_methods(
|
||
State(s): State<AppState>,
|
||
auth: AuthUser,
|
||
ValidJson(req): ValidJson<OrderRequest>,
|
||
) -> AppResult<Json<Vec<ItemDto>>> {
|
||
reorder(Catalog::PaymentMethods, s, auth, req).await
|
||
}
|