refactor: account category service into dto, repo and service
This commit is contained in:
141
src-tauri/src/services/accounts/category/dto.rs
Normal file
141
src-tauri/src/services/accounts/category/dto.rs
Normal 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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
6
src-tauri/src/services/accounts/category/mod.rs
Normal file
6
src-tauri/src/services/accounts/category/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod dto;
|
||||
mod repo;
|
||||
mod service;
|
||||
|
||||
pub use dto::*;
|
||||
pub use service::*;
|
||||
151
src-tauri/src/services/accounts/category/repo.rs
Normal file
151
src-tauri/src/services/accounts/category/repo.rs
Normal 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(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src-tauri/src/services/accounts/category/service.rs
Normal file
107
src-tauri/src/services/accounts/category/service.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user