refactor: account category service into dto, repo and service

This commit is contained in:
GW_MC
2026-05-28 10:27:17 +00:00
parent be74bf5fc1
commit 50cafa8341
4 changed files with 405 additions and 0 deletions

View File

@@ -0,0 +1,141 @@
use sea_orm::ActiveValue::Set;
use serde::{Deserialize, Serialize};
use crate::db::{
DbServiceError,
entities::account_category::{
ActiveModel as AccountCategoryActiveModel, Model as AccountCategoryModel,
},
};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AccountCategoryServiceError {
#[error("database error: {0}")]
DbErr(#[from] DbServiceError),
#[error("Target not found: {0}")]
TargetNotFound(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("unknown error: {0}")]
Unknown(String),
}
impl From<sea_orm::DbErr> for AccountCategoryServiceError {
fn from(err: sea_orm::DbErr) -> Self {
AccountCategoryServiceError::DbErr(DbServiceError::from(err))
}
}
pub struct AccountCategory {
pub id: i64,
pub name: String,
pub account_type: AccountType,
//
pub created_at: String,
pub updated_at: String,
}
impl From<AccountCategoryModel> for AccountCategory {
fn from(model: AccountCategoryModel) -> Self {
Self {
id: model.id,
name: model.name,
account_type: AccountType::from(model.account_type),
created_at: model.created_at,
updated_at: model.updated_at,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AccountType {
#[serde(rename = "Asset")]
Asset, // Positive balance, e.g. Checking, Savings
#[serde(rename = "Liability")]
Liability, // Negative balance, e.g. Credit Card
#[serde(other, rename = "Unknown")]
Unknown, // Fallback for unknown types
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CreateAccountCategoryRequest {
pub name: String,
pub account_type: AccountType,
}
impl CreateAccountCategoryRequest {
pub fn validate(&self) -> Result<(), AccountCategoryServiceError> {
if self.name.trim().is_empty() {
return Err(AccountCategoryServiceError::Validation(
"Name cannot be empty".to_string(),
));
}
if let AccountType::Unknown = self.account_type {
return Err(AccountCategoryServiceError::Validation(
"Invalid account type".to_string(),
));
}
Ok(())
}
}
impl From<CreateAccountCategoryRequest> for AccountCategoryActiveModel {
fn from(request: CreateAccountCategoryRequest) -> Self {
AccountCategoryActiveModel {
name: Set(request.name),
account_type: Set(request.account_type.to_string()),
created_at: Set(chrono::Utc::now().to_rfc3339()),
updated_at: Set(chrono::Utc::now().to_rfc3339()),
..Default::default()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpdateAccountCategoryRequest {
pub name: Option<String>,
pub account_type: Option<AccountType>,
}
impl UpdateAccountCategoryRequest {
pub fn validate(&self) -> Result<(), AccountCategoryServiceError> {
if let Some(name) = &self.name {
if name.trim().is_empty() {
return Err(AccountCategoryServiceError::Validation(
"Name cannot be empty".to_string(),
));
}
}
Ok(())
}
pub fn apply_to(self, transaction: &mut AccountCategoryActiveModel) {
if let Some(name) = self.name {
transaction.name = Set(name);
}
if let Some(account_type) = self.account_type {
transaction.account_type = Set(account_type.to_string());
}
}
}
impl From<String> for AccountType {
fn from(s: String) -> Self {
match s.as_str() {
"Asset" => AccountType::Asset,
"Liability" => AccountType::Liability,
_ => AccountType::Unknown,
}
}
}
impl std::fmt::Display for AccountType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AccountType::Asset => write!(f, "Asset"),
AccountType::Liability => write!(f, "Liability"),
AccountType::Unknown => write!(f, "Unknown"),
}
}
}

View File

@@ -0,0 +1,6 @@
mod dto;
mod repo;
mod service;
pub use dto::*;
pub use service::*;

View File

@@ -0,0 +1,151 @@
use crate::{
db::{
Db,
entities::account_category::{
ActiveModel as AccountCategoryActiveModel, Entity as AccountCategoryEntity,
},
},
services::accounts::category::dto::{
AccountCategory, AccountCategoryServiceError, CreateAccountCategoryRequest,
UpdateAccountCategoryRequest,
},
};
use migration::OnConflict;
use sea_orm::{
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait,
IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter,
};
#[async_trait::async_trait]
pub trait AccountCategoryRepoService: Send + Sync + 'static {
async fn get_categories(&self) -> Result<Vec<AccountCategory>, AccountCategoryServiceError>;
async fn create_category(
&self,
create_request: CreateAccountCategoryRequest,
) -> Result<i64, AccountCategoryServiceError>;
async fn create_category_with_tx(
&self,
create_request: CreateAccountCategoryRequest,
tx: &Db,
) -> Result<i64, AccountCategoryServiceError>;
async fn update_category(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
) -> Result<(), AccountCategoryServiceError>;
async fn update_category_with_tx(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
tx: &Db,
) -> Result<(), AccountCategoryServiceError>;
async fn delete_category(&self, id: &i64) -> Result<(), AccountCategoryServiceError>;
}
pub struct AccountCategoryRepoServiceImpl {
db: DatabaseConnection,
}
impl AccountCategoryRepoServiceImpl {
pub fn new(db: DatabaseConnection) -> Self {
Self { db }
}
}
#[async_trait::async_trait]
impl AccountCategoryRepoService for AccountCategoryRepoServiceImpl {
async fn get_categories(&self) -> Result<Vec<AccountCategory>, AccountCategoryServiceError> {
let categories = AccountCategoryEntity::find().all(&self.db).await?;
Ok(categories.into_iter().map(|model| model.into()).collect())
}
async fn create_category(
&self,
create_request: CreateAccountCategoryRequest,
) -> Result<i64, AccountCategoryServiceError> {
self.create_category_with_tx(create_request, &(&self.db).into())
.await
}
async fn create_category_with_tx(
&self,
create_request: CreateAccountCategoryRequest,
tx: &Db,
) -> Result<i64, AccountCategoryServiceError> {
create_request.validate()?;
let new_category: AccountCategoryActiveModel = create_request.into();
let res = AccountCategoryEntity::insert(new_category)
.on_conflict(
// force update to ensure a record is returned
OnConflict::column(crate::db::entities::account_category::Column::Name)
.update_column(crate::db::entities::account_category::Column::Name)
.to_owned(),
)
.exec_with_returning(tx)
.await?;
Ok(res.id)
}
async fn delete_category(&self, id: &i64) -> Result<(), AccountCategoryServiceError> {
// check if any accounts are using this category before deleting
let accounts_using_category = crate::db::entities::account::Entity::find()
.filter(crate::db::entities::account::Column::AccountCategoryId.eq(*id))
.count(&self.db)
.await?;
if accounts_using_category > 0 {
return Err(AccountCategoryServiceError::Validation(
"Cannot delete category that has accounts".to_string(),
));
}
let category = AccountCategoryEntity::find_by_id(*id).one(&self.db).await?;
if let Some(category) = category {
category.delete(&self.db).await?;
Ok(())
} else {
Err(AccountCategoryServiceError::TargetNotFound(
"Category not found".to_string(),
))
}
}
async fn update_category(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
) -> Result<(), AccountCategoryServiceError> {
self.update_category_with_tx(id, update_request, &(&self.db).into())
.await
}
async fn update_category_with_tx(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
tx: &Db,
) -> Result<(), AccountCategoryServiceError> {
update_request.validate()?;
let category = AccountCategoryEntity::find_by_id(*id)
.one(&self.db)
.await
.map(|opt| opt.map(|model| model.into_active_model()))?;
if let Some(mut category) = category {
update_request.apply_to(&mut category);
category.updated_at = Set(chrono::Utc::now().to_rfc3339());
category.update(tx).await?;
Ok(())
} else {
Err(AccountCategoryServiceError::TargetNotFound(
"Category not found".to_string(),
))
}
}
}

View File

@@ -0,0 +1,107 @@
use std::sync::Arc;
use crate::{
db::Db,
services::accounts::category::{
dto::{
AccountCategory, AccountCategoryServiceError, CreateAccountCategoryRequest,
UpdateAccountCategoryRequest,
},
repo::AccountCategoryRepoService,
},
};
use sea_orm::DatabaseConnection;
#[async_trait::async_trait]
pub trait AccountCategoryService: Send + Sync + 'static {
async fn get_categories(&self) -> Result<Vec<AccountCategory>, AccountCategoryServiceError>;
async fn create_category(
&self,
create_request: CreateAccountCategoryRequest,
) -> Result<i64, AccountCategoryServiceError>;
async fn create_category_with_tx(
&self,
create_request: CreateAccountCategoryRequest,
tx: &Db,
) -> Result<i64, AccountCategoryServiceError>;
async fn update_category(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
) -> Result<(), AccountCategoryServiceError>;
async fn update_category_with_tx(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
tx: &Db,
) -> Result<(), AccountCategoryServiceError>;
async fn delete_category(&self, id: &i64) -> Result<(), AccountCategoryServiceError>;
}
pub struct AccountCategoryServiceImpl<T>
where
T: super::repo::AccountCategoryRepoService + ?Sized,
{
db: DatabaseConnection,
repo: Arc<T>,
}
impl AccountCategoryServiceImpl<super::repo::AccountCategoryRepoServiceImpl> {
pub fn new(db: DatabaseConnection) -> Self {
Self {
db: db.clone(),
repo: Arc::new(super::repo::AccountCategoryRepoServiceImpl::new(db.clone())),
}
}
}
#[async_trait::async_trait]
impl AccountCategoryService
for AccountCategoryServiceImpl<super::repo::AccountCategoryRepoServiceImpl>
{
async fn get_categories(&self) -> Result<Vec<AccountCategory>, AccountCategoryServiceError> {
self.repo.get_categories().await
}
async fn create_category(
&self,
create_request: CreateAccountCategoryRequest,
) -> Result<i64, AccountCategoryServiceError> {
self.repo.create_category(create_request).await
}
async fn create_category_with_tx(
&self,
create_request: CreateAccountCategoryRequest,
tx: &Db,
) -> Result<i64, AccountCategoryServiceError> {
self.repo.create_category_with_tx(create_request, tx).await
}
async fn delete_category(&self, id: &i64) -> Result<(), AccountCategoryServiceError> {
self.repo.delete_category(id).await
}
async fn update_category(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
) -> Result<(), AccountCategoryServiceError> {
self.repo.update_category(id, update_request).await
}
async fn update_category_with_tx(
&self,
id: &i64,
update_request: UpdateAccountCategoryRequest,
tx: &Db,
) -> Result<(), AccountCategoryServiceError> {
self.repo
.update_category_with_tx(id, update_request, tx)
.await
}
}