summaryrefslogtreecommitdiff
path: root/actualbudget_queries
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2024-12-12 08:28:04 +0000
committerJoe Carstairs <me@joeac.net>2024-12-12 08:28:04 +0000
commit54894f9de9bad4fecb2a116d59a03cdd003f84c0 (patch)
tree6ce5517cecf23d4e93d7a381a374e1fa6c810cee /actualbudget_queries
parent8a30bcbdf9264d235bf93da3df8e170cfde53d02 (diff)
Moves rust workspace to root
Diffstat (limited to 'actualbudget_queries')
-rw-r--r--actualbudget_queries/Cargo.toml13
-rw-r--r--actualbudget_queries/README.md70
-rw-r--r--actualbudget_queries/src/actualbudget_accounts.rs25
-rw-r--r--actualbudget_queries/src/actualbudget_categories.rs7
-rw-r--r--actualbudget_queries/src/actualbudget_payees.rs13
-rw-r--r--actualbudget_queries/src/actualbudget_transactions.rs23
-rw-r--r--actualbudget_queries/src/actualbudget_zero_budgets.rs20
-rw-r--r--actualbudget_queries/src/lib.rs5
-rw-r--r--actualbudget_queries/tests/actualbudget_accounts.rs32
-rw-r--r--actualbudget_queries/tests/actualbudget_categories.rs19
-rw-r--r--actualbudget_queries/tests/actualbudget_payees.rs25
-rw-r--r--actualbudget_queries/tests/actualbudget_transactions.rs38
-rw-r--r--actualbudget_queries/tests/actualbudget_zero_budgets.rs34
-rw-r--r--actualbudget_queries/tests/common/account_names.rs17
-rw-r--r--actualbudget_queries/tests/common/category_names.rs29
-rw-r--r--actualbudget_queries/tests/common/db_url.rs1
-rw-r--r--actualbudget_queries/tests/common/get_reference_data.rs45
-rw-r--r--actualbudget_queries/tests/common/mod.rs8
-rw-r--r--actualbudget_queries/tests/common/payee_matchers.rs124
-rw-r--r--actualbudget_queries/tests/common/payee_names.rs17
-rw-r--r--actualbudget_queries/tests/common/transaction_matchers.rs315
-rw-r--r--actualbudget_queries/tests/common/zero_budget_matchers.rs285
-rw-r--r--actualbudget_queries/tests/resources/actualbudget_test_db.sqlitebin0 -> 528384 bytes
-rw-r--r--actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json1
24 files changed, 1166 insertions, 0 deletions
diff --git a/actualbudget_queries/Cargo.toml b/actualbudget_queries/Cargo.toml
new file mode 100644
index 0000000..78fd603
--- /dev/null
+++ b/actualbudget_queries/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "actualbudget_queries"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+actualbudget_models = { path = "../actualbudget_models" }
+actualbudget_schema = { path = "../actualbudget_schema" }
+anyhow = { workspace = true }
+diesel = { workspace = true }
+
+[dev-dependencies]
+libsqlite3-sys = { workspace = true, features = ["bundled"] }
diff --git a/actualbudget_queries/README.md b/actualbudget_queries/README.md
new file mode 100644
index 0000000..69ab0e7
--- /dev/null
+++ b/actualbudget_queries/README.md
@@ -0,0 +1,70 @@
+# Actualbudget Queries
+
+This is a Rust library defining queries for querying an Actualbudget database
+export.
+
+## Creating the test database
+
+To re-create the test database from scratch:
+
+1. Open an instance of Actualbudget
+2. Start a new file
+3. Add a new local account, called 'My bank account', with an initial balance of
+ 1200.00
+4. Add a new local account, called 'My cash account', with an initial balance of
+ 45.10
+5. Add a new local account, called 'Category transfers', with an initial balance
+ of nil
+6. Move the Starting Balance transaction for both to 1 Aug 2024
+6. Go to the Budgets tab
+7. Adjust the budgets for the existing categories for August 2024:
+ - Usual Expenses, under which:
+ - Food: 200
+ - General: 100
+ - Bills: 125
+ - Bills (flexible): 25
+ - Investments and Savings, under which:
+ - Savings: 400
+8. Add transactions for Income for 846.46 in the 'My bank account' on 21 Aug,
+ Sept, Oct and Nov 2024, with the 'Note' column set to 'Salary', ticking the
+ 'cleared' box
+9. On 10 Sept 2024, add a transaction in 'My bank account' transferring 80.00 to
+ 'My cash account' with no Note, and tick the 'cleared' box
+10. Add transactions for 200.00 to 'Tesco' in 'My bank account', categorised as
+ 'Food', for 8 Aug, Sept, Oct, and Nov 2024
+11. In the 'Category transfers' account, add a transaction for 23rd Oct 2024,
+ transferring 120.00 from 'Savings' to 'General'
+12. Export the database by going to Settings > Export data
+13. Download the resulting ZIP archive
+14. Extract `db.sqlite` and `metadata.json`
+15. Move them to
+ `actualbudget_queries/tests/resources/actualbudget_test_db.sqlite` and
+ `actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json`
+ respectively, overwriting the files which are already there
+16. Format the JSON in `actualbudget_test_db_metadata.json` to minimise diffs
+
+## Updating the test database
+
+To make a change to the test database:
+
+1. Make a copy of `actualbudget_test_db_metadata.json` and rename it to
+ `metadata.json`
+2. Make a copy of `actualbudget_test_db.sqlite` and rename it to `db.sqlite`
+3. Compress these files together in a `.zip` archive
+4. Open an instance of Actualbudget
+5. If you have a file open, close it
+6. Choose 'Import file'
+7. Choose to import an 'Actual' file
+8. Choose the ZIP archive you just made
+9. This should open the file
+10. Make your edits
+11. Document your edits in
+ [the database creation guide](#creating-the-test-database)
+12. Export the database by going to Settings > Export data
+13. Download the resulting ZIP archive
+14. Extract the `.sqlite` database file
+15. Move them to
+ `actualbudget_queries/tests/resources/actualbudget_test_db.sqlite` and
+ `actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json`
+ respectively, overwriting the files which are already there
+16. Format the JSON in `actualbudget_test_db_metadata.json` to minimise diffs
diff --git a/actualbudget_queries/src/actualbudget_accounts.rs b/actualbudget_queries/src/actualbudget_accounts.rs
new file mode 100644
index 0000000..d499208
--- /dev/null
+++ b/actualbudget_queries/src/actualbudget_accounts.rs
@@ -0,0 +1,25 @@
+use actualbudget_models::ActualbudgetAccount;
+use actualbudget_schema::actualbudget_schema::accounts::{
+ self as accounts_schema,
+ dsl::accounts as accounts_table,
+};
+use anyhow::{Context, Result};
+use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_accounts(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetAccount>> {
+ let actualbudget_accounts = accounts_table
+ .select(ActualbudgetAccount::as_select())
+ .load(connection)
+ .with_context(|| "failed to get all Actualbudget accounts")?;
+ Ok(actualbudget_accounts)
+}
+
+pub fn get_category_transfers_account(connection: &mut SqliteConnection) -> Result<Option<ActualbudgetAccount>> {
+ let category_transfers_account = accounts_table
+ .filter(accounts_schema::name.eq("Category transfers"))
+ .select(ActualbudgetAccount::as_select())
+ .first(connection)
+ .optional()
+ .with_context(|| "failed to get Category Transfers account")?;
+ Ok(category_transfers_account)
+}
diff --git a/actualbudget_queries/src/actualbudget_categories.rs b/actualbudget_queries/src/actualbudget_categories.rs
new file mode 100644
index 0000000..846faf5
--- /dev/null
+++ b/actualbudget_queries/src/actualbudget_categories.rs
@@ -0,0 +1,7 @@
+use actualbudget_models::ActualbudgetCategory;
+use actualbudget_schema::actualbudget_schema::categories::dsl::categories;
+use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_categories(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetCategory>, Error> {
+ categories.select(ActualbudgetCategory::as_select()).load(connection)
+}
diff --git a/actualbudget_queries/src/actualbudget_payees.rs b/actualbudget_queries/src/actualbudget_payees.rs
new file mode 100644
index 0000000..d5b0c5f
--- /dev/null
+++ b/actualbudget_queries/src/actualbudget_payees.rs
@@ -0,0 +1,13 @@
+use actualbudget_models::ActualbudgetPayee;
+use actualbudget_schema::actualbudget_schema::payees::dsl::payees;
+use anyhow::{Result, Context};
+use diesel::{QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_payees(
+ connection: &mut SqliteConnection,
+) -> Result<Vec<ActualbudgetPayee>> {
+ payees
+ .select(ActualbudgetPayee::as_select())
+ .load(connection)
+ .with_context(|| "failed to get actualbudget payees")
+}
diff --git a/actualbudget_queries/src/actualbudget_transactions.rs b/actualbudget_queries/src/actualbudget_transactions.rs
new file mode 100644
index 0000000..4dc38e8
--- /dev/null
+++ b/actualbudget_queries/src/actualbudget_transactions.rs
@@ -0,0 +1,23 @@
+use actualbudget_models::{ActualbudgetDate, ActualbudgetTransaction};
+use actualbudget_schema::actualbudget_schema::v_transactions::{
+ self as transactions_schema,
+ dsl::v_transactions as transactions_table,
+};
+use anyhow::{Context, Result};
+use diesel::{dsl::min, result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_transactions(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetTransaction>> {
+ let actualbudget_transactions = transactions_table
+ .select(ActualbudgetTransaction::as_select())
+ .load(connection)
+ .with_context(|| "failed to get all Actualbudget transactions")?;
+ Ok(actualbudget_transactions)
+}
+
+pub fn get_first_actualbudget_transaction_date(connection: &mut SqliteConnection) -> Result<ActualbudgetDate> {
+ let date_as_int = transactions_table
+ .select(min(transactions_schema::date))
+ .first::<Option<i32>>(connection)
+ .map_or_else(|e| Err(e), |o| o.map_or_else(|| Err(Error::NotFound), |i| Ok(i)))?;
+ Ok(ActualbudgetDate::from_i32(date_as_int))
+}
diff --git a/actualbudget_queries/src/actualbudget_zero_budgets.rs b/actualbudget_queries/src/actualbudget_zero_budgets.rs
new file mode 100644
index 0000000..5ae4055
--- /dev/null
+++ b/actualbudget_queries/src/actualbudget_zero_budgets.rs
@@ -0,0 +1,20 @@
+use anyhow::{Context, Result};
+use actualbudget_models::{actualbudget_schema::zero_budgets as zero_budgets_schema, ActualbudgetDate, ActualbudgetZeroBudget};
+use actualbudget_schema::actualbudget_schema::zero_budgets::dsl::zero_budgets as zero_budgets_table;
+use diesel::{dsl::min, result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_zero_budgets(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetZeroBudget>> {
+ let zero_budgets = zero_budgets_table
+ .select(ActualbudgetZeroBudget::as_select())
+ .load(connection)
+ .with_context(|| "failed to get all Actualbudget zero budgets")?;
+ Ok(zero_budgets)
+}
+
+pub fn get_first_zero_budget_date(connection: &mut SqliteConnection) -> Result<ActualbudgetDate> {
+ let date_as_int = zero_budgets_table
+ .select(min(zero_budgets_schema::month))
+ .first::<Option<i32>>(connection)
+ .map_or_else(|e| Err(e), |o| o.map_or_else(|| Err(Error::NotFound), |i| Ok(i)))?;
+ Ok(ActualbudgetDate::from_i32(date_as_int))
+}
diff --git a/actualbudget_queries/src/lib.rs b/actualbudget_queries/src/lib.rs
new file mode 100644
index 0000000..ca3f6ea
--- /dev/null
+++ b/actualbudget_queries/src/lib.rs
@@ -0,0 +1,5 @@
+pub mod actualbudget_accounts;
+pub mod actualbudget_categories;
+pub mod actualbudget_payees;
+pub mod actualbudget_transactions;
+pub mod actualbudget_zero_budgets;
diff --git a/actualbudget_queries/tests/actualbudget_accounts.rs b/actualbudget_queries/tests/actualbudget_accounts.rs
new file mode 100644
index 0000000..954cac1
--- /dev/null
+++ b/actualbudget_queries/tests/actualbudget_accounts.rs
@@ -0,0 +1,32 @@
+pub mod common;
+
+use actualbudget_queries::actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account};
+use common::{account_names::ACCOUNT_NAMES, db_url::DB_URL};
+use diesel::{Connection, SqliteConnection};
+
+#[test]
+fn given_test_database_when_get_all_accounts_then_return_accounts() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+
+ let accounts = get_all_actualbudget_accounts(connection);
+
+ assert!(accounts.is_ok());
+ let accounts = accounts.unwrap();
+ assert_eq!(ACCOUNT_NAMES.len(), accounts.len());
+ for account_name in ACCOUNT_NAMES {
+ assert!(accounts.iter().any(|a| a.name.as_str() == *account_name));
+ }
+}
+
+#[test]
+fn given_test_database_when_get_category_transfer_account_then_return_category_transfer_account() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+
+ let account = get_category_transfers_account(connection);
+
+ assert!(account.is_ok());
+ let account = account.unwrap();
+ assert!(account.is_some());
+ let account = account.unwrap();
+ assert!(account.name == "Category transfers");
+}
diff --git a/actualbudget_queries/tests/actualbudget_categories.rs b/actualbudget_queries/tests/actualbudget_categories.rs
new file mode 100644
index 0000000..27c17c8
--- /dev/null
+++ b/actualbudget_queries/tests/actualbudget_categories.rs
@@ -0,0 +1,19 @@
+pub mod common;
+
+use actualbudget_queries::actualbudget_categories::get_all_actualbudget_categories;
+use common::{category_names::CATEGORY_NAMES, db_url::DB_URL};
+use diesel::{Connection, SqliteConnection};
+
+#[test]
+fn given_test_database_when_get_all_categories_then_return_categories() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+
+ let categories = get_all_actualbudget_categories(connection);
+
+ assert!(categories.is_ok());
+ let categories = categories.unwrap();
+ assert_eq!(CATEGORY_NAMES.len(), categories.len());
+ for &category_name in CATEGORY_NAMES {
+ assert!(categories.iter().any(|a| a.name.as_str() == category_name));
+ }
+}
diff --git a/actualbudget_queries/tests/actualbudget_payees.rs b/actualbudget_queries/tests/actualbudget_payees.rs
new file mode 100644
index 0000000..acf5c4e
--- /dev/null
+++ b/actualbudget_queries/tests/actualbudget_payees.rs
@@ -0,0 +1,25 @@
+pub mod common;
+
+use actualbudget_queries::actualbudget_payees::get_all_actualbudget_payees;
+use common::{db_url::DB_URL, get_reference_data::get_accounts, payee_matchers::get_payee_matchers};
+use diesel::{Connection, SqliteConnection};
+
+#[test]
+fn given_test_database_when_get_all_payees_then_return_payees() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+ let accounts = get_accounts(connection);
+
+ let payees = get_all_actualbudget_payees(connection);
+
+ assert!(payees.is_ok());
+ let payees = payees.unwrap();
+ let payee_matchers = get_payee_matchers();
+ assert_eq!(payee_matchers.len(), payees.len());
+ for payee in payees {
+ assert!(
+ payee_matchers.iter().any(|payee_matcher| payee_matcher(&payee, &accounts)),
+ "no match found for payee {:?}",
+ payee,
+ );
+ }
+}
diff --git a/actualbudget_queries/tests/actualbudget_transactions.rs b/actualbudget_queries/tests/actualbudget_transactions.rs
new file mode 100644
index 0000000..c9d72a9
--- /dev/null
+++ b/actualbudget_queries/tests/actualbudget_transactions.rs
@@ -0,0 +1,38 @@
+pub mod common;
+
+use actualbudget_queries::actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date};
+use common::{db_url::DB_URL, get_reference_data::{get_accounts, get_categories}, transaction_matchers::get_transaction_matchers};
+use diesel::{Connection, SqliteConnection};
+
+#[test]
+fn given_test_database_when_get_all_transactions_then_return_transactions() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+ let accounts = get_accounts(connection);
+ let categories = get_categories(connection);
+
+ let transactions = get_all_actualbudget_transactions(connection);
+
+ assert!(transactions.is_ok());
+ let transactions = transactions.unwrap();
+ let transaction_matchers = get_transaction_matchers();
+ assert_eq!(transaction_matchers.len(), transactions.len());
+ for transaction in transactions {
+ let any_match = transaction_matchers.iter().any(|transaction_matcher|
+ transaction_matcher(&transaction, &accounts, &categories)
+ );
+ assert!(any_match, "no match found for transaction {:?}", transaction);
+ }
+}
+
+#[test]
+fn given_test_database_when_get_first_transaction_date_then_return_01_aug_2024() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+
+ let date = get_first_actualbudget_transaction_date(connection);
+
+ assert!(date.is_ok());
+ let date = date.unwrap();
+ assert_eq!("01", date.day());
+ assert_eq!("08", date.month());
+ assert_eq!("2024", date.year());
+}
diff --git a/actualbudget_queries/tests/actualbudget_zero_budgets.rs b/actualbudget_queries/tests/actualbudget_zero_budgets.rs
new file mode 100644
index 0000000..7218227
--- /dev/null
+++ b/actualbudget_queries/tests/actualbudget_zero_budgets.rs
@@ -0,0 +1,34 @@
+pub mod common;
+
+use actualbudget_queries::actualbudget_zero_budgets::{get_all_actualbudget_zero_budgets, get_first_zero_budget_date};
+use common::{db_url::DB_URL, get_reference_data::get_categories, zero_budget_matchers::get_zero_budget_matchers};
+use diesel::{Connection, SqliteConnection};
+
+#[test]
+fn given_test_database_when_get_all_zero_budgets_then_return_zero_budgets() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+ let categories = get_categories(connection);
+
+ let zero_budgets = get_all_actualbudget_zero_budgets(connection);
+
+ assert!(zero_budgets.is_ok());
+ let zero_budgets = zero_budgets.unwrap();
+ let zero_budget_matchers = get_zero_budget_matchers();
+ assert_eq!(zero_budget_matchers.len(), zero_budgets.len());
+ for matcher in zero_budget_matchers {
+ assert!(zero_budgets.iter().any(|zb| matcher(zb, &categories)))
+ }
+}
+
+#[test]
+fn given_test_database_when_get_first_zero_budget_date_then_return_1_aug_2024() {
+ let connection = &mut SqliteConnection::establish(DB_URL).unwrap();
+
+ let date = get_first_zero_budget_date(connection);
+
+ assert!(date.is_ok());
+ let date = date.unwrap();
+ assert_eq!("01", date.day());
+ assert_eq!("08", date.month());
+ assert_eq!("2024", date.year());
+}
diff --git a/actualbudget_queries/tests/common/account_names.rs b/actualbudget_queries/tests/common/account_names.rs
new file mode 100644
index 0000000..1abee4b
--- /dev/null
+++ b/actualbudget_queries/tests/common/account_names.rs
@@ -0,0 +1,17 @@
+pub const ACCOUNT_NAMES: &[&'static str] = &[
+ "My bank account",
+ "My cash account",
+ "Category transfers",
+];
+
+pub struct AccountNamesDict {
+ pub bank_account: &'static str,
+ pub cash_account: &'static str,
+ pub transfers_account: &'static str,
+}
+
+pub const ACCOUNT_NAMES_DICT: AccountNamesDict = AccountNamesDict {
+ bank_account: ACCOUNT_NAMES[0],
+ cash_account: ACCOUNT_NAMES[1],
+ transfers_account: ACCOUNT_NAMES[2],
+};
diff --git a/actualbudget_queries/tests/common/category_names.rs b/actualbudget_queries/tests/common/category_names.rs
new file mode 100644
index 0000000..a2f7d84
--- /dev/null
+++ b/actualbudget_queries/tests/common/category_names.rs
@@ -0,0 +1,29 @@
+pub const CATEGORY_NAMES: &[&'static str] = &[
+ "Food",
+ "General",
+ "Bills",
+ "Bills (Flexible)",
+ "Savings",
+ "Starting Balances",
+ "Income",
+];
+
+pub struct CategoryNamesDict {
+ pub food: &'static str,
+ pub general: &'static str,
+ pub bills: &'static str,
+ pub bills_flexible: &'static str,
+ pub savings: &'static str,
+ pub starting_balances: &'static str,
+ pub income: &'static str,
+}
+
+pub const CATEGORY_NAMES_DICT: CategoryNamesDict = CategoryNamesDict {
+ food: CATEGORY_NAMES[0],
+ general: CATEGORY_NAMES[1],
+ bills: CATEGORY_NAMES[2],
+ bills_flexible: CATEGORY_NAMES[3],
+ savings: CATEGORY_NAMES[4],
+ starting_balances: CATEGORY_NAMES[5],
+ income: CATEGORY_NAMES[6],
+};
diff --git a/actualbudget_queries/tests/common/db_url.rs b/actualbudget_queries/tests/common/db_url.rs
new file mode 100644
index 0000000..ff778a8
--- /dev/null
+++ b/actualbudget_queries/tests/common/db_url.rs
@@ -0,0 +1 @@
+pub const DB_URL: &str = "tests/resources/actualbudget_test_db.sqlite";
diff --git a/actualbudget_queries/tests/common/get_reference_data.rs b/actualbudget_queries/tests/common/get_reference_data.rs
new file mode 100644
index 0000000..0d4f19c
--- /dev/null
+++ b/actualbudget_queries/tests/common/get_reference_data.rs
@@ -0,0 +1,45 @@
+use std::collections::HashMap;
+
+use actualbudget_models::{ActualbudgetAccount, ActualbudgetCategory};
+use actualbudget_queries::{actualbudget_accounts::get_all_actualbudget_accounts, actualbudget_categories::get_all_actualbudget_categories};
+use diesel::SqliteConnection;
+
+use super::{account_names::ACCOUNT_NAMES, category_names::CATEGORY_NAMES};
+
+pub fn get_accounts(connection: &mut SqliteConnection) -> HashMap<String, ActualbudgetAccount> {
+ let accounts = get_all_actualbudget_accounts(connection).expect("failed to get accounts");
+ let mut accounts_hash_map = HashMap::new();
+
+ for &account_name in ACCOUNT_NAMES {
+ let account = accounts
+ .clone()
+ .into_iter()
+ .find(|a| a.name.as_str() == account_name)
+ .expect(&format!("failed to get account: {}", account_name));
+ accounts_hash_map.insert(
+ String::from(account_name),
+ account,
+ );
+ }
+
+ accounts_hash_map
+}
+
+pub fn get_categories(connection: &mut SqliteConnection) -> HashMap<String, ActualbudgetCategory> {
+ let categories = get_all_actualbudget_categories(connection).expect("failed to get categories");
+ let mut categories_hash_map = HashMap::new();
+
+ for &category_name in CATEGORY_NAMES {
+ let category = categories
+ .clone()
+ .into_iter()
+ .find(|a| a.name.as_str() == category_name)
+ .expect(&format!("failed to get category: {}", category_name));
+ categories_hash_map.insert(
+ String::from(category_name),
+ category,
+ );
+ }
+
+ categories_hash_map
+}
diff --git a/actualbudget_queries/tests/common/mod.rs b/actualbudget_queries/tests/common/mod.rs
new file mode 100644
index 0000000..b19b061
--- /dev/null
+++ b/actualbudget_queries/tests/common/mod.rs
@@ -0,0 +1,8 @@
+pub mod account_names;
+pub mod category_names;
+pub mod db_url;
+pub mod get_reference_data;
+pub mod payee_matchers;
+pub mod payee_names;
+pub mod transaction_matchers;
+pub mod zero_budget_matchers;
diff --git a/actualbudget_queries/tests/common/payee_matchers.rs b/actualbudget_queries/tests/common/payee_matchers.rs
new file mode 100644
index 0000000..ccba204
--- /dev/null
+++ b/actualbudget_queries/tests/common/payee_matchers.rs
@@ -0,0 +1,124 @@
+use std::collections::HashMap;
+
+use actualbudget_models::{ActualbudgetAccount, ActualbudgetPayee};
+use anyhow::Context;
+
+use super::{account_names::ACCOUNT_NAMES_DICT, payee_names::PAYEE_NAMES_DICT};
+
+pub fn get_payee_matchers(
+) -> Vec<Box<dyn Fn(&ActualbudgetPayee, &HashMap<String, ActualbudgetAccount>) -> bool>> {
+ vec![
+ Box::new(|p, a| matches_bank_account_transfer_account(p, a)),
+ Box::new(|p, a| matches_cash_account_transfer_account(p, a)),
+ Box::new(|p, a| matches_category_transfers_transfer_account(p, a)),
+ Box::new(|p, a| matches_starting_balances(p, a)),
+ Box::new(|p, a| matches_employer(p, a)),
+ Box::new(|p, a| matches_tesco(p, a)),
+ ]
+}
+
+fn matches_bank_account_transfer_account(
+ payee: &ActualbudgetPayee,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+) -> bool {
+ let bank_account = accounts
+ .get(ACCOUNT_NAMES_DICT.bank_account)
+ .with_context(|| "failed to get bank account")
+ .unwrap();
+
+ match payee {
+ ActualbudgetPayee {
+ name,
+ transfer_account_id,
+ ..
+ } => name == ""
+ && transfer_account_id.clone().is_some_and(
+ |account_id| account_id == bank_account.id,
+ ),
+ }
+}
+
+fn matches_cash_account_transfer_account(
+ payee: &ActualbudgetPayee,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+) -> bool {
+ let cash_account = accounts
+ .get(ACCOUNT_NAMES_DICT.cash_account)
+ .with_context(|| "failed to find cash account")
+ .unwrap();
+
+ match payee {
+ ActualbudgetPayee {
+ name,
+ transfer_account_id,
+ ..
+ } => name == ""
+ && transfer_account_id.clone().is_some_and(
+ |account_id| account_id == cash_account.id,
+ ),
+ }
+}
+
+fn matches_category_transfers_transfer_account(
+ payee: &ActualbudgetPayee,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+) -> bool {
+ let category_transfers_account = accounts
+ .get(ACCOUNT_NAMES_DICT.transfers_account)
+ .with_context(|| "failed to find category transfers account")
+ .unwrap();
+
+ match payee {
+ ActualbudgetPayee {
+ name,
+ transfer_account_id,
+ ..
+ } => name == ""
+ && transfer_account_id.clone().is_some_and(
+ |account_id| account_id == category_transfers_account.id,
+ ),
+ }
+}
+
+fn matches_starting_balances(
+ payee: &ActualbudgetPayee,
+ _accounts: &HashMap<String, ActualbudgetAccount>,
+) -> bool {
+ match payee {
+ ActualbudgetPayee {
+ name,
+ transfer_account_id,
+ ..
+ } => transfer_account_id.is_none()
+ && name.clone() == PAYEE_NAMES_DICT.starting_balances,
+ }
+}
+
+fn matches_employer(
+ payee: &ActualbudgetPayee,
+ _accounts: &HashMap<String, ActualbudgetAccount>,
+) -> bool {
+ match payee {
+ ActualbudgetPayee {
+ name,
+ transfer_account_id,
+ ..
+ } => transfer_account_id.is_none()
+ && name == PAYEE_NAMES_DICT.employer,
+ }
+}
+
+
+fn matches_tesco(
+ payee: &ActualbudgetPayee,
+ _accounts: &HashMap<String, ActualbudgetAccount>,
+) -> bool {
+ match payee {
+ ActualbudgetPayee {
+ name,
+ transfer_account_id,
+ ..
+ } => transfer_account_id.is_none()
+ && name == PAYEE_NAMES_DICT.tesco,
+ }
+}
diff --git a/actualbudget_queries/tests/common/payee_names.rs b/actualbudget_queries/tests/common/payee_names.rs
new file mode 100644
index 0000000..42b6024
--- /dev/null
+++ b/actualbudget_queries/tests/common/payee_names.rs
@@ -0,0 +1,17 @@
+pub const PAYEE_NAMES: &[&'static str] = &[
+ "Starting Balance",
+ "Employer Ltd",
+ "Tesco",
+];
+
+pub struct PayeeNamesDict {
+ pub starting_balances: &'static str,
+ pub employer: &'static str,
+ pub tesco: &'static str,
+}
+
+pub const PAYEE_NAMES_DICT: PayeeNamesDict = PayeeNamesDict {
+ starting_balances: PAYEE_NAMES[0],
+ employer: PAYEE_NAMES[1],
+ tesco: PAYEE_NAMES[2],
+};
diff --git a/actualbudget_queries/tests/common/transaction_matchers.rs b/actualbudget_queries/tests/common/transaction_matchers.rs
new file mode 100644
index 0000000..2620da5
--- /dev/null
+++ b/actualbudget_queries/tests/common/transaction_matchers.rs
@@ -0,0 +1,315 @@
+use std::collections::HashMap;
+
+use actualbudget_models::{ActualbudgetAccount, ActualbudgetCategory, ActualbudgetTransaction};
+
+use super::{account_names::ACCOUNT_NAMES_DICT, category_names::CATEGORY_NAMES_DICT};
+
+pub fn get_transaction_matchers() -> Vec<Box<dyn Fn(&ActualbudgetTransaction, &HashMap<String, ActualbudgetAccount>, &HashMap<String, ActualbudgetCategory>) -> bool>> {
+ vec![
+ Box::new(|t, a, c| matches_bank_account_initial_transaction(t, a, c)),
+ Box::new(|t, a, c| matches_cash_account_initial_transaction(t, a, c)),
+ Box::new(|t, a, c| matches_income_transaction(t, a, c, "08")),
+ Box::new(|t, a, c| matches_income_transaction(t, a, c, "09")),
+ Box::new(|t, a, c| matches_income_transaction(t, a, c, "10")),
+ Box::new(|t, a, c| matches_income_transaction(t, a, c, "11")),
+ Box::new(|t, a, c| matches_food_transaction(t, a, c, "08")),
+ Box::new(|t, a, c| matches_food_transaction(t, a, c, "09")),
+ Box::new(|t, a, c| matches_food_transaction(t, a, c, "10")),
+ Box::new(|t, a, c| matches_food_transaction(t, a, c, "11")),
+ Box::new(|t, a, c| matches_category_transfer_parent_transaction(t, a, c)),
+ Box::new(|t, a, c| matches_category_transfer_child_out_transaction(t, a, c)),
+ Box::new(|t, a, c| matches_category_transfer_child_in_transaction(t, a, c)),
+ Box::new(|t, a, c| matches_account_transfer_out_transaction(t, a, c)),
+ Box::new(|t, a, c| matches_account_transfer_in_transaction(t, a, c)),
+ ]
+}
+
+fn matches_bank_account_initial_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ categories: &HashMap<String, ActualbudgetCategory>,
+) -> bool {
+ let bank_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.bank_account)
+ .expect("failed to find bank account")
+ .id
+ .clone();
+
+ let starting_balances_category_id = categories
+ .get(CATEGORY_NAMES_DICT.starting_balances)
+ .expect("failed to find starting balances category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: false,
+ parent_id: None,
+ account_id,
+ category_id: Some(category_id),
+ amount: 1200_00,
+ date,
+ ..
+ } => *account_id == bank_account_id
+ && *category_id == starting_balances_category_id
+ && date.to_iso().get(0..10) == Some("2024-08-01"),
+ _ => false,
+ }
+}
+
+fn matches_cash_account_initial_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ categories: &HashMap<String, ActualbudgetCategory>,
+) -> bool {
+ let bank_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.cash_account)
+ .expect("failed to find cash account")
+ .id
+ .clone();
+
+ let starting_balances_category_id = categories
+ .get(CATEGORY_NAMES_DICT.starting_balances)
+ .expect("failed to find starting balances category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: false,
+ parent_id: None,
+ account_id,
+ category_id: Some(category_id),
+ amount: 45_10,
+ date,
+ ..
+ } => *account_id == bank_account_id
+ && *category_id == starting_balances_category_id
+ && date.to_iso().get(0..10) == Some("2024-08-01"),
+ _ => false,
+ }
+}
+
+fn matches_income_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let bank_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.bank_account)
+ .expect("failed to find bank account")
+ .id
+ .clone();
+
+ let income_category_id = categories
+ .get(CATEGORY_NAMES_DICT.income)
+ .expect("failed to find income category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: false,
+ parent_id: None,
+ account_id,
+ category_id,
+ amount: 846_46,
+ date,
+ notes: Some(notes),
+ ..
+ } =>
+ *account_id == bank_account_id
+ && *category_id == Some(income_category_id)
+ && date.to_iso().get(0..10) == Some(format!("2024-{}-21", month).as_str())
+ && *notes == String::from("Salary"),
+ _ => false,
+ }
+}
+
+fn matches_food_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let bank_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.bank_account)
+ .expect("failed to find bank account")
+ .id
+ .clone();
+
+ let income_category_id = categories
+ .get(CATEGORY_NAMES_DICT.food)
+ .expect("failed to find food category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: false,
+ parent_id: None,
+ account_id,
+ category_id,
+ amount: -200_00,
+ date,
+ ..
+ } => *account_id == bank_account_id
+ && *category_id == Some(income_category_id)
+ && date.to_iso().get(0..10) == Some(format!("2024-{}-08", month).as_str()),
+ _ => false,
+ }
+}
+
+fn matches_category_transfer_parent_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ _categories: &HashMap<String, ActualbudgetCategory>,
+) -> bool {
+ let category_transfers_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.transfers_account)
+ .expect("failed to find transfers account")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: true,
+ is_child: false,
+ parent_id: None,
+ account_id,
+ category_id: None,
+ amount: 0,
+ date,
+ ..
+ } => *account_id == category_transfers_account_id
+ && date.to_iso().get(0..10) == Some("2024-10-23"),
+ _ => false,
+ }
+}
+
+fn matches_category_transfer_child_out_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ categories: &HashMap<String, ActualbudgetCategory>,
+) -> bool {
+ let category_transfers_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.transfers_account)
+ .expect("failed to find transfers account")
+ .id
+ .clone();
+
+ let savings_category_id = categories
+ .get(CATEGORY_NAMES_DICT.savings)
+ .expect("failed to find savings category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: true,
+ parent_id: Some(_),
+ account_id,
+ category_id: Some(category_id),
+ amount: -120_00,
+ date,
+ ..
+ } => *account_id == category_transfers_account_id
+ && *category_id == savings_category_id
+ && date.to_iso().get(0..10) == Some("2024-10-23"),
+ _ => false,
+ }
+}
+
+fn matches_category_transfer_child_in_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ categories: &HashMap<String, ActualbudgetCategory>,
+) -> bool {
+ let category_transfers_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.transfers_account)
+ .expect("failed to find transfers account")
+ .id
+ .clone();
+
+ let savings_category_id = categories
+ .get(CATEGORY_NAMES_DICT.general)
+ .expect("failed to find general category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: true,
+ parent_id: Some(_),
+ account_id,
+ category_id: Some(category_id),
+ amount: 120_00,
+ date,
+ ..
+ } => *account_id == category_transfers_account_id
+ && *category_id == savings_category_id
+ && date.to_iso().get(0..10) == Some("2024-10-23"),
+ _ => false,
+ }
+}
+
+fn matches_account_transfer_out_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ _categories: &HashMap<String, ActualbudgetCategory>,
+) -> bool {
+ let bank_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.bank_account)
+ .expect("failed to find bank account")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: false,
+ parent_id: None,
+ account_id,
+ category_id: None,
+ amount: -80_00,
+ date,
+ ..
+ } => *account_id == bank_account_id
+ && date.to_iso().get(0..10) == Some("2024-09-10"),
+ _ => false,
+ }
+}
+
+fn matches_account_transfer_in_transaction(
+ t: &ActualbudgetTransaction,
+ accounts: &HashMap<String, ActualbudgetAccount>,
+ _categories: &HashMap<String, ActualbudgetCategory>,
+) -> bool {
+ let cash_account_id = accounts
+ .get(ACCOUNT_NAMES_DICT.cash_account)
+ .expect("failed to find cash account")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetTransaction {
+ is_parent: false,
+ is_child: false,
+ parent_id: None,
+ account_id,
+ category_id: None,
+ amount: 80_00,
+ date,
+ ..
+ } => *account_id == cash_account_id
+ && date.to_iso().get(0..10) == Some("2024-09-10"),
+ _ => false,
+ }
+}
diff --git a/actualbudget_queries/tests/common/zero_budget_matchers.rs b/actualbudget_queries/tests/common/zero_budget_matchers.rs
new file mode 100644
index 0000000..ac88d3b
--- /dev/null
+++ b/actualbudget_queries/tests/common/zero_budget_matchers.rs
@@ -0,0 +1,285 @@
+use std::collections::HashMap;
+
+use actualbudget_models::{ActualbudgetCategory, ActualbudgetZeroBudget};
+
+use super::category_names::CATEGORY_NAMES_DICT;
+
+pub fn get_zero_budget_matchers() -> Vec<Box<dyn Fn(&ActualbudgetZeroBudget, &HashMap<String, ActualbudgetCategory>) -> bool>> {
+ vec![
+ Box::new(|t, c| matches_food_budget(t, c, "08")),
+ Box::new(|t, c| matches_food_budget(t, c, "09")),
+ Box::new(|t, c| matches_food_budget(t, c, "10")),
+ Box::new(|t, c| matches_food_budget(t, c, "11")),
+ Box::new(|t, c| matches_zero_food_budget(t, c, "12")),
+ Box::new(|t, c| matches_general_budget(t, c, "08")),
+ Box::new(|t, c| matches_general_budget(t, c, "09")),
+ Box::new(|t, c| matches_general_budget(t, c, "10")),
+ Box::new(|t, c| matches_general_budget(t, c, "11")),
+ Box::new(|t, c| matches_zero_general_budget(t, c, "12")),
+ Box::new(|t, c| matches_bills_budget(t, c, "08")),
+ Box::new(|t, c| matches_bills_budget(t, c, "09")),
+ Box::new(|t, c| matches_bills_budget(t, c, "10")),
+ Box::new(|t, c| matches_bills_budget(t, c, "11")),
+ Box::new(|t, c| matches_zero_bills_budget(t, c, "12")),
+ Box::new(|t, c| matches_bills_flexible_budget(t, c, "08")),
+ Box::new(|t, c| matches_bills_flexible_budget(t, c, "09")),
+ Box::new(|t, c| matches_bills_flexible_budget(t, c, "10")),
+ Box::new(|t, c| matches_bills_flexible_budget(t, c, "11")),
+ Box::new(|t, c| matches_zero_bills_flexible_budget(t, c, "12")),
+ Box::new(|t, c| matches_savings_budget(t, c, "08")),
+ Box::new(|t, c| matches_savings_budget(t, c, "09")),
+ Box::new(|t, c| matches_savings_budget(t, c, "10")),
+ Box::new(|t, c| matches_savings_budget(t, c, "11")),
+ Box::new(|t, c| matches_zero_savings_budget(t, c, "12")),
+ ]
+}
+
+fn matches_food_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let food_category_id = categories
+ .get(CATEGORY_NAMES_DICT.food)
+ .expect("failed to find food category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 200_00,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == food_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_zero_food_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let food_category_id = categories
+ .get(CATEGORY_NAMES_DICT.food)
+ .expect("failed to find food category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 0,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == food_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_general_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let general_category_id = categories
+ .get(CATEGORY_NAMES_DICT.general)
+ .expect("failed to find general category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 100_00,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == general_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_zero_general_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let general_category_id = categories
+ .get(CATEGORY_NAMES_DICT.general)
+ .expect("failed to find general category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 0,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == general_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_bills_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let bills_category_id = categories
+ .get(CATEGORY_NAMES_DICT.bills)
+ .expect("failed to find bills category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 125_00,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == bills_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_zero_bills_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let bills_category_id = categories
+ .get(CATEGORY_NAMES_DICT.bills)
+ .expect("failed to find bills category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 0,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == bills_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_bills_flexible_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let bills_flexible_category_id = categories
+ .get(CATEGORY_NAMES_DICT.bills_flexible)
+ .expect("failed to find bills (flexible) category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 25_00,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == bills_flexible_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_zero_bills_flexible_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let bills_flexible_category_id = categories
+ .get(CATEGORY_NAMES_DICT.bills_flexible)
+ .expect("failed to find bills (flexible) category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 0,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == bills_flexible_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_savings_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let savings_category_id = categories
+ .get(CATEGORY_NAMES_DICT.savings)
+ .expect("failed to find savings category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 400_00,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == savings_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
+
+fn matches_zero_savings_budget(
+ t: &ActualbudgetZeroBudget,
+ categories: &HashMap<String, ActualbudgetCategory>,
+ month: &str,
+) -> bool {
+ let savings_category_id = categories
+ .get(CATEGORY_NAMES_DICT.savings)
+ .expect("failed to find savings category")
+ .id
+ .clone();
+
+ match t {
+ ActualbudgetZeroBudget {
+ month: date,
+ category_id,
+ amount: 0,
+ do_carry_over: false,
+ ..
+ } =>
+ *category_id == savings_category_id
+ && date.month() == month,
+ _ => false,
+ }
+}
diff --git a/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite b/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite
new file mode 100644
index 0000000..4146bb0
--- /dev/null
+++ b/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite
Binary files differ
diff --git a/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json b/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json
new file mode 100644
index 0000000..4d83f81
--- /dev/null
+++ b/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json
@@ -0,0 +1 @@
+{"id":"My-Finances-1-36ec11a","budgetName":"My Finances 1","userId":"fec9ba17-c260-4f55-8d44-676ef0ec5da9","lastScheduleRun":"2024-11-21","resetClock":true} \ No newline at end of file