Compare commits
10 Commits
671e0f8ead
...
a768e477ec
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a768e477ec | ||
|
|
c88af8391d | ||
|
|
c11d9f5a1f | ||
|
|
bda56b7e7e | ||
|
|
aa6496de59 | ||
|
|
52a096d934 | ||
|
|
574d082100 | ||
|
|
50cafa8341 | ||
|
|
be74bf5fc1 | ||
|
|
bdfa98b3ed |
@@ -2,6 +2,7 @@ pub use sea_orm_migration::prelude::*;
|
|||||||
|
|
||||||
mod m20260526_062833_create_account_table;
|
mod m20260526_062833_create_account_table;
|
||||||
mod m20260526_110957_create_transcations_table;
|
mod m20260526_110957_create_transcations_table;
|
||||||
|
mod m20260528_110733_create_cache_table;
|
||||||
|
|
||||||
pub struct Migrator;
|
pub struct Migrator;
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ impl MigratorTrait for Migrator {
|
|||||||
vec![
|
vec![
|
||||||
Box::new(m20260526_062833_create_account_table::Migration),
|
Box::new(m20260526_062833_create_account_table::Migration),
|
||||||
Box::new(m20260526_110957_create_transcations_table::Migration),
|
Box::new(m20260526_110957_create_transcations_table::Migration),
|
||||||
|
Box::new(m20260528_110733_create_cache_table::Migration),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
crates/migration/src/m20260528_110733_create_cache_table.rs
Normal file
61
crates/migration/src/m20260528_110733_create_cache_table.rs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
use sea_orm_migration::{prelude::*, schema::*};
|
||||||
|
|
||||||
|
#[derive(DeriveMigrationName)]
|
||||||
|
pub struct Migration;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl MigrationTrait for Migration {
|
||||||
|
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.create_table(
|
||||||
|
Table::create()
|
||||||
|
.table("cache")
|
||||||
|
.comment("2 level cache for general use")
|
||||||
|
.if_not_exists()
|
||||||
|
.col(string("category").not_null().comment("Cache category"))
|
||||||
|
.col(string("key").not_null().comment("Cache key"))
|
||||||
|
.col(string("value").not_null().comment("Cache value"))
|
||||||
|
.col(
|
||||||
|
boolean("is_invalidated")
|
||||||
|
.not_null()
|
||||||
|
.comment("Whether the cache is manually invalidated"),
|
||||||
|
)
|
||||||
|
.col(string("expire_at").null().comment("Expiration time"))
|
||||||
|
.col(string("created_at").not_null().comment("Creation time"))
|
||||||
|
.col(string("updated_at").not_null().comment("Last update time"))
|
||||||
|
//
|
||||||
|
.primary_key(Index::create().col("category").col("key"))
|
||||||
|
//
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_cache_category")
|
||||||
|
.table("cache")
|
||||||
|
.col("category")
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
manager
|
||||||
|
.create_index(
|
||||||
|
Index::create()
|
||||||
|
.name("idx_cache_expire_at")
|
||||||
|
.table("cache")
|
||||||
|
.col("expire_at")
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||||
|
manager
|
||||||
|
.drop_table(Table::drop().table("cache").to_owned())
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src-tauri/src/commands/account.rs
Normal file
34
src-tauri/src/commands/account.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_account(
|
||||||
|
id: i64,
|
||||||
|
services: tauri::State<'_, crate::services::AppState>,
|
||||||
|
) -> Result<Option<crate::services::accounts::Account>, String> {
|
||||||
|
let accounts_service = &services.accounts;
|
||||||
|
accounts_service
|
||||||
|
.get_account(&id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_accounts(
|
||||||
|
services: tauri::State<'_, crate::services::AppState>,
|
||||||
|
) -> Result<Vec<crate::services::accounts::Account>, String> {
|
||||||
|
let accounts_service = &services.accounts;
|
||||||
|
accounts_service
|
||||||
|
.get_accounts()
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_account(
|
||||||
|
request: crate::services::accounts::CreateAccountRequest,
|
||||||
|
services: tauri::State<'_, crate::services::AppState>,
|
||||||
|
) -> Result<i64, String> {
|
||||||
|
let accounts_service = &services.accounts;
|
||||||
|
accounts_service
|
||||||
|
.create_account(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
5
src-tauri/src/commands/mod.rs
Normal file
5
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
mod account;
|
||||||
|
mod transactions;
|
||||||
|
|
||||||
|
pub use account::*;
|
||||||
|
pub use transactions::*;
|
||||||
36
src-tauri/src/commands/transactions.rs
Normal file
36
src-tauri/src/commands/transactions.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_transactions_for_account(
|
||||||
|
id: i64,
|
||||||
|
services: tauri::State<'_, crate::services::AppState>,
|
||||||
|
) -> Result<Vec<crate::services::transaction::Transaction>, String> {
|
||||||
|
let transaction_service = &services.transaction;
|
||||||
|
transaction_service
|
||||||
|
.get_transactions_by_account(id)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_transaction(
|
||||||
|
request: crate::services::transaction::CreateTransactionRequest,
|
||||||
|
services: tauri::State<'_, crate::services::AppState>,
|
||||||
|
) -> Result<i64, String> {
|
||||||
|
let transaction_service = &services.transaction;
|
||||||
|
transaction_service
|
||||||
|
.create_transaction(request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn update_transaction(
|
||||||
|
id: i64,
|
||||||
|
request: crate::services::transaction::UpdateTransactionRequest,
|
||||||
|
services: tauri::State<'_, crate::services::AppState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let transaction_service = &services.transaction;
|
||||||
|
transaction_service
|
||||||
|
.update_transaction(id, request)
|
||||||
|
.await
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
24
src-tauri/src/db/entities/cache.rs
Normal file
24
src-tauri/src/db/entities/cache.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
//! `SeaORM` Entity, @generated by sea-orm-codegen 2.0
|
||||||
|
|
||||||
|
use sea_orm::entity::prelude::*;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||||
|
#[sea_orm(table_name = "cache")]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct Model {
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub category: String,
|
||||||
|
#[sea_orm(primary_key, auto_increment = false)]
|
||||||
|
pub key: String,
|
||||||
|
pub value: String,
|
||||||
|
pub is_invalidated: bool,
|
||||||
|
pub expire_at: Option<String>,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||||
|
pub enum Relation {}
|
||||||
|
|
||||||
|
impl ActiveModelBehavior for ActiveModel {}
|
||||||
@@ -4,6 +4,7 @@ pub mod prelude;
|
|||||||
|
|
||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod account_category;
|
pub mod account_category;
|
||||||
|
pub mod cache;
|
||||||
pub mod category;
|
pub mod category;
|
||||||
pub mod tag;
|
pub mod tag;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
pub use super::account::Entity as Account;
|
pub use super::account::Entity as Account;
|
||||||
pub use super::account_category::Entity as AccountCategory;
|
pub use super::account_category::Entity as AccountCategory;
|
||||||
|
pub use super::cache::Entity as Cache;
|
||||||
pub use super::category::Entity as Category;
|
pub use super::category::Entity as Category;
|
||||||
pub use super::tag::Entity as Tag;
|
pub use super::tag::Entity as Tag;
|
||||||
pub use super::transaction::Entity as Transaction;
|
pub use super::transaction::Entity as Transaction;
|
||||||
|
|||||||
@@ -1,217 +0,0 @@
|
|||||||
// TODO: validation for all dto
|
|
||||||
use crate::db::{
|
|
||||||
Db,
|
|
||||||
entities::account_category::{
|
|
||||||
ActiveModel as AccountCategoryActiveModel, Entity as AccountCategoryEntity,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use migration::OnConflict;
|
|
||||||
use sea_orm::{
|
|
||||||
ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait,
|
|
||||||
IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub struct AccountCategory {
|
|
||||||
pub id: i64,
|
|
||||||
pub name: String,
|
|
||||||
pub account_type: AccountType,
|
|
||||||
//
|
|
||||||
pub created_at: String,
|
|
||||||
pub updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct UpdateAccountCategoryRequest {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub account_type: Option<AccountType>,
|
|
||||||
}
|
|
||||||
|
|
||||||
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"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
pub trait AccountCategoryService: Send + Sync + 'static {
|
|
||||||
async fn get_categories(&self) -> Result<Vec<AccountCategory>, String>;
|
|
||||||
async fn create_category(
|
|
||||||
&self,
|
|
||||||
create_request: CreateAccountCategoryRequest,
|
|
||||||
) -> Result<i64, String>;
|
|
||||||
|
|
||||||
async fn create_category_with_tx(
|
|
||||||
&self,
|
|
||||||
create_request: CreateAccountCategoryRequest,
|
|
||||||
tx: &Db,
|
|
||||||
) -> Result<i64, String>;
|
|
||||||
|
|
||||||
async fn update_category(
|
|
||||||
&self,
|
|
||||||
id: &i64,
|
|
||||||
update_request: UpdateAccountCategoryRequest,
|
|
||||||
) -> Result<(), String>;
|
|
||||||
|
|
||||||
async fn update_category_with_tx(
|
|
||||||
&self,
|
|
||||||
id: &i64,
|
|
||||||
update_request: UpdateAccountCategoryRequest,
|
|
||||||
tx: &Db,
|
|
||||||
) -> Result<(), String>;
|
|
||||||
|
|
||||||
async fn delete_category(&self, id: &i64) -> Result<(), String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AccountCategoryServiceImpl {
|
|
||||||
db: DatabaseConnection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccountCategoryServiceImpl {
|
|
||||||
pub fn new(db: DatabaseConnection) -> Self {
|
|
||||||
Self { db }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl AccountCategoryService for AccountCategoryServiceImpl {
|
|
||||||
async fn get_categories(&self) -> Result<Vec<AccountCategory>, String> {
|
|
||||||
let categories = AccountCategoryEntity::find()
|
|
||||||
.all(&self.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(categories
|
|
||||||
.into_iter()
|
|
||||||
.map(|model| AccountCategory {
|
|
||||||
id: model.id,
|
|
||||||
name: model.name,
|
|
||||||
account_type: AccountType::from(model.account_type),
|
|
||||||
created_at: model.created_at,
|
|
||||||
updated_at: model.updated_at,
|
|
||||||
})
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_category(
|
|
||||||
&self,
|
|
||||||
create_request: CreateAccountCategoryRequest,
|
|
||||||
) -> Result<i64, String> {
|
|
||||||
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, String> {
|
|
||||||
let new_category = AccountCategoryActiveModel {
|
|
||||||
name: Set(create_request.name),
|
|
||||||
account_type: Set(create_request.account_type.to_string()),
|
|
||||||
created_at: Set(chrono::Utc::now().to_rfc3339()),
|
|
||||||
updated_at: Set(chrono::Utc::now().to_rfc3339()),
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
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
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
Ok(res.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_category(&self, id: &i64) -> Result<(), String> {
|
|
||||||
// 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
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
if accounts_using_category > 0 {
|
|
||||||
return Err("Cannot delete category that is in use by accounts".to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
let category = AccountCategoryEntity::find_by_id(*id)
|
|
||||||
.one(&self.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
if let Some(category) = category {
|
|
||||||
category.delete(&self.db).await.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err("Category not found".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_category(
|
|
||||||
&self,
|
|
||||||
id: &i64,
|
|
||||||
update_request: UpdateAccountCategoryRequest,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
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<(), String> {
|
|
||||||
let category = AccountCategoryEntity::find_by_id(*id)
|
|
||||||
.one(&self.db)
|
|
||||||
.await
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
.map(|opt| opt.map(|model| model.into_active_model()))?;
|
|
||||||
|
|
||||||
if let Some(mut category) = category {
|
|
||||||
if let Some(name) = update_request.name {
|
|
||||||
category.name = Set(name);
|
|
||||||
}
|
|
||||||
if let Some(account_type) = update_request.account_type {
|
|
||||||
category.account_type = Set(account_type.to_string());
|
|
||||||
}
|
|
||||||
category.updated_at = Set(chrono::Utc::now().to_rfc3339());
|
|
||||||
category.update(tx).await.map_err(|e| e.to_string())?;
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
Err("Category not found".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
||||||
|
}
|
||||||
|
}
|
||||||
130
src-tauri/src/services/accounts/dto.rs
Normal file
130
src-tauri/src/services/accounts/dto.rs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
use sea_orm::ActiveValue::Set;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{
|
||||||
|
DbServiceError,
|
||||||
|
entities::{
|
||||||
|
account::{ActiveModel as AccountActiveModel, Model as AccountModel},
|
||||||
|
account_category::Model as AccountCategoryModel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
services::accounts::category::{
|
||||||
|
AccountCategoryServiceError, AccountType, CreateAccountCategoryRequest,
|
||||||
|
UpdateAccountCategoryRequest,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum AccountsServiceError {
|
||||||
|
#[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 AccountsServiceError {
|
||||||
|
fn from(err: sea_orm::DbErr) -> Self {
|
||||||
|
AccountsServiceError::DbErr(DbServiceError::from(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AccountCategoryServiceError> for AccountsServiceError {
|
||||||
|
fn from(err: AccountCategoryServiceError) -> Self {
|
||||||
|
match err {
|
||||||
|
AccountCategoryServiceError::DbErr(db_err) => AccountsServiceError::DbErr(db_err),
|
||||||
|
AccountCategoryServiceError::Validation(msg) => AccountsServiceError::Validation(msg),
|
||||||
|
AccountCategoryServiceError::TargetNotFound(msg) => {
|
||||||
|
AccountsServiceError::TargetNotFound(msg)
|
||||||
|
}
|
||||||
|
AccountCategoryServiceError::Unknown(msg) => AccountsServiceError::Unknown(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Account {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
pub account_type: AccountType,
|
||||||
|
pub account_category: String,
|
||||||
|
//
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(AccountModel, AccountCategoryModel)> for Account {
|
||||||
|
fn from((account_model, category_model): (AccountModel, AccountCategoryModel)) -> Self {
|
||||||
|
Account {
|
||||||
|
id: account_model.id,
|
||||||
|
name: account_model.name,
|
||||||
|
account_type: AccountType::from(category_model.account_type),
|
||||||
|
account_category: category_model.name,
|
||||||
|
created_at: account_model.created_at,
|
||||||
|
updated_at: account_model.updated_at,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct CreateAccountRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub category: CreateAccountCategoryRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateAccountRequest {
|
||||||
|
pub fn validate(&self) -> Result<(), AccountsServiceError> {
|
||||||
|
if self.name.trim().is_empty() {
|
||||||
|
return Err(AccountsServiceError::Validation(
|
||||||
|
"Account name cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self.category.validate()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<(CreateAccountRequest, i64)> for AccountActiveModel {
|
||||||
|
fn from((request, category_id): (CreateAccountRequest, i64)) -> Self {
|
||||||
|
AccountActiveModel {
|
||||||
|
name: Set(request.name),
|
||||||
|
account_category_id: Set(category_id),
|
||||||
|
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 UpdateAccountRequest {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub category: Option<UpdateAccountCategoryRequest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UpdateAccountRequest {
|
||||||
|
pub fn validate(&self) -> Result<(), AccountsServiceError> {
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
if name.trim().is_empty() {
|
||||||
|
return Err(AccountsServiceError::Validation(
|
||||||
|
"Account name cannot be empty".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(category) = &self.category {
|
||||||
|
category.validate()?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_to(&self, account: &mut AccountActiveModel) {
|
||||||
|
if let Some(name) = &self.name {
|
||||||
|
account.name = Set(name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,191 +1,7 @@
|
|||||||
// TODO: validation for all dto
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
db::entities::{
|
|
||||||
account::{
|
|
||||||
ActiveModel as AccountActiveModel, Entity as AccountEntity, Model as AccountModel,
|
|
||||||
},
|
|
||||||
account_category::{
|
|
||||||
ActiveModel as AccountCategoryActiveModel, Entity as AccountCategoryEntity,
|
|
||||||
Model as AccountCategoryModel,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
services::accounts::category::{
|
|
||||||
AccountType, CreateAccountCategoryRequest, UpdateAccountCategoryRequest,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use sea_orm::{
|
|
||||||
ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, TransactionTrait,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub mod category;
|
pub mod category;
|
||||||
|
mod dto;
|
||||||
|
mod repo;
|
||||||
|
mod service;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
pub use dto::*;
|
||||||
pub struct Account {
|
pub use service::*;
|
||||||
pub id: i64,
|
|
||||||
pub name: String,
|
|
||||||
pub account_type: AccountType,
|
|
||||||
pub account_category: String,
|
|
||||||
//
|
|
||||||
pub created_at: String,
|
|
||||||
pub updated_at: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<(AccountModel, AccountCategoryModel)> for Account {
|
|
||||||
fn from((account_model, category_model): (AccountModel, AccountCategoryModel)) -> Self {
|
|
||||||
Account {
|
|
||||||
id: account_model.id,
|
|
||||||
name: account_model.name,
|
|
||||||
account_type: AccountType::from(category_model.account_type),
|
|
||||||
account_category: category_model.name,
|
|
||||||
created_at: account_model.created_at,
|
|
||||||
updated_at: account_model.updated_at,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct CreateAccountRequest {
|
|
||||||
pub name: String,
|
|
||||||
pub category: CreateAccountCategoryRequest,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct UpdateAccountRequest {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub category: Option<UpdateAccountCategoryRequest>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
pub trait AccountsService: Send + Sync + 'static {
|
|
||||||
async fn get_accounts(&self) -> Result<Vec<Account>, String>;
|
|
||||||
async fn get_account(&self, id: &i64) -> Result<Option<Account>, String>;
|
|
||||||
async fn create_account(&self, create_request: CreateAccountRequest) -> Result<i64, String>;
|
|
||||||
async fn update_account(
|
|
||||||
&self,
|
|
||||||
id: &i64,
|
|
||||||
update_request: UpdateAccountRequest,
|
|
||||||
) -> Result<(), String>;
|
|
||||||
async fn delete_account(&self, id: &i64) -> Result<(), String>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct AccountsServiceImpl {
|
|
||||||
db: DatabaseConnection,
|
|
||||||
account_category_service: Arc<dyn category::AccountCategoryService>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AccountsServiceImpl {
|
|
||||||
pub fn new(
|
|
||||||
db: DatabaseConnection,
|
|
||||||
account_category_service: Arc<dyn category::AccountCategoryService>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
db,
|
|
||||||
account_category_service,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
|
||||||
impl AccountsService for AccountsServiceImpl {
|
|
||||||
async fn get_accounts(&self) -> Result<Vec<Account>, String> {
|
|
||||||
let accounts = AccountEntity::find()
|
|
||||||
.find_both_related(AccountCategoryEntity)
|
|
||||||
.all(&self.db)
|
|
||||||
.await;
|
|
||||||
match accounts {
|
|
||||||
Ok(accounts) => Ok(accounts.into_iter().map(|a| a.into()).collect()),
|
|
||||||
Err(e) => Err(format!("Failed to fetch accounts: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_account(&self, id: &i64) -> Result<Option<Account>, String> {
|
|
||||||
let result = AccountEntity::find_by_id(*id)
|
|
||||||
.find_both_related(AccountCategoryEntity)
|
|
||||||
.one(&self.db)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(Some(account)) => Ok(Some(account.into())),
|
|
||||||
Ok(None) => Ok(None),
|
|
||||||
Err(e) => Err(format!("Failed to fetch account: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_account(&self, create_request: CreateAccountRequest) -> Result<i64, String> {
|
|
||||||
let tx = self.db.begin().await.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
let category_id = self
|
|
||||||
.account_category_service
|
|
||||||
.create_category_with_tx(
|
|
||||||
CreateAccountCategoryRequest {
|
|
||||||
name: create_request.category.name,
|
|
||||||
account_type: create_request.category.account_type,
|
|
||||||
},
|
|
||||||
&(&tx).into(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let new_account = AccountEntity::insert(AccountActiveModel {
|
|
||||||
name: Set(create_request.name),
|
|
||||||
created_at: Set(chrono::Utc::now().to_rfc3339()),
|
|
||||||
updated_at: Set(chrono::Utc::now().to_rfc3339()),
|
|
||||||
account_category_id: Set(category_id),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.exec(&tx)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
tx.commit().await.map_err(|e| e.to_string())?;
|
|
||||||
|
|
||||||
match new_account {
|
|
||||||
Ok(res) => Ok(res.last_insert_id),
|
|
||||||
Err(e) => Err(format!("Failed to create account: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_account(
|
|
||||||
&self,
|
|
||||||
id: &i64,
|
|
||||||
update_request: UpdateAccountRequest,
|
|
||||||
) -> Result<(), String> {
|
|
||||||
let tx = self.db.begin().await.map_err(|e| e.to_string())?;
|
|
||||||
let account = AccountEntity::find_by_id(*id).one(&tx).await;
|
|
||||||
match account {
|
|
||||||
Ok(Some(account)) => {
|
|
||||||
let mut active_model: AccountActiveModel = account.into();
|
|
||||||
// Only update fields that are provided in the request
|
|
||||||
if let Some(name) = update_request.name {
|
|
||||||
active_model.name = Set(name);
|
|
||||||
}
|
|
||||||
//
|
|
||||||
if let Some(category_request) = update_request.category {
|
|
||||||
// update the category if it is provided in the request
|
|
||||||
self.account_category_service
|
|
||||||
.update_category_with_tx(id, category_request, &(&tx).into())
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
active_model.updated_at = Set(chrono::Utc::now().to_rfc3339());
|
|
||||||
//
|
|
||||||
active_model.update(&tx).await.map_err(|e| e.to_string())?;
|
|
||||||
let res = tx.commit().await.map_err(|e| e.to_string());
|
|
||||||
match res {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(format!("Failed to update account: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => Err("Account not found".to_string()),
|
|
||||||
Err(e) => Err(format!("Failed to fetch account: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn delete_account(&self, id: &i64) -> Result<(), String> {
|
|
||||||
let res = AccountEntity::delete_by_id(*id).exec(&self.db).await;
|
|
||||||
match res {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(format!("Failed to delete account: {}", e)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
150
src-tauri/src/services/accounts/repo.rs
Normal file
150
src-tauri/src/services/accounts/repo.rs
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use sea_orm::{
|
||||||
|
ActiveModelTrait, ActiveValue::Set, DatabaseConnection, EntityTrait, TransactionTrait,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::entities::{
|
||||||
|
account::{ActiveModel as AccountActiveModel, Entity as AccountEntity},
|
||||||
|
account_category::Entity as AccountCategoryEntity,
|
||||||
|
},
|
||||||
|
services::accounts::{
|
||||||
|
category::CreateAccountCategoryRequest,
|
||||||
|
dto::{Account, AccountsServiceError, CreateAccountRequest, UpdateAccountRequest},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait AccountsRepoService: Send + Sync + 'static {
|
||||||
|
async fn get_accounts(&self) -> Result<Vec<Account>, AccountsServiceError>;
|
||||||
|
async fn get_account(&self, id: &i64) -> Result<Option<Account>, AccountsServiceError>;
|
||||||
|
async fn create_account(
|
||||||
|
&self,
|
||||||
|
create_request: CreateAccountRequest,
|
||||||
|
) -> Result<i64, AccountsServiceError>;
|
||||||
|
async fn update_account(
|
||||||
|
&self,
|
||||||
|
id: &i64,
|
||||||
|
update_request: UpdateAccountRequest,
|
||||||
|
) -> Result<(), AccountsServiceError>;
|
||||||
|
async fn delete_account(&self, id: &i64) -> Result<(), AccountsServiceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AccountsRepoServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::category::AccountCategoryService + ?Sized,
|
||||||
|
{
|
||||||
|
db: DatabaseConnection,
|
||||||
|
account_category_service: Arc<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> AccountsRepoServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::category::AccountCategoryService + ?Sized,
|
||||||
|
{
|
||||||
|
pub fn new(db: DatabaseConnection, account_category_service: Arc<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
db,
|
||||||
|
account_category_service,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T> AccountsRepoService for AccountsRepoServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::category::AccountCategoryService + ?Sized,
|
||||||
|
{
|
||||||
|
async fn get_accounts(&self) -> Result<Vec<Account>, AccountsServiceError> {
|
||||||
|
let accounts = AccountEntity::find()
|
||||||
|
.find_both_related(AccountCategoryEntity)
|
||||||
|
.all(&self.db)
|
||||||
|
.await?;
|
||||||
|
Ok(accounts.into_iter().map(|a| a.into()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_account(&self, id: &i64) -> Result<Option<Account>, AccountsServiceError> {
|
||||||
|
let result = AccountEntity::find_by_id(*id)
|
||||||
|
.find_both_related(AccountCategoryEntity)
|
||||||
|
.one(&self.db)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(Some(account)) => Ok(Some(account.into())),
|
||||||
|
Ok(None) => Ok(None),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_account(
|
||||||
|
&self,
|
||||||
|
create_request: CreateAccountRequest,
|
||||||
|
) -> Result<i64, AccountsServiceError> {
|
||||||
|
let tx = self.db.begin().await?;
|
||||||
|
|
||||||
|
let category_id = self
|
||||||
|
.account_category_service
|
||||||
|
.create_category_with_tx(
|
||||||
|
CreateAccountCategoryRequest {
|
||||||
|
name: create_request.category.name,
|
||||||
|
account_type: create_request.category.account_type,
|
||||||
|
},
|
||||||
|
&(&tx).into(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let new_account = AccountEntity::insert(AccountActiveModel {
|
||||||
|
name: Set(create_request.name),
|
||||||
|
created_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||||
|
updated_at: Set(chrono::Utc::now().to_rfc3339()),
|
||||||
|
account_category_id: Set(category_id),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.exec(&tx)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(new_account.last_insert_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_account(
|
||||||
|
&self,
|
||||||
|
id: &i64,
|
||||||
|
update_request: UpdateAccountRequest,
|
||||||
|
) -> Result<(), AccountsServiceError> {
|
||||||
|
let tx = self.db.begin().await?;
|
||||||
|
let account = AccountEntity::find_by_id(*id).one(&tx).await;
|
||||||
|
match account {
|
||||||
|
Ok(Some(account)) => {
|
||||||
|
let mut active_model: AccountActiveModel = account.into();
|
||||||
|
// Only update fields that are provided in the request
|
||||||
|
if let Some(name) = update_request.name {
|
||||||
|
active_model.name = Set(name);
|
||||||
|
}
|
||||||
|
//
|
||||||
|
if let Some(category_request) = update_request.category {
|
||||||
|
// update the category if it is provided in the request
|
||||||
|
self.account_category_service
|
||||||
|
.update_category_with_tx(id, category_request, &(&tx).into())
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
active_model.updated_at = Set(chrono::Utc::now().to_rfc3339());
|
||||||
|
//
|
||||||
|
active_model.update(&tx).await?;
|
||||||
|
tx.commit().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(None) => Err(AccountsServiceError::TargetNotFound(
|
||||||
|
"Account not found".to_string(),
|
||||||
|
)),
|
||||||
|
Err(e) => Err(e.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_account(&self, id: &i64) -> Result<(), AccountsServiceError> {
|
||||||
|
AccountEntity::delete_by_id(*id).exec(&self.db).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src-tauri/src/services/accounts/service.rs
Normal file
78
src-tauri/src/services/accounts/service.rs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::services::accounts::{
|
||||||
|
Account, AccountsServiceError, CreateAccountRequest, UpdateAccountRequest,
|
||||||
|
};
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait AccountsService: Send + Sync + 'static {
|
||||||
|
async fn get_accounts(&self) -> Result<Vec<Account>, AccountsServiceError>;
|
||||||
|
async fn get_account(&self, id: &i64) -> Result<Option<Account>, AccountsServiceError>;
|
||||||
|
async fn create_account(
|
||||||
|
&self,
|
||||||
|
create_request: CreateAccountRequest,
|
||||||
|
) -> Result<i64, AccountsServiceError>;
|
||||||
|
async fn update_account(
|
||||||
|
&self,
|
||||||
|
id: &i64,
|
||||||
|
update_request: UpdateAccountRequest,
|
||||||
|
) -> Result<(), AccountsServiceError>;
|
||||||
|
async fn delete_account(&self, id: &i64) -> Result<(), AccountsServiceError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AccountsServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::repo::AccountsRepoService + ?Sized,
|
||||||
|
{
|
||||||
|
db: DatabaseConnection,
|
||||||
|
repo: Arc<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> AccountsServiceImpl<super::repo::AccountsRepoServiceImpl<T>>
|
||||||
|
where
|
||||||
|
T: super::category::AccountCategoryService + ?Sized,
|
||||||
|
{
|
||||||
|
pub fn new(db: DatabaseConnection, account_category_service: Arc<T>) -> Self {
|
||||||
|
Self {
|
||||||
|
db: db.clone(),
|
||||||
|
repo: Arc::new(super::repo::AccountsRepoServiceImpl::new(
|
||||||
|
db.clone(),
|
||||||
|
account_category_service,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T> AccountsService for AccountsServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::repo::AccountsRepoService + ?Sized,
|
||||||
|
{
|
||||||
|
async fn get_accounts(&self) -> Result<Vec<Account>, AccountsServiceError> {
|
||||||
|
self.repo.get_accounts().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_account(&self, id: &i64) -> Result<Option<Account>, AccountsServiceError> {
|
||||||
|
self.repo.get_account(id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_account(
|
||||||
|
&self,
|
||||||
|
create_request: CreateAccountRequest,
|
||||||
|
) -> Result<i64, AccountsServiceError> {
|
||||||
|
self.repo.create_account(create_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_account(
|
||||||
|
&self,
|
||||||
|
id: &i64,
|
||||||
|
update_request: UpdateAccountRequest,
|
||||||
|
) -> Result<(), AccountsServiceError> {
|
||||||
|
self.repo.update_account(id, update_request).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_account(&self, id: &i64) -> Result<(), AccountsServiceError> {
|
||||||
|
self.repo.delete_account(id).await
|
||||||
|
}
|
||||||
|
}
|
||||||
108
src-tauri/src/services/cache/dto.rs
vendored
Normal file
108
src-tauri/src/services/cache/dto.rs
vendored
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use crate::db::entities::cache::Model as CacheModel;
|
||||||
|
|
||||||
|
use sea_orm::DbErr;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::db::DbServiceError;
|
||||||
|
|
||||||
|
pub enum CacheValue {
|
||||||
|
Ok(String),
|
||||||
|
Expired(String),
|
||||||
|
NotFound,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<CacheModel>> for CacheValue {
|
||||||
|
fn from(opt: Option<CacheModel>) -> Self {
|
||||||
|
match opt {
|
||||||
|
Some(model) => model.into(),
|
||||||
|
None => CacheValue::NotFound,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CacheModel> for (String, CacheValue) {
|
||||||
|
fn from(model: CacheModel) -> Self {
|
||||||
|
let key = model.key.clone();
|
||||||
|
let value = model.into();
|
||||||
|
(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CacheModel> for CacheValue {
|
||||||
|
fn from(model: CacheModel) -> Self {
|
||||||
|
if let Some(expire_at) = model.expire_at {
|
||||||
|
let expire_at = chrono::DateTime::parse_from_rfc3339(&expire_at)
|
||||||
|
.map(|dt| dt.with_timezone(&chrono::Utc));
|
||||||
|
|
||||||
|
match expire_at {
|
||||||
|
Ok(expire_at) if expire_at < chrono::Utc::now() => {
|
||||||
|
return CacheValue::Expired(model.value);
|
||||||
|
}
|
||||||
|
Ok(_) => {} // Not expired
|
||||||
|
Err(_) => {
|
||||||
|
// warn!("Failed to parse expire_at: {}", model.expire_at);
|
||||||
|
// If parsing fails, treat it as expired to be safe
|
||||||
|
return CacheValue::Expired(model.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheValue::Ok(model.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum CacheServiceError {
|
||||||
|
#[error("database error: {0}")]
|
||||||
|
DbErr(#[from] DbServiceError),
|
||||||
|
#[error("target expired: {0}")]
|
||||||
|
TargetExpired(String),
|
||||||
|
#[error("Target not found: {0}")]
|
||||||
|
TargetNotFound(String),
|
||||||
|
#[error("Validation error: {0}")]
|
||||||
|
Validation(String),
|
||||||
|
#[error("unknown error: {0}")]
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DbErr> for CacheServiceError {
|
||||||
|
fn from(value: DbErr) -> Self {
|
||||||
|
CacheServiceError::DbErr(DbServiceError::from(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum CacheCategory {
|
||||||
|
Transaction,
|
||||||
|
TransactionTag,
|
||||||
|
Account,
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&str> for CacheCategory {
|
||||||
|
fn from(value: &str) -> Self {
|
||||||
|
match value {
|
||||||
|
"Transaction" => CacheCategory::Transaction,
|
||||||
|
"TransactionTag" => CacheCategory::TransactionTag,
|
||||||
|
"Account" => CacheCategory::Account,
|
||||||
|
other => {
|
||||||
|
// warn!("Unknown cache category: {}", other);
|
||||||
|
CacheCategory::Unknown(other.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CacheCategory {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
CacheCategory::Transaction => "Transaction",
|
||||||
|
CacheCategory::TransactionTag => "TransactionTag",
|
||||||
|
CacheCategory::Account => "Account",
|
||||||
|
CacheCategory::Unknown(other) => other.as_str(),
|
||||||
|
};
|
||||||
|
write!(f, "{}", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src-tauri/src/services/cache/mod.rs
vendored
Normal file
6
src-tauri/src/services/cache/mod.rs
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
mod dto;
|
||||||
|
mod repo;
|
||||||
|
mod service;
|
||||||
|
|
||||||
|
pub use dto::*;
|
||||||
|
pub use service::*;
|
||||||
182
src-tauri/src/services/cache/repo.rs
vendored
Normal file
182
src-tauri/src/services/cache/repo.rs
vendored
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use migration::Expr;
|
||||||
|
use sea_orm::{ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, Value};
|
||||||
|
|
||||||
|
use super::{dto::CacheCategory, service::CacheResult};
|
||||||
|
use crate::db::{
|
||||||
|
Db,
|
||||||
|
entities::cache::{
|
||||||
|
ActiveModel as CacheActiveModel, Column as CacheColumn, Entity as CacheEntity,
|
||||||
|
Model as CacheModel,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T> = CacheResult<T>;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait CacheRepo: Send + Sync + 'static {
|
||||||
|
async fn get_category(&self, category: CacheCategory) -> Result<HashMap<String, CacheModel>>;
|
||||||
|
async fn get(&self, category: CacheCategory, key: &str) -> Result<Option<CacheModel>>;
|
||||||
|
async fn set_with_tx(
|
||||||
|
&self,
|
||||||
|
tx: &Db,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()>;
|
||||||
|
async fn set(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()>;
|
||||||
|
async fn invalidate_category(&self, category: CacheCategory) -> Result<()>;
|
||||||
|
async fn invalidate_with_tx(&self, tx: &Db, category: CacheCategory, key: &str) -> Result<()>;
|
||||||
|
async fn invalidate(&self, category: CacheCategory, key: &str) -> Result<()>;
|
||||||
|
async fn set_expire(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CacheRepoImpl {
|
||||||
|
db: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheRepoImpl {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl CacheRepo for CacheRepoImpl {
|
||||||
|
async fn get_category(&self, category: CacheCategory) -> Result<HashMap<String, CacheModel>> {
|
||||||
|
let cache_entries = CacheEntity::find()
|
||||||
|
.filter(CacheColumn::Category.eq(category.to_string()))
|
||||||
|
.all(&self.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let result = cache_entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|entry| (entry.key.clone(), entry))
|
||||||
|
.collect();
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, category: CacheCategory, key: &str) -> Result<Option<CacheModel>> {
|
||||||
|
let cache_entry = CacheEntity::find()
|
||||||
|
.filter(CacheColumn::Category.eq(category.to_string()))
|
||||||
|
.filter(CacheColumn::Key.eq(key.to_string()))
|
||||||
|
.one(&self.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(cache_entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.set_with_tx(&(&self.db).into(), category, key, value, expire_at)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_with_tx(
|
||||||
|
&self,
|
||||||
|
tx: &Db,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let expire_at_str = expire_at.map(|dt| dt.to_rfc3339());
|
||||||
|
let now_str = chrono::Utc::now().to_rfc3339();
|
||||||
|
let active_model = CacheActiveModel {
|
||||||
|
category: Set(category.to_string()),
|
||||||
|
key: Set(key.to_string()),
|
||||||
|
value: Set(value.to_string()),
|
||||||
|
is_invalidated: Set(false),
|
||||||
|
expire_at: Set(expire_at_str),
|
||||||
|
created_at: Set(now_str.clone()),
|
||||||
|
updated_at: Set(now_str),
|
||||||
|
};
|
||||||
|
|
||||||
|
CacheEntity::insert(active_model)
|
||||||
|
.on_conflict(
|
||||||
|
sea_orm::sea_query::OnConflict::columns([CacheColumn::Category, CacheColumn::Key])
|
||||||
|
.update_columns([
|
||||||
|
CacheColumn::Value,
|
||||||
|
CacheColumn::IsInvalidated,
|
||||||
|
CacheColumn::ExpireAt,
|
||||||
|
CacheColumn::UpdatedAt,
|
||||||
|
])
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(tx)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invalidate_category(&self, category: CacheCategory) -> Result<()> {
|
||||||
|
CacheEntity::update_many()
|
||||||
|
.col_expr(
|
||||||
|
CacheColumn::IsInvalidated,
|
||||||
|
Expr::value(Value::Bool(Some(true))),
|
||||||
|
)
|
||||||
|
.filter(CacheColumn::Category.eq(category.to_string()))
|
||||||
|
.exec(&self.db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invalidate(&self, category: CacheCategory, key: &str) -> Result<()> {
|
||||||
|
self.invalidate_with_tx(&(&self.db).into(), category, key)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invalidate_with_tx(&self, tx: &Db, category: CacheCategory, key: &str) -> Result<()> {
|
||||||
|
let target_entry = CacheActiveModel {
|
||||||
|
// pks
|
||||||
|
category: Set(category.to_string()),
|
||||||
|
key: Set(key.to_string()),
|
||||||
|
// update
|
||||||
|
is_invalidated: Set(true),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
CacheEntity::update(target_entry)
|
||||||
|
.validate()?
|
||||||
|
.exec(tx)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn set_expire(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let expire_at_str = expire_at.map(|dt| dt.to_rfc3339());
|
||||||
|
let target_entry = CacheActiveModel {
|
||||||
|
// pks
|
||||||
|
category: Set(category.to_string()),
|
||||||
|
key: Set(key.to_string()),
|
||||||
|
// update
|
||||||
|
expire_at: Set(expire_at_str),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
CacheEntity::update(target_entry)
|
||||||
|
.validate()?
|
||||||
|
.exec(&self.db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src-tauri/src/services/cache/service.rs
vendored
Normal file
123
src-tauri/src/services/cache/service.rs
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
use sea_orm::DatabaseConnection;
|
||||||
|
|
||||||
|
use super::dto::CacheCategory;
|
||||||
|
use crate::{
|
||||||
|
db::Db,
|
||||||
|
services::cache::{CacheServiceError, CacheValue},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub type CacheResult<T> = std::result::Result<T, CacheServiceError>;
|
||||||
|
type Result<T> = CacheResult<T>;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
pub trait CacheService: Send + Sync + 'static {
|
||||||
|
async fn get_category(&self, category: CacheCategory) -> Result<HashMap<String, CacheValue>>;
|
||||||
|
async fn get(&self, category: CacheCategory, key: &str) -> Result<CacheValue>;
|
||||||
|
async fn set_with_tx(
|
||||||
|
&self,
|
||||||
|
tx: &Db,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()>;
|
||||||
|
async fn set(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()>;
|
||||||
|
async fn invalidate_category(&self, category: CacheCategory) -> Result<()>;
|
||||||
|
async fn invalidate_with_tx(&self, tx: &Db, category: CacheCategory, key: &str) -> Result<()>;
|
||||||
|
async fn invalidate(&self, category: CacheCategory, key: &str) -> Result<()>;
|
||||||
|
async fn set_expire(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct CacheServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::repo::CacheRepo + ?Sized,
|
||||||
|
{
|
||||||
|
db: DatabaseConnection,
|
||||||
|
repo: Arc<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CacheServiceImpl<super::repo::CacheRepoImpl> {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self {
|
||||||
|
db: db.clone(),
|
||||||
|
repo: Arc::new(super::repo::CacheRepoImpl::new(db)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl<T> CacheService for CacheServiceImpl<T>
|
||||||
|
where
|
||||||
|
T: super::repo::CacheRepo + ?Sized,
|
||||||
|
{
|
||||||
|
async fn get_category(&self, category: CacheCategory) -> Result<HashMap<String, CacheValue>> {
|
||||||
|
let cache_entries = self.repo.get_category(category).await?;
|
||||||
|
|
||||||
|
let result = cache_entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|(key, model)| (key, model.into()))
|
||||||
|
.collect();
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, category: CacheCategory, key: &str) -> Result<CacheValue> {
|
||||||
|
let cache_entry = self.repo.get(category, key).await?;
|
||||||
|
Ok(cache_entry.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.repo.set(category, key, value, expire_at).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_with_tx(
|
||||||
|
&self,
|
||||||
|
tx: &Db,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
value: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.repo
|
||||||
|
.set_with_tx(tx, category, key, value, expire_at)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invalidate_category(&self, category: CacheCategory) -> Result<()> {
|
||||||
|
self.repo.invalidate_category(category).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invalidate(&self, category: CacheCategory, key: &str) -> Result<()> {
|
||||||
|
self.repo.invalidate(category, key).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn invalidate_with_tx(&self, tx: &Db, category: CacheCategory, key: &str) -> Result<()> {
|
||||||
|
self.repo.invalidate_with_tx(tx, category, key).await
|
||||||
|
}
|
||||||
|
async fn set_expire(
|
||||||
|
&self,
|
||||||
|
category: CacheCategory,
|
||||||
|
key: &str,
|
||||||
|
expire_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.repo.set_expire(category, key, expire_at).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ use crate::services::{
|
|||||||
AccountsService, AccountsServiceImpl,
|
AccountsService, AccountsServiceImpl,
|
||||||
category::{AccountCategoryService, AccountCategoryServiceImpl},
|
category::{AccountCategoryService, AccountCategoryServiceImpl},
|
||||||
},
|
},
|
||||||
|
cache::{CacheService, CacheServiceImpl},
|
||||||
transaction::{
|
transaction::{
|
||||||
TransactionService, TransactionServiceImpl,
|
TransactionService, TransactionServiceImpl,
|
||||||
category::{CategoryService, CategoryServiceImpl},
|
category::{CategoryService, CategoryServiceImpl},
|
||||||
@@ -15,6 +16,7 @@ use crate::services::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub mod accounts;
|
pub mod accounts;
|
||||||
|
pub mod cache;
|
||||||
pub mod transaction;
|
pub mod transaction;
|
||||||
|
|
||||||
pub type AppState = Services<
|
pub type AppState = Services<
|
||||||
@@ -23,22 +25,25 @@ pub type AppState = Services<
|
|||||||
dyn TransactionService,
|
dyn TransactionService,
|
||||||
dyn CategoryService,
|
dyn CategoryService,
|
||||||
dyn TagService,
|
dyn TagService,
|
||||||
|
dyn CacheService,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Services<AC, A, T, TC, TT>
|
pub struct Services<AC, A, T, TC, TT, C>
|
||||||
where
|
where
|
||||||
AC: AccountCategoryService + ?Sized,
|
AC: AccountCategoryService + ?Sized,
|
||||||
A: AccountsService + ?Sized,
|
A: AccountsService + ?Sized,
|
||||||
T: TransactionService + ?Sized,
|
T: TransactionService + ?Sized,
|
||||||
TC: CategoryService + ?Sized,
|
TC: CategoryService + ?Sized,
|
||||||
TT: TagService + ?Sized,
|
TT: TagService + ?Sized,
|
||||||
|
C: CacheService + ?Sized,
|
||||||
{
|
{
|
||||||
pub account_category: Arc<AC>,
|
pub account_category: Arc<AC>,
|
||||||
pub accounts: Arc<A>,
|
pub accounts: Arc<A>,
|
||||||
pub transaction: Arc<T>,
|
pub transaction: Arc<T>,
|
||||||
pub transaction_category: Arc<TC>,
|
pub transaction_category: Arc<TC>,
|
||||||
pub transaction_tag: Arc<TT>,
|
pub transaction_tag: Arc<TT>,
|
||||||
|
pub cache: Arc<C>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_services(db: DatabaseConnection) -> AppState {
|
pub fn create_services(db: DatabaseConnection) -> AppState {
|
||||||
@@ -51,6 +56,8 @@ pub fn create_services(db: DatabaseConnection) -> AppState {
|
|||||||
let transaction_category_service: Arc<dyn CategoryService> =
|
let transaction_category_service: Arc<dyn CategoryService> =
|
||||||
Arc::new(CategoryServiceImpl::new(db.clone()));
|
Arc::new(CategoryServiceImpl::new(db.clone()));
|
||||||
let transaction_tag_service: Arc<dyn TagService> = Arc::new(TagServiceImpl::new(db.clone()));
|
let transaction_tag_service: Arc<dyn TagService> = Arc::new(TagServiceImpl::new(db.clone()));
|
||||||
|
let cache_service: Arc<dyn CacheService> = Arc::new(CacheServiceImpl::new(db.clone()));
|
||||||
|
|
||||||
Services {
|
Services {
|
||||||
account_category: account_category_service.clone(),
|
account_category: account_category_service.clone(),
|
||||||
accounts: account_service.clone(),
|
accounts: account_service.clone(),
|
||||||
@@ -62,5 +69,6 @@ pub fn create_services(db: DatabaseConnection) -> AppState {
|
|||||||
)) as Arc<dyn TransactionService>,
|
)) as Arc<dyn TransactionService>,
|
||||||
transaction_category: transaction_category_service,
|
transaction_category: transaction_category_service,
|
||||||
transaction_tag: transaction_tag_service,
|
transaction_tag: transaction_tag_service,
|
||||||
|
cache: cache_service,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
entities::category::{ActiveModel as CategoryActiveModel, Entity as CategoryEntity},
|
|
||||||
Db,
|
Db,
|
||||||
|
entities::category::{ActiveModel as CategoryActiveModel, Entity as CategoryEntity},
|
||||||
},
|
},
|
||||||
services::transaction::category::{
|
services::transaction::category::{
|
||||||
Category, CategoryServiceError, CategoryServiceResult, CategoryWithParent,
|
Category, CategoryServiceError, CategoryServiceResult, CategoryWithParent,
|
||||||
@@ -22,7 +22,7 @@ pub trait CategoryRepoService: Send + Sync + 'static {
|
|||||||
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>>;
|
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>>;
|
||||||
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64>;
|
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64>;
|
||||||
async fn create_category_with_tx(&self, request: CreateCategoryRequest, tx: &Db)
|
async fn create_category_with_tx(&self, request: CreateCategoryRequest, tx: &Db)
|
||||||
-> Result<i64>;
|
-> Result<i64>;
|
||||||
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()>;
|
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()>;
|
||||||
async fn update_category_with_tx(
|
async fn update_category_with_tx(
|
||||||
&self,
|
&self,
|
||||||
@@ -35,7 +35,13 @@ pub trait CategoryRepoService: Send + Sync + 'static {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct CategoryRepoServiceImpl {
|
pub struct CategoryRepoServiceImpl {
|
||||||
pub db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CategoryRepoServiceImpl {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CategorySelfRefLink;
|
struct CategorySelfRefLink;
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use thiserror::Error;
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{Db, DbServiceError},
|
db::{Db, DbServiceError},
|
||||||
services::transaction::category::{
|
services::transaction::category::{
|
||||||
repo::{CategoryRepoService, CategoryRepoServiceImpl},
|
|
||||||
Category, CategoryWithParent, CreateCategoryRequest, UpdateCategoryRequest,
|
Category, CategoryWithParent, CreateCategoryRequest, UpdateCategoryRequest,
|
||||||
|
repo::{CategoryRepoService, CategoryRepoServiceImpl},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ pub trait CategoryService: Send + Sync + 'static {
|
|||||||
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>>;
|
async fn get_category(&self, id: i64) -> Result<Option<CategoryWithParent>>;
|
||||||
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64>;
|
async fn create_category(&self, request: CreateCategoryRequest) -> Result<i64>;
|
||||||
async fn create_category_with_tx(&self, request: CreateCategoryRequest, tx: &Db)
|
async fn create_category_with_tx(&self, request: CreateCategoryRequest, tx: &Db)
|
||||||
-> Result<i64>;
|
-> Result<i64>;
|
||||||
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()>;
|
async fn update_category(&self, id: i64, request: UpdateCategoryRequest) -> Result<()>;
|
||||||
async fn update_category_with_tx(
|
async fn update_category_with_tx(
|
||||||
&self,
|
&self,
|
||||||
@@ -62,7 +62,7 @@ impl CategoryServiceImpl<CategoryRepoServiceImpl> {
|
|||||||
pub fn new(db: DatabaseConnection) -> Self {
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
Self {
|
Self {
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
category_repo: Arc::new(CategoryRepoServiceImpl { db }),
|
category_repo: Arc::new(CategoryRepoServiceImpl::new(db)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ use crate::{
|
|||||||
DbServiceError,
|
DbServiceError,
|
||||||
entities::{account::Column::Id, transaction::Model as TransactionModel},
|
entities::{account::Column::Id, transaction::Model as TransactionModel},
|
||||||
},
|
},
|
||||||
services::transaction::{category::CategoryServiceError, tag::TagServiceError},
|
services::{
|
||||||
|
accounts::{Account, AccountsServiceError},
|
||||||
|
transaction::{category::CategoryServiceError, tag::TagServiceError},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::db::{
|
use crate::db::{
|
||||||
@@ -61,6 +64,19 @@ impl From<CategoryServiceError> for TransactionServiceError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<AccountsServiceError> for TransactionServiceError {
|
||||||
|
fn from(err: AccountsServiceError) -> Self {
|
||||||
|
match err {
|
||||||
|
AccountsServiceError::DbErr(db_err) => TransactionServiceError::DbErr(db_err),
|
||||||
|
AccountsServiceError::TargetNotFound(msg) => {
|
||||||
|
TransactionServiceError::TargetNotFound(msg)
|
||||||
|
}
|
||||||
|
AccountsServiceError::Validation(msg) => TransactionServiceError::Validation(msg),
|
||||||
|
AccountsServiceError::Unknown(msg) => TransactionServiceError::Unknown(msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub enum TransactionType {
|
pub enum TransactionType {
|
||||||
#[serde(rename = "STANDARD")]
|
#[serde(rename = "STANDARD")]
|
||||||
@@ -158,12 +174,7 @@ async fn validate_account_exists<T>(
|
|||||||
where
|
where
|
||||||
T: crate::services::accounts::AccountsService + ?Sized,
|
T: crate::services::accounts::AccountsService + ?Sized,
|
||||||
{
|
{
|
||||||
let account_exists = account_service
|
let account_exists = account_service.get_account(&account_id).await?.is_some();
|
||||||
.get_account(&account_id)
|
|
||||||
.await
|
|
||||||
// TODO: add error from account service to error chain
|
|
||||||
.map_err(|err| TransactionServiceError::Unknown(err))?
|
|
||||||
.is_some();
|
|
||||||
if !account_exists {
|
if !account_exists {
|
||||||
return Err(TransactionServiceError::Validation(format!(
|
return Err(TransactionServiceError::Validation(format!(
|
||||||
"Account with id {} does not exist",
|
"Account with id {} does not exist",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{
|
db::{
|
||||||
|
Db,
|
||||||
entities::{
|
entities::{
|
||||||
tag::{ActiveModel as TagActiveModel, Entity as TagEntity},
|
tag::{ActiveModel as TagActiveModel, Entity as TagEntity},
|
||||||
transaction_tag::{
|
transaction_tag::{
|
||||||
@@ -7,7 +8,6 @@ use crate::{
|
|||||||
Entity as TransactionTagEntity,
|
Entity as TransactionTagEntity,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Db,
|
|
||||||
},
|
},
|
||||||
services::transaction::tag::{
|
services::transaction::tag::{
|
||||||
CreateTagRequest, Tag, TagServiceError, TagServiceResult, UpdateTagRequest,
|
CreateTagRequest, Tag, TagServiceError, TagServiceResult, UpdateTagRequest,
|
||||||
@@ -45,7 +45,13 @@ pub trait TagRepoService: Send + Sync + 'static {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TagRepoServiceImpl {
|
pub struct TagRepoServiceImpl {
|
||||||
pub db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TagRepoServiceImpl {
|
||||||
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
|
Self { db }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use thiserror::Error;
|
|||||||
use crate::{
|
use crate::{
|
||||||
db::{Db, DbServiceError},
|
db::{Db, DbServiceError},
|
||||||
services::transaction::tag::{
|
services::transaction::tag::{
|
||||||
repo::{TagRepoService, TagRepoServiceImpl},
|
|
||||||
CreateTagRequest, Tag, UpdateTagRequest,
|
CreateTagRequest, Tag, UpdateTagRequest,
|
||||||
|
repo::{TagRepoService, TagRepoServiceImpl},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ impl TagServiceImpl<TagRepoServiceImpl> {
|
|||||||
pub fn new(db: DatabaseConnection) -> Self {
|
pub fn new(db: DatabaseConnection) -> Self {
|
||||||
Self {
|
Self {
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
tag_repo: Arc::new(TagRepoServiceImpl { db }),
|
tag_repo: Arc::new(TagRepoServiceImpl::new(db)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "otter",
|
"title": "otter",
|
||||||
"width": 800,
|
"width": 390,
|
||||||
"height": 600
|
"height": 760
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|||||||
Reference in New Issue
Block a user