раскидал структуру для монорепозитория

This commit is contained in:
arrelin
2025-12-09 18:31:45 +03:00
parent 7e1e89424a
commit aadbc099b0
48 changed files with 4048 additions and 5 deletions

View File

@@ -0,0 +1,266 @@
use axum::{
extract::{Path, State},
http::StatusCode,
Json,
};
use sea_orm::{prelude::Decimal, DatabaseConnection};
use serde::{Deserialize, Serialize};
use serde_json::json;
use utoipa::ToSchema;
use crate::models::expense::Model as ExpenseModel;
use crate::services::{CategoryService, ExpenseService};
#[derive(Debug, Deserialize, ToSchema)]
#[schema(example = json!({"amount": 150.50, "description": "Weekly grocery shopping"}))]
pub struct CreateExpenseRequest {
pub amount: Decimal,
pub description: Option<String>,
}
#[derive(Debug, Deserialize, ToSchema)]
#[schema(example = json!({"amount": 200.00, "description": "Updated expense description"}))]
pub struct UpdateExpenseRequest {
pub amount: Option<Decimal>,
pub description: Option<String>,
}
#[derive(Debug, Serialize, ToSchema)]
#[schema(example = json!({"category_id": 1, "remaining_limit": 4500.00}))]
pub struct RemainingLimitResponse {
pub category_id: i32,
pub remaining_limit: Decimal,
}
#[utoipa::path(
post,
path = "/families/{family_id}/categories/{category_id}/expenses",
tag = "expenses",
params(
("family_id" = i32, Path, description = "Family ID"),
("category_id" = i32, Path, description = "Category ID")
),
request_body = CreateExpenseRequest,
responses(
(status = 200, description = "Expense created successfully", body = ExpenseModel),
(status = 404, description = "Category not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn create_expense(
State(db): State<DatabaseConnection>,
Path((family_id, category_id)): Path<(i32, i32)>,
Json(payload): Json<CreateExpenseRequest>,
) -> Result<Json<ExpenseModel>, StatusCode> {
let category = CategoryService::find_by_id(&db, category_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if category.family_id != family_id {
return Err(StatusCode::NOT_FOUND);
}
ExpenseService::create(&db, category_id, payload.amount, payload.description)
.await
.map(Json)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
#[utoipa::path(
get,
path = "/families/{family_id}/categories/{category_id}/expenses/{expense_id}",
tag = "expenses",
params(
("family_id" = i32, Path, description = "Family ID"),
("category_id" = i32, Path, description = "Category ID"),
("expense_id" = i32, Path, description = "Expense ID")
),
responses(
(status = 200, description = "Expense found", body = ExpenseModel),
(status = 404, description = "Expense not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_expense(
State(db): State<DatabaseConnection>,
Path((family_id, category_id, expense_id)): Path<(i32, i32, i32)>,
) -> Result<Json<ExpenseModel>, StatusCode> {
let category = CategoryService::find_by_id(&db, category_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if category.family_id != family_id {
return Err(StatusCode::NOT_FOUND);
}
let expense = ExpenseService::find_by_id(&db, expense_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if expense.category_id != category_id {
return Err(StatusCode::NOT_FOUND);
}
Ok(Json(expense))
}
#[utoipa::path(
get,
path = "/families/{family_id}/categories/{category_id}/expenses",
tag = "expenses",
params(
("family_id" = i32, Path, description = "Family ID"),
("category_id" = i32, Path, description = "Category ID")
),
responses(
(status = 200, description = "List of expenses for the category", body = Vec<ExpenseModel>),
(status = 404, description = "Category not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_expenses_by_category(
State(db): State<DatabaseConnection>,
Path((family_id, category_id)): Path<(i32, i32)>,
) -> Result<Json<Vec<ExpenseModel>>, StatusCode> {
let category = CategoryService::find_by_id(&db, category_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if category.family_id != family_id {
return Err(StatusCode::NOT_FOUND);
}
ExpenseService::find_by_category_id(&db, category_id)
.await
.map(Json)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
#[utoipa::path(
put,
path = "/families/{family_id}/categories/{category_id}/expenses/{expense_id}",
tag = "expenses",
params(
("family_id" = i32, Path, description = "Family ID"),
("category_id" = i32, Path, description = "Category ID"),
("expense_id" = i32, Path, description = "Expense ID")
),
request_body = UpdateExpenseRequest,
responses(
(status = 200, description = "Expense updated successfully", body = ExpenseModel),
(status = 404, description = "Expense not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn update_expense(
State(db): State<DatabaseConnection>,
Path((family_id, category_id, expense_id)): Path<(i32, i32, i32)>,
Json(payload): Json<UpdateExpenseRequest>,
) -> Result<Json<ExpenseModel>, StatusCode> {
let category = CategoryService::find_by_id(&db, category_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if category.family_id != family_id {
return Err(StatusCode::NOT_FOUND);
}
let expense = ExpenseService::find_by_id(&db, expense_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if expense.category_id != category_id {
return Err(StatusCode::NOT_FOUND);
}
ExpenseService::update(&db, expense_id, payload.amount, payload.description)
.await
.map(Json)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
#[utoipa::path(
delete,
path = "/families/{family_id}/categories/{category_id}/expenses/{expense_id}",
tag = "expenses",
params(
("family_id" = i32, Path, description = "Family ID"),
("category_id" = i32, Path, description = "Category ID"),
("expense_id" = i32, Path, description = "Expense ID")
),
responses(
(status = 204, description = "Expense deleted successfully"),
(status = 404, description = "Expense not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn delete_expense(
State(db): State<DatabaseConnection>,
Path((family_id, category_id, expense_id)): Path<(i32, i32, i32)>,
) -> Result<StatusCode, StatusCode> {
let category = CategoryService::find_by_id(&db, category_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if category.family_id != family_id {
return Err(StatusCode::NOT_FOUND);
}
let expense = ExpenseService::find_by_id(&db, expense_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if expense.category_id != category_id {
return Err(StatusCode::NOT_FOUND);
}
ExpenseService::delete(&db, expense_id)
.await
.map(|_| StatusCode::NO_CONTENT)
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
#[utoipa::path(
get,
path = "/families/{family_id}/categories/{category_id}/remaining",
tag = "expenses",
params(
("family_id" = i32, Path, description = "Family ID"),
("category_id" = i32, Path, description = "Category ID")
),
responses(
(status = 200, description = "Remaining budget limit", body = RemainingLimitResponse),
(status = 404, description = "Category not found"),
(status = 500, description = "Internal server error")
)
)]
pub async fn get_remaining_limit(
State(db): State<DatabaseConnection>,
Path((family_id, category_id)): Path<(i32, i32)>,
) -> Result<Json<RemainingLimitResponse>, StatusCode> {
let category = CategoryService::find_by_id(&db, category_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
.ok_or(StatusCode::NOT_FOUND)?;
if category.family_id != family_id {
return Err(StatusCode::NOT_FOUND);
}
let remaining = ExpenseService::calculate_remaining_limit(&db, category_id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(Json(RemainingLimitResponse {
category_id,
remaining_limit: remaining,
}))
}