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, } pub struct ExpenseService; impl ExpenseService { pub async fn create( db: &DatabaseConnection, category_id: i32, amount: Decimal, description: Option, ) -> Result { 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, DbErr> { Expense::find_by_id(id).one(db).await } pub async fn find_all(db: &DatabaseConnection) -> Result, DbErr> { Expense::find().all(db).await } pub async fn find_by_category_id( db: &DatabaseConnection, category_id: i32, ) -> Result, 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, 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, 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, description: Option, ) -> Result { 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 { 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 { 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, show_archive: bool, ) -> Result, 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> = 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 = grouped .into_iter() .map(|((year, month), mut expenses)| { let total_amount: Decimal = expenses .iter() .map(|e| e.amount) .sum(); expenses.sort_by(|a, b| b.created_at.cmp(&a.created_at)); 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) } }