summaryrefslogtreecommitdiff
path: root/backend/actualbudget_importer/src
diff options
context:
space:
mode:
authorJoe Carstairs <me@joeac.net>2024-11-17 09:22:32 +0000
committerJoe Carstairs <me@joeac.net>2024-11-17 09:22:32 +0000
commit1a494937021561595d0fb29e26fb707aef9e55de (patch)
tree91234bbc9d1946946cf93d4d57b0b5a13a09de0a /backend/actualbudget_importer/src
parent3c7218be1446f8cf914063b1c52c1cbdc8497c8c (diff)
Refactors rust code into workspace
Diffstat (limited to 'backend/actualbudget_importer/src')
-rw-r--r--backend/actualbudget_importer/src/main.rs45
-rw-r--r--backend/actualbudget_importer/src/models/actualbudget_account.rs8
-rw-r--r--backend/actualbudget_importer/src/models/actualbudget_category.rs8
-rw-r--r--backend/actualbudget_importer/src/models/actualbudget_date.rs75
-rw-r--r--backend/actualbudget_importer/src/models/actualbudget_transaction.rs18
-rw-r--r--backend/actualbudget_importer/src/models/actualbudget_zero_budget.rs13
-rw-r--r--backend/actualbudget_importer/src/models/mod.rs5
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_accounts/get_all.rs6
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_accounts/get_category_transfers_account.rs13
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_accounts/mod.rs5
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_categories/get_all.rs6
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_categories/mod.rs3
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_transactions/get_all.rs6
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_transactions/get_first_date.rs14
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_transactions/mod.rs5
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/get_all.rs6
-rw-r--r--backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/mod.rs3
-rw-r--r--backend/actualbudget_importer/src/queries/mod.rs4
-rw-r--r--backend/actualbudget_importer/src/schema.rs47
-rw-r--r--backend/actualbudget_importer/src/transformers.rs218
20 files changed, 508 insertions, 0 deletions
diff --git a/backend/actualbudget_importer/src/main.rs b/backend/actualbudget_importer/src/main.rs
new file mode 100644
index 0000000..cb69bb5
--- /dev/null
+++ b/backend/actualbudget_importer/src/main.rs
@@ -0,0 +1,45 @@
+pub mod models;
+pub mod queries;
+pub mod schema;
+pub mod transformers;
+
+use anyhow::Result;
+use budgeting_app_core::queries as budgeting_app_queries;
+use diesel::{Connection, SqliteConnection};
+use queries::{actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account}, actualbudget_categories::get_all_actualbudget_categories, actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date}, actualbudget_zero_budgets::get_all_actualbudget_zero_budgets};
+
+fn main() -> Result<()> {
+ let connection_src = &mut SqliteConnection::establish("in.sqlite").unwrap();
+ let connection_dest = &mut SqliteConnection::establish("out.sqlite").unwrap();
+
+ let actualbudget_first_transaction_date = get_first_actualbudget_transaction_date(connection_src)?;
+ let actualbudget_accounts = get_all_actualbudget_accounts(connection_src)?;
+ let actualbudget_categories = get_all_actualbudget_categories(connection_src)?;
+ let actualbudget_zero_budgets = get_all_actualbudget_zero_budgets(connection_src)?;
+ let actualbudget_category_transfers_account = get_category_transfers_account(connection_src)?;
+ let actualbudget_transactions = get_all_actualbudget_transactions(connection_src)?;
+
+ let first_transaction_date = transformers::transform_date(&actualbudget_first_transaction_date)?;
+ let accounts =
+ transformers::transform_accounts(&actualbudget_accounts, &first_transaction_date);
+ let categories = transformers::transform_categories(&actualbudget_categories);
+ let budget_updates = transformers::transform_budget_updates(&actualbudget_zero_budgets)?;
+ let (transactions, transaction_categorisations) = transformers::transform_transactions(
+ &actualbudget_transactions,
+ &actualbudget_category_transfers_account,
+ )?;
+ let category_transfers = transformers::transform_category_transfers(
+ &actualbudget_transactions,
+ &actualbudget_category_transfers_account,
+ );
+
+ budgeting_app_queries::clear::clear(connection_dest)?;
+ budgeting_app_queries::accounts::insert_accounts(&accounts, connection_dest)?;
+ budgeting_app_queries::categories::insert_categories(&categories, connection_dest)?;
+ budgeting_app_queries::budget_updates::insert_budget_updates(&budget_updates, connection_dest)?;
+ budgeting_app_queries::transactions::insert_transactions(&transactions, connection_dest)?;
+ budgeting_app_queries::transaction_categorisations::insert_transaction_categorisations(&transaction_categorisations, connection_dest)?;
+ budgeting_app_queries::category_transfers::insert_category_transfers(&category_transfers, connection_dest)?;
+
+ Ok(())
+}
diff --git a/backend/actualbudget_importer/src/models/actualbudget_account.rs b/backend/actualbudget_importer/src/models/actualbudget_account.rs
new file mode 100644
index 0000000..b3d00ac
--- /dev/null
+++ b/backend/actualbudget_importer/src/models/actualbudget_account.rs
@@ -0,0 +1,8 @@
+use diesel::prelude::{Queryable, Selectable};
+
+#[derive(Queryable, Selectable)]
+#[diesel(table_name = crate::schema::accounts)]
+pub struct ActualbudgetAccount {
+ pub id: String,
+ pub name: String,
+}
diff --git a/backend/actualbudget_importer/src/models/actualbudget_category.rs b/backend/actualbudget_importer/src/models/actualbudget_category.rs
new file mode 100644
index 0000000..cbea99b
--- /dev/null
+++ b/backend/actualbudget_importer/src/models/actualbudget_category.rs
@@ -0,0 +1,8 @@
+use diesel::{prelude::Queryable, Selectable};
+
+#[derive(Queryable, Selectable)]
+#[diesel(table_name = crate::schema::categories)]
+pub struct ActualbudgetCategory {
+ pub id: String,
+ pub name: String,
+}
diff --git a/backend/actualbudget_importer/src/models/actualbudget_date.rs b/backend/actualbudget_importer/src/models/actualbudget_date.rs
new file mode 100644
index 0000000..bef160c
--- /dev/null
+++ b/backend/actualbudget_importer/src/models/actualbudget_date.rs
@@ -0,0 +1,75 @@
+use diesel::{backend::Backend, deserialize::{FromSql, FromSqlRow}, expression::AsExpression, sql_types::Integer, sqlite::Sqlite};
+
+#[derive(Clone, PartialEq, Eq, Debug, AsExpression, FromSqlRow)]
+#[diesel(sql_type=Integer)]
+pub struct ActualbudgetDate(i32);
+
+#[derive(Clone, PartialOrd, PartialEq, Eq)]
+enum DatePart {
+ MonthOfYear,
+ DayOfMonth,
+}
+
+// actualbudget represents dates as numbers which "look like" the corresponding
+// ISO string when written in decimal. E.g. 20240901 represents 1 Sep 2024. It
+// also uses month specifiers, e.g. 202409 to represent Sep 2024. In this case,
+// we interpret this as the first day of the month, e.g. 2024-09-01.
+
+impl ActualbudgetDate {
+ pub fn to_iso(&self) -> String {
+ format!("{0}-{1}-{2}", self.year(), self.month(), self.day())
+ }
+
+ pub fn year(&self) -> String {
+ self.0.to_string()[0..4].to_string()
+ }
+
+ pub fn month(&self) -> String {
+ self.0.to_string()[4..6].to_string()
+ }
+
+ pub fn day(&self) -> String {
+ match self.date_part() {
+ DatePart::DayOfMonth => self.0.to_string()[6..8].to_string(),
+ DatePart::MonthOfYear => String::from("01"),
+ }
+ }
+
+ pub fn from_i32(i: i32) -> Self {
+ ActualbudgetDate(i)
+ }
+
+ fn date_part(&self) -> DatePart {
+ if self.0 < 10000000 {
+ DatePart::MonthOfYear
+ } else {
+ DatePart::DayOfMonth
+ }
+ }
+
+ fn dayified_value(&self) -> i32 {
+ match self.date_part() {
+ DatePart::DayOfMonth => self.0,
+ DatePart::MonthOfYear => self.0 * 100 + 1,
+ }
+ }
+}
+
+impl Ord for ActualbudgetDate {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ self.dayified_value().cmp(&other.dayified_value())
+ }
+}
+
+impl PartialOrd for ActualbudgetDate {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl FromSql<Integer, Sqlite> for ActualbudgetDate {
+ fn from_sql(bytes: <Sqlite as Backend>::RawValue<'_>) -> diesel::deserialize::Result<Self> {
+ let int: i32 = FromSql::<Integer, Sqlite>::from_sql(bytes)?;
+ Ok(ActualbudgetDate(int))
+ }
+}
diff --git a/backend/actualbudget_importer/src/models/actualbudget_transaction.rs b/backend/actualbudget_importer/src/models/actualbudget_transaction.rs
new file mode 100644
index 0000000..be2ca1a
--- /dev/null
+++ b/backend/actualbudget_importer/src/models/actualbudget_transaction.rs
@@ -0,0 +1,18 @@
+use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable};
+
+use crate::models::actualbudget_date::ActualbudgetDate;
+
+#[derive(Identifiable, QueryableByName, Queryable, Selectable, Clone)]
+#[diesel(table_name = crate::schema::transactions)]
+pub struct ActualbudgetTransaction {
+ pub id: String,
+ pub is_parent: bool,
+ pub is_child: bool,
+ pub account_id: String,
+ pub category_id: String,
+ pub amount: i32,
+ pub payee: String,
+ pub notes: String,
+ pub date: ActualbudgetDate,
+ pub parent_id: Option<String>,
+}
diff --git a/backend/actualbudget_importer/src/models/actualbudget_zero_budget.rs b/backend/actualbudget_importer/src/models/actualbudget_zero_budget.rs
new file mode 100644
index 0000000..6478d34
--- /dev/null
+++ b/backend/actualbudget_importer/src/models/actualbudget_zero_budget.rs
@@ -0,0 +1,13 @@
+use crate::models::actualbudget_date::ActualbudgetDate;
+use diesel::{prelude::{Identifiable, Queryable, QueryableByName, Selectable}, sqlite::Sqlite};
+
+#[derive(Identifiable, QueryableByName, Queryable, Selectable, Clone)]
+#[diesel(table_name = crate::schema::zero_budgets)]
+#[diesel(check_for_backend(Sqlite))]
+pub struct ActualbudgetZeroBudget {
+ pub id: String,
+ pub month: ActualbudgetDate,
+ pub category_id: String,
+ pub amount: i32,
+ pub do_carry_over: i32,
+}
diff --git a/backend/actualbudget_importer/src/models/mod.rs b/backend/actualbudget_importer/src/models/mod.rs
new file mode 100644
index 0000000..1c97c65
--- /dev/null
+++ b/backend/actualbudget_importer/src/models/mod.rs
@@ -0,0 +1,5 @@
+pub mod actualbudget_account;
+pub mod actualbudget_category;
+pub mod actualbudget_date;
+pub mod actualbudget_transaction;
+pub mod actualbudget_zero_budget;
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_accounts/get_all.rs b/backend/actualbudget_importer/src/queries/actualbudget_accounts/get_all.rs
new file mode 100644
index 0000000..6f667de
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_accounts/get_all.rs
@@ -0,0 +1,6 @@
+use crate::{models::actualbudget_account::ActualbudgetAccount, schema::accounts::dsl::accounts};
+use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_accounts(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetAccount>, Error> {
+ accounts.select(ActualbudgetAccount::as_select()).load(connection)
+}
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_accounts/get_category_transfers_account.rs b/backend/actualbudget_importer/src/queries/actualbudget_accounts/get_category_transfers_account.rs
new file mode 100644
index 0000000..f05dc78
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_accounts/get_category_transfers_account.rs
@@ -0,0 +1,13 @@
+use diesel::{result::Error, ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+use crate::{models::actualbudget_account::ActualbudgetAccount, schema::accounts::{
+ self as accounts_schema,
+ dsl::accounts as accounts_table,
+}};
+
+pub fn get_category_transfers_account(connection: &mut SqliteConnection) -> Result<ActualbudgetAccount, Error> {
+ accounts_table
+ .filter(accounts_schema::name.eq("Category transfers"))
+ .select(ActualbudgetAccount::as_select())
+ .first(connection)
+}
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_accounts/mod.rs b/backend/actualbudget_importer/src/queries/actualbudget_accounts/mod.rs
new file mode 100644
index 0000000..c6d6015
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_accounts/mod.rs
@@ -0,0 +1,5 @@
+mod get_all;
+mod get_category_transfers_account;
+
+pub use get_all::get_all_actualbudget_accounts;
+pub use get_category_transfers_account::get_category_transfers_account;
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_categories/get_all.rs b/backend/actualbudget_importer/src/queries/actualbudget_categories/get_all.rs
new file mode 100644
index 0000000..4cb7ebc
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_categories/get_all.rs
@@ -0,0 +1,6 @@
+use crate::{models::actualbudget_category::ActualbudgetCategory, 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/backend/actualbudget_importer/src/queries/actualbudget_categories/mod.rs b/backend/actualbudget_importer/src/queries/actualbudget_categories/mod.rs
new file mode 100644
index 0000000..0c04e53
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_categories/mod.rs
@@ -0,0 +1,3 @@
+mod get_all;
+
+pub use get_all::get_all_actualbudget_categories;
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_transactions/get_all.rs b/backend/actualbudget_importer/src/queries/actualbudget_transactions/get_all.rs
new file mode 100644
index 0000000..84c8d08
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_transactions/get_all.rs
@@ -0,0 +1,6 @@
+use crate::{models::actualbudget_transaction::ActualbudgetTransaction, schema::transactions::dsl::transactions};
+use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_transactions(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetTransaction>, Error> {
+ transactions.select(ActualbudgetTransaction::as_select()).load(connection)
+}
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_transactions/get_first_date.rs b/backend/actualbudget_importer/src/queries/actualbudget_transactions/get_first_date.rs
new file mode 100644
index 0000000..b958c37
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_transactions/get_first_date.rs
@@ -0,0 +1,14 @@
+use anyhow::Result;
+use crate::{models::actualbudget_date::ActualbudgetDate, schema::transactions::{
+ self as transactions_schema,
+ dsl::transactions as transactions_table,
+}};
+use diesel::{dsl::min, result::Error, QueryDsl, RunQueryDsl, SqliteConnection};
+
+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/backend/actualbudget_importer/src/queries/actualbudget_transactions/mod.rs b/backend/actualbudget_importer/src/queries/actualbudget_transactions/mod.rs
new file mode 100644
index 0000000..b4ed0dd
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_transactions/mod.rs
@@ -0,0 +1,5 @@
+mod get_all;
+mod get_first_date;
+
+pub use get_all::get_all_actualbudget_transactions;
+pub use get_first_date::get_first_actualbudget_transaction_date;
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/get_all.rs b/backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/get_all.rs
new file mode 100644
index 0000000..0c3f695
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/get_all.rs
@@ -0,0 +1,6 @@
+use crate::{models::actualbudget_zero_budget::ActualbudgetZeroBudget, schema::zero_budgets::dsl::zero_budgets};
+use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_zero_budgets(connection: &mut SqliteConnection) -> Result<Vec<ActualbudgetZeroBudget>, Error> {
+ zero_budgets.select(ActualbudgetZeroBudget::as_select()).load(connection)
+}
diff --git a/backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/mod.rs b/backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/mod.rs
new file mode 100644
index 0000000..ba7e21b
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/actualbudget_zero_budgets/mod.rs
@@ -0,0 +1,3 @@
+mod get_all;
+
+pub use get_all::get_all_actualbudget_zero_budgets;
diff --git a/backend/actualbudget_importer/src/queries/mod.rs b/backend/actualbudget_importer/src/queries/mod.rs
new file mode 100644
index 0000000..e16dafc
--- /dev/null
+++ b/backend/actualbudget_importer/src/queries/mod.rs
@@ -0,0 +1,4 @@
+pub mod actualbudget_accounts;
+pub mod actualbudget_categories;
+pub mod actualbudget_transactions;
+pub mod actualbudget_zero_budgets;
diff --git a/backend/actualbudget_importer/src/schema.rs b/backend/actualbudget_importer/src/schema.rs
new file mode 100644
index 0000000..a872231
--- /dev/null
+++ b/backend/actualbudget_importer/src/schema.rs
@@ -0,0 +1,47 @@
+diesel::table! {
+ accounts (id) {
+ id -> Text,
+ name -> Text,
+ }
+}
+
+diesel::table! {
+ categories (id) {
+ id -> Text,
+ name -> Text,
+ }
+}
+
+diesel::table! {
+ transactions (id) {
+ id -> Text,
+ #[sql_name = "isParent"]
+ is_parent -> Bool,
+ #[sql_name = "isChild"]
+ is_child -> Bool,
+ #[sql_name = "acct"]
+ account_id -> Text,
+ #[sql_name = "category"]
+ category_id -> Text,
+ amount -> Integer,
+ #[sql_name = "description"]
+ payee -> Text,
+ notes -> Text,
+ date -> Integer,
+ parent_id -> Nullable<Text>,
+ }
+}
+
+diesel::table! {
+ zero_budgets (id) {
+ id -> Text,
+ month -> Integer,
+ #[sql_name = "category"]
+ category_id -> Text,
+ amount -> Integer,
+ #[sql_name = "carryover"]
+ do_carry_over -> Integer,
+ // goal -> Integer,
+ // long_goal -> Integer,
+ }
+}
diff --git a/backend/actualbudget_importer/src/transformers.rs b/backend/actualbudget_importer/src/transformers.rs
new file mode 100644
index 0000000..dd4d154
--- /dev/null
+++ b/backend/actualbudget_importer/src/transformers.rs
@@ -0,0 +1,218 @@
+use anyhow::Result;
+use budgeting_app_core::models::{
+ account::Account, budget_update::BudgetUpdate, category::Category, category_transfer::CategoryTransfer, date_utc::DateUtc, datetime_utc::DatetimeUtc, transaction::Transaction, transaction_categorisation::TransactionCategorisation
+};
+use core::panic;
+use std::hash::{DefaultHasher, Hash, Hasher};
+
+use crate::models::{actualbudget_account::ActualbudgetAccount, actualbudget_category::ActualbudgetCategory, actualbudget_date::ActualbudgetDate, actualbudget_transaction::ActualbudgetTransaction, actualbudget_zero_budget::ActualbudgetZeroBudget};
+
+pub fn transform_date(actualbudget_date: &ActualbudgetDate) -> Result<DatetimeUtc> {
+ actualbudget_date.to_iso().parse()
+}
+
+pub fn transform_accounts(
+ actualbudget_accounts: &[ActualbudgetAccount],
+ first_transaction_date: &DatetimeUtc,
+) -> Vec<Account> {
+ let mut accounts = Vec::<Account>::new();
+ for account in actualbudget_accounts {
+ accounts.push(transform_account(account, first_transaction_date));
+ }
+ accounts
+}
+
+fn transform_account(
+ actualbudget_account: &ActualbudgetAccount,
+ opening_date: &DatetimeUtc,
+) -> Account {
+ Account {
+ id: transform_id(&actualbudget_account.id.clone()),
+ name: actualbudget_account.name.clone(),
+ opening_balance: 0,
+ opening_date: opening_date.clone(),
+ }
+}
+
+pub fn transform_categories(
+ actualbudget_categories: &[ActualbudgetCategory],
+) -> Vec<Category> {
+ let mut categories = Vec::<Category>::new();
+ for actualbudget_category in actualbudget_categories {
+ categories.push(Category {
+ id: transform_id(&actualbudget_category.id.clone()),
+ name: actualbudget_category.name.clone(),
+ })
+ }
+ categories
+}
+
+fn transform_id(actualbudget_id: &str) -> i32 {
+ let mut hasher = DefaultHasher::new();
+ actualbudget_id.hash(&mut hasher);
+ (hasher.finish() % std::convert::TryInto::<u64>::try_into(i32::MAX).unwrap())
+ .try_into()
+ .unwrap()
+}
+
+pub fn transform_budget_updates(
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+) -> Result<Vec<BudgetUpdate>> {
+ let mut budget_updates = Vec::<BudgetUpdate>::new();
+ for zero_budget in actualbudget_zero_budgets {
+ let first_zero_budget_in_category = get_first_zero_budget_in_category(
+ actualbudget_zero_budgets,
+ &zero_budget.category_id.clone(),
+ );
+ if first_zero_budget_in_category.is_some_and(|zb| zb.id == zero_budget.id) {
+ continue;
+ }
+ budget_updates.push(BudgetUpdate {
+ id: transform_id(&zero_budget.id.clone()),
+ category_id: transform_id(&zero_budget.category_id.clone()),
+ date: DateUtc::from(transform_date(&zero_budget.month.clone())?),
+ new_budget: zero_budget.amount.try_into().unwrap(),
+ new_period: 30,
+ })
+ }
+ Ok(budget_updates)
+}
+
+fn get_first_zero_budget_in_category(
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+ actualbudget_category_id: &str,
+) -> Option<ActualbudgetZeroBudget> {
+ let mut category_zero_budgets = actualbudget_zero_budgets.to_vec();
+ category_zero_budgets.retain(|zb| zb.category_id == actualbudget_category_id);
+ category_zero_budgets
+ .iter()
+ .min_by_key(|zb| zb.month.clone())
+ .cloned()
+}
+
+pub fn transform_transactions(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+ actualbudget_category_transfers_account: &ActualbudgetAccount,
+) -> Result<(Vec<Transaction>, Vec<TransactionCategorisation>)> {
+ let mut transactions = Vec::<Transaction>::new();
+ let mut transaction_categorisations = Vec::<TransactionCategorisation>::new();
+
+ let mut actualbudget_transactions = actualbudget_transactions.to_vec();
+ actualbudget_transactions
+ .retain(|t| t.category_id.clone() != actualbudget_category_transfers_account.id.clone());
+ let mut parent_actualbudget_transactions = actualbudget_transactions.to_vec();
+ parent_actualbudget_transactions.retain(|t| t.is_parent);
+ let mut child_actualbudget_transactions = actualbudget_transactions.to_vec();
+ child_actualbudget_transactions.retain(|t| t.is_child);
+ let mut atomic_actualbudget_transactions = actualbudget_transactions.to_vec();
+ atomic_actualbudget_transactions.retain(|t| !t.is_parent && !t.is_child);
+
+ for atomic_actualbudget_transaction in atomic_actualbudget_transactions {
+ let transaction_id = transform_id(&atomic_actualbudget_transaction.id);
+
+ transactions.push(Transaction {
+ id: transaction_id,
+ description: atomic_actualbudget_transaction.notes,
+ payee: atomic_actualbudget_transaction.payee,
+ quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(),
+ date: transform_date(&atomic_actualbudget_transaction.date)?,
+ account_id: transform_id(&atomic_actualbudget_transaction.account_id),
+ });
+
+ transaction_categorisations.push(TransactionCategorisation {
+ id: transaction_id,
+ description: String::new(),
+ quantity: atomic_actualbudget_transaction.amount.try_into().unwrap(),
+ transaction_id,
+ category_id: transform_id(&atomic_actualbudget_transaction.category_id),
+ });
+ }
+
+ for parent_actualbudget_transaction in parent_actualbudget_transactions {
+ let mut child_actualbudget_transactions = child_actualbudget_transactions.to_vec();
+ child_actualbudget_transactions.retain(|t| {
+ t.parent_id
+ .clone()
+ .is_some_and(|parent_id| parent_id == parent_actualbudget_transaction.id.clone())
+ });
+
+ let transaction_id = transform_id(&parent_actualbudget_transaction.id);
+
+ transactions.push(Transaction {
+ id: transaction_id,
+ description: parent_actualbudget_transaction.notes,
+ payee: parent_actualbudget_transaction.payee,
+ quantity: parent_actualbudget_transaction.amount.try_into().unwrap(),
+ date: transform_date(&parent_actualbudget_transaction.date)?,
+ account_id: transform_id(&parent_actualbudget_transaction.account_id),
+ });
+
+ for child_actualbudget_transaction in child_actualbudget_transactions {
+ transaction_categorisations.push(TransactionCategorisation {
+ id: transform_id(&child_actualbudget_transaction.id),
+ description: child_actualbudget_transaction.notes,
+ quantity: child_actualbudget_transaction.amount.try_into().unwrap(),
+ transaction_id,
+ category_id: transform_id(&child_actualbudget_transaction.category_id),
+ });
+ }
+ }
+
+ Ok((transactions, transaction_categorisations))
+}
+
+pub fn transform_category_transfers(
+ actualbudget_transactions: &[ActualbudgetTransaction],
+ actualbudget_category_transfers_account: &ActualbudgetAccount,
+) -> Vec<CategoryTransfer> {
+ let mut category_transfers = Vec::<CategoryTransfer>::new();
+
+ let mut actualbudget_category_transfers = actualbudget_transactions.to_vec();
+ actualbudget_category_transfers
+ .retain(|t| t.category_id.clone() == actualbudget_category_transfers_account.id.clone());
+ let mut actualbudget_parent_category_transfers = actualbudget_category_transfers.to_vec();
+ actualbudget_parent_category_transfers.retain(|t| t.is_parent);
+ let mut actualbudget_child_category_transfers = actualbudget_category_transfers.to_vec();
+ actualbudget_child_category_transfers.retain(|t| t.is_child);
+
+ for actualbudget_parent_category_transfer in actualbudget_parent_category_transfers {
+ let mut actualbudget_child_category_transfers =
+ actualbudget_child_category_transfers.to_vec();
+ actualbudget_child_category_transfers.retain(|t| {
+ t.parent_id
+ .clone()
+ .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id)
+ });
+
+ if actualbudget_child_category_transfers.len() != 2 {
+ panic!("Category transfers should always have exactly two children.");
+ }
+
+ let category_1 = actualbudget_child_category_transfers[0].clone();
+ let category_2 = actualbudget_child_category_transfers[1].clone();
+
+ // To avoid duplicates
+ if category_1.amount >= 0 {
+ continue;
+ }
+
+ if actualbudget_child_category_transfers[0].amount
+ != -actualbudget_child_category_transfers[1].amount
+ {
+ panic!("Category transfer children should sum to zero.");
+ }
+
+ category_transfers.push(CategoryTransfer {
+ id: transform_id(&actualbudget_parent_category_transfer.id),
+ description: actualbudget_parent_category_transfer.notes,
+ quantity: actualbudget_child_category_transfers[0]
+ .amount
+ .try_into()
+ .unwrap(),
+ from_category_id: transform_id(&category_1.id),
+ to_category_id: transform_id(&category_2.id),
+ });
+ }
+
+ category_transfers
+}