184 lines
5.3 KiB
Rust
184 lines
5.3 KiB
Rust
use sea_orm::*;
|
|
use sea_orm::prelude::Decimal;
|
|
use chrono::{Utc, Datelike};
|
|
use std::collections::HashMap;
|
|
use crate::models::expense::{self, Entity as Expense, Model as ExpenseModel};
|
|
use crate::models::category::{Entity as Category};
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct MonthlyExpenseGroup {
|
|
pub year: i32,
|
|
pub month: u32,
|
|
pub total_amount: Decimal,
|
|
pub expenses: Vec<ExpenseModel>,
|
|
}
|
|
|
|
pub struct ExpenseService;
|
|
|
|
impl ExpenseService {
|
|
pub async fn create(
|
|
db: &DatabaseConnection,
|
|
category_id: i32,
|
|
amount: Decimal,
|
|
description: Option<String>,
|
|
) -> Result<ExpenseModel, DbErr> {
|
|
let expense = expense::ActiveModel {
|
|
category_id: Set(category_id),
|
|
amount: Set(amount),
|
|
description: Set(description),
|
|
created_at: Set(Utc::now().naive_utc()),
|
|
active: Set(true),
|
|
..Default::default()
|
|
};
|
|
|
|
expense.insert(db).await
|
|
}
|
|
|
|
pub async fn find_by_id(db: &DatabaseConnection, id: i32) -> Result<Option<ExpenseModel>, DbErr> {
|
|
Expense::find_by_id(id).one(db).await
|
|
}
|
|
|
|
pub async fn find_all(db: &DatabaseConnection) -> Result<Vec<ExpenseModel>, DbErr> {
|
|
Expense::find().all(db).await
|
|
}
|
|
|
|
pub async fn find_by_category_id(
|
|
db: &DatabaseConnection,
|
|
category_id: i32,
|
|
) -> Result<Vec<ExpenseModel>, DbErr> {
|
|
Expense::find()
|
|
.filter(expense::Column::CategoryId.eq(category_id))
|
|
.filter(expense::Column::Active.eq(true))
|
|
.all(db)
|
|
.await
|
|
}
|
|
|
|
pub async fn find_all_by_category_id(
|
|
db: &DatabaseConnection,
|
|
category_id: i32,
|
|
) -> Result<Vec<ExpenseModel>, DbErr> {
|
|
Expense::find()
|
|
.filter(expense::Column::CategoryId.eq(category_id))
|
|
.all(db)
|
|
.await
|
|
}
|
|
|
|
pub async fn find_inactive_by_category_id(
|
|
db: &DatabaseConnection,
|
|
category_id: i32,
|
|
) -> Result<Vec<ExpenseModel>, DbErr> {
|
|
Expense::find()
|
|
.filter(expense::Column::CategoryId.eq(category_id))
|
|
.filter(expense::Column::Active.eq(false))
|
|
.all(db)
|
|
.await
|
|
}
|
|
|
|
pub async fn update(
|
|
db: &DatabaseConnection,
|
|
id: i32,
|
|
amount: Option<Decimal>,
|
|
description: Option<String>,
|
|
) -> Result<ExpenseModel, DbErr> {
|
|
let expense = Expense::find_by_id(id)
|
|
.one(db)
|
|
.await?
|
|
.ok_or(DbErr::RecordNotFound("Expense not found".to_string()))?;
|
|
|
|
let mut expense: expense::ActiveModel = expense.into();
|
|
|
|
if let Some(amount) = amount {
|
|
expense.amount = Set(amount);
|
|
}
|
|
|
|
if let Some(desc) = description {
|
|
expense.description = Set(Some(desc));
|
|
}
|
|
|
|
expense.update(db).await
|
|
}
|
|
|
|
pub async fn delete(db: &DatabaseConnection, id: i32) -> Result<ExpenseModel, DbErr> {
|
|
let expense = Expense::find_by_id(id)
|
|
.one(db)
|
|
.await?
|
|
.ok_or(DbErr::RecordNotFound("Expense not found".to_string()))?;
|
|
|
|
let mut expense: expense::ActiveModel = expense.into();
|
|
expense.active = Set(false);
|
|
expense.update(db).await
|
|
}
|
|
|
|
pub async fn calculate_remaining_limit(
|
|
db: &DatabaseConnection,
|
|
category_id: i32,
|
|
) -> Result<Decimal, DbErr> {
|
|
let category = Category::find_by_id(category_id)
|
|
.one(db)
|
|
.await?
|
|
.ok_or(DbErr::RecordNotFound("Category not found".to_string()))?;
|
|
|
|
let expenses = Self::find_by_category_id(db, category_id).await?;
|
|
|
|
let total_spent: Decimal = expenses.iter().map(|e| e.amount).sum();
|
|
let remaining = category.limit_amount - total_spent;
|
|
|
|
Ok(remaining)
|
|
}
|
|
|
|
pub async fn get_expense_history(
|
|
db: &DatabaseConnection,
|
|
category_id: i32,
|
|
sort_order: Option<String>,
|
|
show_archive: bool,
|
|
) -> Result<Vec<MonthlyExpenseGroup>, DbErr> {
|
|
let expenses = if show_archive {
|
|
Self::find_inactive_by_category_id(db, category_id).await?
|
|
} else {
|
|
Self::find_by_category_id(db, category_id).await?
|
|
};
|
|
|
|
let mut grouped: HashMap<(i32, u32), Vec<ExpenseModel>> = HashMap::new();
|
|
|
|
for expense in expenses {
|
|
let year = expense.created_at.year();
|
|
let month = expense.created_at.month();
|
|
grouped.entry((year, month))
|
|
.or_insert_with(Vec::new)
|
|
.push(expense);
|
|
}
|
|
|
|
let mut result: Vec<MonthlyExpenseGroup> = grouped
|
|
.into_iter()
|
|
.map(|((year, month), expenses)| {
|
|
let total_amount: Decimal = expenses
|
|
.iter()
|
|
.map(|e| e.amount)
|
|
.sum();
|
|
MonthlyExpenseGroup {
|
|
year,
|
|
month,
|
|
total_amount,
|
|
expenses,
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let sort_desc = sort_order
|
|
.as_deref()
|
|
.map(|s| s.to_lowercase() == "desc")
|
|
.unwrap_or(true);
|
|
|
|
result.sort_by(|a, b| {
|
|
let cmp = a.year.cmp(&b.year).then(a.month.cmp(&b.month));
|
|
if sort_desc {
|
|
cmp.reverse()
|
|
} else {
|
|
cmp
|
|
}
|
|
});
|
|
|
|
Ok(result)
|
|
}
|
|
}
|