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