feat(katalog): kategori ve ödeme türü uç noktaları, hane izolasyonu entegrasyon testleri
- /api/categories ve /api/payment-methods: listele, ekle, düzenle, sırala, pasife al - Sürüm kontrolü ile eşzamanlı düzenleme çakışması (409) - Backend lib + bin yapısına ayrıldı (entegrasyon testleri için) - tests/: hane izolasyonu (3) ve katalog (4) entegrasyon testleri - db:reset artık artık test veritabanlarını da siler - api.http tamamlandı; Faz 1 tamamlandı Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
//! 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 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)]
|
||||
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)]
|
||||
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)]
|
||||
pub struct OrderRequest {
|
||||
group: Option<Group>,
|
||||
#[validate(length(max = 200, message = "Liste çok uzun"))]
|
||||
ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ItemDto {
|
||||
id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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
|
||||
}
|
||||
@@ -1,16 +1,18 @@
|
||||
//! API route'larının tek giriş noktası. Tüm uç noktalar `/api` altında.
|
||||
|
||||
mod auth;
|
||||
mod catalog;
|
||||
mod health;
|
||||
mod household;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
routing::{delete, get, post},
|
||||
routing::{delete, get, patch, post, put},
|
||||
};
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// Tüm API route'ları. Testler de bu fonksiyonu kullanır.
|
||||
pub fn router(state: AppState) -> Router {
|
||||
let api = Router::new()
|
||||
.route("/health", get(health::health))
|
||||
@@ -23,7 +25,25 @@ pub fn router(state: AppState) -> Router {
|
||||
"/household/invites",
|
||||
get(household::list_invites).post(household::create_invite),
|
||||
)
|
||||
.route("/household/invites/{id}", delete(household::revoke_invite));
|
||||
.route("/household/invites/{id}", delete(household::revoke_invite))
|
||||
.route(
|
||||
"/categories",
|
||||
get(catalog::list_categories).post(catalog::create_category),
|
||||
)
|
||||
.route("/categories/order", put(catalog::reorder_categories))
|
||||
.route("/categories/{id}", patch(catalog::update_category))
|
||||
.route(
|
||||
"/payment-methods",
|
||||
get(catalog::list_payment_methods).post(catalog::create_payment_method),
|
||||
)
|
||||
.route(
|
||||
"/payment-methods/order",
|
||||
put(catalog::reorder_payment_methods),
|
||||
)
|
||||
.route(
|
||||
"/payment-methods/{id}",
|
||||
patch(catalog::update_payment_method),
|
||||
);
|
||||
|
||||
Router::new().nest("/api", api).with_state(state)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user