diff --git a/Cargo.lock b/Cargo.lock index c953648..981fcdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -827,8 +827,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -1164,6 +1166,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "decimal" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a8ab77e91baeb15034c3be91e87bff4665c9036216148e4996d9a9f5792114d" +dependencies = [ + "bitflags 1.3.2", + "cc", + "libc", + "ord_subset", + "rustc-serialize", + "serde", +] + [[package]] name = "der" version = "0.7.10" @@ -3254,6 +3270,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ord_subset" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdcf5505c0f054ce51fa0fa74142738930a45d5ac1faacae4dd4e2f54afe00fa" + [[package]] name = "ordered-float" version = "4.6.0" @@ -3277,6 +3299,11 @@ dependencies = [ name = "otter" version = "0.1.0" dependencies = [ + "async-trait", + "chrono", + "decimal", + "migration", + "sea-orm", "serde", "serde_json", "tauri", @@ -3980,6 +4007,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustc-serialize" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe834bc780604f4674073badbad26d7219cadfb4a2275802db12cbae17498401" + [[package]] name = "rustc_version" version = "0.4.1" diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index c09a379..0dbd873 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -1,11 +1,15 @@ pub use sea_orm_migration::prelude::*; +mod m20260526_062833_create_account_table; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec> { - vec![] + vec![ + // + Box::new(m20260526_062833_create_account_table::Migration), + ] } } diff --git a/crates/migration/src/m20260526_062833_create_account_table.rs b/crates/migration/src/m20260526_062833_create_account_table.rs new file mode 100644 index 0000000..da698aa --- /dev/null +++ b/crates/migration/src/m20260526_062833_create_account_table.rs @@ -0,0 +1,80 @@ +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("account_category") + .if_not_exists() + .col(pk_auto("id").not_null()) + .col(string("name").not_null()) + .col(string("account_type").not_null()) + .col(string("created_at").not_null()) + .col(string("updated_at").not_null()) + .to_owned(), + ) + .await?; + + manager + .create_table( + Table::create() + .table("account") + .if_not_exists() + .col(pk_auto("id").not_null()) + // + .col(string("name").not_null()) + .col(integer("account_category_id").not_null()) + // + .col(string("created_at").not_null()) + .col(string("updated_at").not_null()) + // Add foreign key constraint to account_category + .foreign_key( + ForeignKey::create() + .name("fk-account-category") + .from("account", "account_category_id") + .to("account_category", "id"), + ) + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx-account-account_type") + .table("account_category") + .col("account_type") + .to_owned(), + ) + .await?; + + manager + .create_index( + Index::create() + .name("idx-account-category_id") + .table("account") + .col("account_category_id") + .to_owned(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table("account").to_owned()) + .await?; + + manager + .drop_table(Table::drop().table("account_category").to_owned()) + .await?; + + Ok(()) + } +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c10ddab..c9b6780 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,4 +22,10 @@ tauri = { version = "2", features = [] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +async-trait = "0.1.89" +decimal = "2.1.0" + +sea-orm.workspace = true +migration = { path = "../crates/migration" } +chrono.workspace = true diff --git a/src-tauri/src/db/entities/account.rs b/src-tauri/src/db/entities/account.rs new file mode 100644 index 0000000..77bc27e --- /dev/null +++ b/src-tauri/src/db/entities/account.rs @@ -0,0 +1,36 @@ +//! `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 = "account")] +#[allow(dead_code)] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub name: String, + pub account_category_id: i64, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::account_category::Entity", + from = "Column::AccountCategoryId", + to = "super::account_category::Column::Id", + on_update = "NoAction", + on_delete = "NoAction" + )] + AccountCategory, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::AccountCategory.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src-tauri/src/db/entities/account_category.rs b/src-tauri/src/db/entities/account_category.rs new file mode 100644 index 0000000..1ea9baf --- /dev/null +++ b/src-tauri/src/db/entities/account_category.rs @@ -0,0 +1,30 @@ +//! `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 = "account_category")] +#[allow(dead_code)] +pub struct Model { + #[sea_orm(primary_key)] + pub id: i64, + pub name: String, + pub account_type: String, + pub created_at: String, + pub updated_at: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::account::Entity")] + Account, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Account.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/src-tauri/src/db/entities/mod.rs b/src-tauri/src/db/entities/mod.rs index e284bbf..d72fe24 100644 --- a/src-tauri/src/db/entities/mod.rs +++ b/src-tauri/src/db/entities/mod.rs @@ -1,3 +1,6 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 pub mod prelude; + +pub mod account; +pub mod account_category; diff --git a/src-tauri/src/db/entities/prelude.rs b/src-tauri/src/db/entities/prelude.rs index acf76ef..453e3eb 100644 --- a/src-tauri/src/db/entities/prelude.rs +++ b/src-tauri/src/db/entities/prelude.rs @@ -1,3 +1,6 @@ //! `SeaORM` Entity, @generated by sea-orm-codegen 2.0 #![allow(unused_imports)] + +pub use super::account::Entity as Account; +pub use super::account_category::Entity as AccountCategory; diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs new file mode 100644 index 0000000..b338ed2 --- /dev/null +++ b/src-tauri/src/db/mod.rs @@ -0,0 +1,85 @@ +pub mod entities; + +use async_trait::async_trait; +use sea_orm::{ + ConnectionTrait, DatabaseConnection, DatabaseTransaction, DbBackend, DbErr, ExecResult, + QueryResult, Statement, StatementBuilder, +}; + +// 1. Create a uniform enum wrapper +pub enum Db<'a> { + Connection(&'a DatabaseConnection), + Transaction(&'a DatabaseTransaction), +} + +impl<'a> From<&'a DatabaseConnection> for Db<'a> { + fn from(conn: &'a DatabaseConnection) -> Self { + Db::Connection(conn) + } +} + +impl<'a> From<&'a DatabaseTransaction> for Db<'a> { + fn from(txn: &'a DatabaseTransaction) -> Self { + Db::Transaction(txn) + } +} + +// 2. Implement ConnectionTrait by forwarding down to the inner variants +#[async_trait] +impl<'a> ConnectionTrait for Db<'a> { + fn get_database_backend(&self) -> DbBackend { + match self { + Db::Connection(conn) => conn.get_database_backend(), + Db::Transaction(txn) => txn.get_database_backend(), + } + } + + async fn execute(&self, stmt: &S) -> Result { + match self { + Db::Connection(conn) => conn.execute(stmt).await, + Db::Transaction(txn) => txn.execute(stmt).await, + } + } + + async fn query_one(&self, stmt: &S) -> Result, DbErr> { + match self { + Db::Connection(conn) => conn.query_one(stmt).await, + Db::Transaction(txn) => txn.query_one(stmt).await, + } + } + + async fn query_all(&self, stmt: &S) -> Result, DbErr> { + match self { + Db::Connection(conn) => conn.query_all(stmt).await, + Db::Transaction(txn) => txn.query_all(stmt).await, + } + } + + async fn query_one_raw(&self, stmt: Statement) -> Result, DbErr> { + match self { + Db::Connection(conn) => conn.query_one_raw(stmt).await, + Db::Transaction(txn) => txn.query_one_raw(stmt).await, + } + } + + async fn query_all_raw(&self, stmt: Statement) -> Result, DbErr> { + match self { + Db::Connection(conn) => conn.query_all_raw(stmt).await, + Db::Transaction(txn) => txn.query_all_raw(stmt).await, + } + } + + async fn execute_raw(&self, stmt: Statement) -> Result { + match self { + Db::Connection(conn) => conn.execute_raw(stmt).await, + Db::Transaction(txn) => txn.execute_raw(stmt).await, + } + } + + async fn execute_unprepared(&self, sql: &str) -> Result { + match self { + Db::Connection(conn) => conn.execute_unprepared(sql).await, + Db::Transaction(txn) => txn.execute_unprepared(sql).await, + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4a277ef..7b42cf3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,20 @@ +#![forbid(unsafe_code, clippy::unwrap_used, clippy::panic)] +#![deny( + unused_must_use, + clippy::expect_used, + clippy::unimplemented, + clippy::todo +)] + +use migration::MigratorTrait; +use sea_orm::{ConnectOptions, ConnectionTrait, Database, Statement}; +use tauri::Manager; + +use crate::services::create_services; + +mod db; +mod services; + // Learn more about Tauri commands at https://tauri.app/develop/calling-rust/ #[tauri::command] fn greet(name: &str) -> String { @@ -6,7 +23,37 @@ fn greet(name: &str) -> String { #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + #[allow(clippy::expect_used)] tauri::Builder::default() + .setup(|app| { + tauri::async_runtime::block_on(async { + let mut connect_options = + ConnectOptions::new("sqlite://finance_manager.db".to_string()); + connect_options.after_connect(|conn| { + Box::pin(async move { + // Enable foreign key support for SQLite + conn.execute_raw(Statement::from_string( + conn.get_database_backend(), + "PRAGMA foreign_keys = ON;", + )) + .await + .map(|_| ()) + }) + }); + + #[allow(clippy::expect_used)] + let db = Database::connect(connect_options) + .await + .expect("Failed to connect to database"); + // Run migrations if needed + #[allow(clippy::expect_used)] + migration::Migrator::up(&db, None) + .await + .expect("Failed to run migrations"); + app.manage(create_services(db)) + }); + Ok(()) + }) .plugin(tauri_plugin_opener::init()) .invoke_handler(tauri::generate_handler![greet]) .run(tauri::generate_context!()) diff --git a/src-tauri/src/services/accounts/category.rs b/src-tauri/src/services/accounts/category.rs new file mode 100644 index 0000000..3411aad --- /dev/null +++ b/src-tauri/src/services/accounts/category.rs @@ -0,0 +1,196 @@ +use crate::db::{ + entities::account_category::{ + ActiveModel as AccountCategoryActiveModel, Entity as AccountCategoryEntity, + }, + Db, +}; +use sea_orm::{ + ActiveModelTrait, ActiveValue::Set, ColumnTrait, ConnectionTrait, DatabaseConnection, + DatabaseTransaction, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter, +}; + +pub struct AccountCategory { + pub id: i64, + pub name: String, + pub account_type: AccountType, + // + pub created_at: String, + pub updated_at: String, +} + +pub enum AccountType { + Asset, // Positive balance, e.g. Checking, Savings + Liability, // Negative balance, e.g. Credit Card + Unknown, // Fallback for unknown types +} + +pub struct CreateAccountCategoryRequest { + pub name: String, + pub account_type: AccountType, +} + +pub struct UpdateAccountCategoryRequest { + pub name: Option, + pub account_type: Option, +} + +impl From 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, String>; + async fn create_category( + &self, + create_request: CreateAccountCategoryRequest, + ) -> Result; + + async fn create_category_with_tx( + &self, + create_request: CreateAccountCategoryRequest, + tx: &Db, + ) -> Result; + + 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, 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 { + self.create_category_with_tx(create_request, &(&self.db).into()) + .await + } + + async fn create_category_with_tx( + &self, + create_request: CreateAccountCategoryRequest, + tx: &Db, + ) -> Result { + let new_category = AccountCategoryActiveModel { + name: Set(create_request.name), + account_type: Set(create_request.account_type.to_string()), + ..Default::default() + }; + let res = new_category.insert(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.update(tx).await.map_err(|e| e.to_string())?; + Ok(()) + } else { + Err("Category not found".to_string()) + } + } +} diff --git a/src-tauri/src/services/accounts/mod.rs b/src-tauri/src/services/accounts/mod.rs new file mode 100644 index 0000000..ac102e5 --- /dev/null +++ b/src-tauri/src/services/accounts/mod.rs @@ -0,0 +1,186 @@ +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, +}; + +pub mod category; + +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, + } + } +} + +pub struct CreateAccountRequest { + pub name: String, + pub category: CreateAccountCategoryRequest, +} + +pub struct UpdateAccountRequest { + pub name: Option, + pub category: Option, +} + +#[async_trait::async_trait] +pub trait AccountsService: Send + Sync + 'static { + async fn get_accounts(&self) -> Result, String>; + async fn get_account(&self, id: &i64) -> Result, String>; + async fn create_account(&self, create_request: CreateAccountRequest) -> Result; + 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, +} + +impl AccountsServiceImpl { + pub fn new( + db: DatabaseConnection, + account_category_service: Arc, + ) -> Self { + Self { + db, + account_category_service, + } + } +} + +#[async_trait::async_trait] +impl AccountsService for AccountsServiceImpl { + async fn get_accounts(&self) -> Result, 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, 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 { + 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_string()), + updated_at: Set(chrono::Utc::now().to_string()), + 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_string()); + // + 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)), + } + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 0000000..7b00bcb --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1,23 @@ +use std::sync::Arc; + +use sea_orm::DatabaseConnection; + +pub mod accounts; + +pub struct Services { + pub account_category: Arc, + pub accounts: Arc, +} + +pub fn create_services(db: DatabaseConnection) -> Services { + let account_category_service = Arc::new(accounts::category::AccountCategoryServiceImpl::new( + db.clone(), + )); + Services { + account_category: account_category_service.clone(), + accounts: Arc::new(accounts::AccountsServiceImpl::new( + db, + account_category_service, + )), + } +}