summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock685
-rw-r--r--Cargo.toml24
-rw-r--r--requirements/kanban.md4
-rw-r--r--requirements/tasks/epic-000.md3
-rw-r--r--requirements/tasks/task-072.md2
-rw-r--r--requirements/tasks/task-087.md8
-rw-r--r--requirements/tasks/task-088.md8
-rw-r--r--requirements/tasks/task-089.md8
-rw-r--r--schist_core/schist_fakes/src/drip.rs8
-rw-r--r--schist_core/schist_fakes/src/pipe.rs3
-rw-r--r--schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql3
-rw-r--r--schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/down.sql39
-rw-r--r--schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/up.sql14
-rw-r--r--schist_core/schist_models/src/bucket.rs13
-rw-r--r--schist_core/schist_models/src/pipe.rs3
-rw-r--r--schist_core/schist_models/src/schema.rs3
-rw-r--r--schist_core/schist_queries/src/clear.rs13
-rw-r--r--schist_core/schist_queries/src/utils/mod.rs4
-rw-r--r--schist_core/schist_queries/tests/category_transfers.rs5
-rw-r--r--schist_desktop_gui/Cargo.toml2
-rw-r--r--schist_desktop_gui/src/connection.rs20
-rw-r--r--schist_desktop_gui/src/database_connection.rs71
-rw-r--r--schist_desktop_gui/src/dialog.rs7
-rw-r--r--schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs26
-rw-r--r--schist_desktop_gui/src/dialog/new_file_dialog.rs15
-rw-r--r--schist_desktop_gui/src/dialog/pick_file_dialog.rs14
-rw-r--r--schist_desktop_gui/src/gui.rs50
-rw-r--r--schist_desktop_gui/src/gui/components.rs2
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs3
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs40
-rw-r--r--schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs11
-rw-r--r--schist_desktop_gui/src/gui/components/buckets_view.rs4
-rw-r--r--schist_desktop_gui/src/gui/components/button.rs21
-rw-r--r--schist_desktop_gui/src/gui/components/text.rs94
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view.rs4
-rw-r--r--schist_desktop_gui/src/gui/screens.rs5
-rw-r--r--schist_desktop_gui/src/gui/screens/files_screen.rs36
-rw-r--r--schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs125
-rw-r--r--schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs106
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs10
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/view.rs4
-rw-r--r--schist_desktop_gui/src/main.rs33
-rw-r--r--schist_desktop_gui/src/paths.rs56
-rw-r--r--schist_desktop_gui/src/style.rs4
-rw-r--r--schist_desktop_gui/src/theme.rs68
-rw-r--r--transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs1
-rw-r--r--transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs13
-rw-r--r--transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs3
-rw-r--r--transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs1
-rw-r--r--transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs1
-rw-r--r--transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs9
-rw-r--r--transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs12
-rw-r--r--transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs11
-rw-r--r--transformers/actualbudget_transformer/actualbudget_models/src/lib.rs3
-rw-r--r--transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs11
-rw-r--r--transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs1
-rw-r--r--transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlitebin528384 -> 573440 bytes
-rw-r--r--transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json10
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml1
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs24
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs (renamed from transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/mod.rs)20
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs46
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs18
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs18
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs182
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs143
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs263
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs49
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs76
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs (renamed from transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs)147
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs6
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs (renamed from transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs)2
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs211
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs8
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs25
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs22
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs (renamed from transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/budget_drip_matchers.rs)45
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs10
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs36
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs112
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlitebin73 -> 573440 bytes
-rw-r--r--transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs26
83 files changed, 2476 insertions, 773 deletions
diff --git a/.gitignore b/.gitignore
index b6f52a1..42b6207 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,4 +3,4 @@ target
*.keep
.zed
.vscode
-**/*.sqlite
+user_data.sqlite
diff --git a/Cargo.lock b/Cargo.lock
index 0334664..65ced99 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -64,6 +64,7 @@ dependencies = [
"diesel_migrations",
"itertools",
"libsqlite3-sys",
+ "schist_fakes",
"schist_models",
"schist_queries",
"schist_traits",
@@ -202,6 +203,28 @@ dependencies = [
]
[[package]]
+name = "ashpd"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
+dependencies = [
+ "async-fs",
+ "async-net",
+ "enumflags2",
+ "futures-channel",
+ "futures-util",
+ "rand 0.9.2",
+ "raw-window-handle",
+ "serde",
+ "serde_repr",
+ "url",
+ "wayland-backend",
+ "wayland-client",
+ "wayland-protocols",
+ "zbus 5.9.0",
+]
+
+[[package]]
name = "async-broadcast"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -280,6 +303,17 @@ dependencies = [
]
[[package]]
+name = "async-net"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
+dependencies = [
+ "async-io",
+ "blocking",
+ "futures-lite",
+]
+
+[[package]]
name = "async-process"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -418,7 +452,16 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
dependencies = [
- "objc2",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "block2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
+dependencies = [
+ "objc2 0.6.2",
]
[[package]]
@@ -563,9 +606,9 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7f4aaa047ba3c3630b080bb9860894732ff23e2aee290a418909aa6d5df38f"
dependencies = [
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -839,7 +882,7 @@ dependencies = [
"rust-ini",
"web-sys",
"winreg",
- "zbus",
+ "zbus 4.4.0",
]
[[package]]
@@ -1031,6 +1074,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
+name = "dispatch2"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.6.1",
+ "libc",
+ "objc2 0.6.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
name = "dlib"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1302,6 +1368,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1830,12 +1905,119 @@ dependencies = [
]
[[package]]
+name = "icu_collections"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
+
+[[package]]
+name = "icu_properties"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "potential_utf",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
+
+[[package]]
+name = "icu_provider"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
name = "indexmap"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2011,6 +2193,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
[[package]]
+name = "litemap"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
+
+[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2184,6 +2372,19 @@ dependencies = [
]
[[package]]
+name = "nix"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "cfg_aliases 0.2.1",
+ "libc",
+ "memoffset",
+]
+
+[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2257,32 +2458,53 @@ dependencies = [
]
[[package]]
+name = "objc2"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc"
+dependencies = [
+ "objc2-encode",
+]
+
+[[package]]
name = "objc2-app-kit"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [
"bitflags 2.9.1",
- "block2",
+ "block2 0.5.1",
"libc",
- "objc2",
+ "objc2 0.5.2",
"objc2-core-data",
"objc2-core-image",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
"objc2-quartz-core",
]
[[package]]
+name = "objc2-app-kit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
+dependencies = [
+ "bitflags 2.9.1",
+ "block2 0.6.1",
+ "objc2 0.6.2",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
name = "objc2-cloud-kit"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [
"bitflags 2.9.1",
- "block2",
- "objc2",
+ "block2 0.5.1",
+ "objc2 0.5.2",
"objc2-core-location",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2291,9 +2513,9 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2303,9 +2525,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.9.1",
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
+[[package]]
+name = "objc2-core-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
+dependencies = [
+ "bitflags 2.9.1",
+ "dispatch2",
+ "objc2 0.6.2",
]
[[package]]
@@ -2314,9 +2547,9 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
"objc2-metal",
]
@@ -2326,10 +2559,10 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [
- "block2",
- "objc2",
+ "block2 0.5.1",
+ "objc2 0.5.2",
"objc2-contacts",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2345,10 +2578,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [
"bitflags 2.9.1",
- "block2",
+ "block2 0.5.1",
"dispatch",
"libc",
- "objc2",
+ "objc2 0.5.2",
+]
+
+[[package]]
+name = "objc2-foundation"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
+dependencies = [
+ "bitflags 2.9.1",
+ "objc2 0.6.2",
+ "objc2-core-foundation",
]
[[package]]
@@ -2357,10 +2601,10 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [
- "block2",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2370,9 +2614,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [
"bitflags 2.9.1",
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2382,9 +2626,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [
"bitflags 2.9.1",
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
"objc2-metal",
]
@@ -2394,8 +2638,8 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc"
dependencies = [
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2405,13 +2649,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [
"bitflags 2.9.1",
- "block2",
- "objc2",
+ "block2 0.5.1",
+ "objc2 0.5.2",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-image",
"objc2-core-location",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
"objc2-link-presentation",
"objc2-quartz-core",
"objc2-symbols",
@@ -2425,9 +2669,9 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [
- "block2",
- "objc2",
- "objc2-foundation",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2437,10 +2681,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [
"bitflags 2.9.1",
- "block2",
- "objc2",
+ "block2 0.5.1",
+ "objc2 0.5.2",
"objc2-core-location",
- "objc2-foundation",
+ "objc2-foundation 0.2.2",
]
[[package]]
@@ -2597,9 +2841,9 @@ checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "percent-encoding"
-version = "2.3.1"
+version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
@@ -2618,7 +2862,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
dependencies = [
"phf_shared",
- "rand",
+ "rand 0.8.5",
]
[[package]]
@@ -2720,6 +2964,21 @@ dependencies = [
]
[[package]]
+name = "pollster"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2795,8 +3054,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
- "rand_chacha",
- "rand_core",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
+dependencies = [
+ "rand_chacha 0.9.0",
+ "rand_core 0.9.3",
]
[[package]]
@@ -2806,7 +3075,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
- "rand_core",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.9.3",
]
[[package]]
@@ -2819,6 +3098,15 @@ dependencies = [
]
[[package]]
+name = "rand_core"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+dependencies = [
+ "getrandom 0.3.3",
+]
+
+[[package]]
name = "range-alloc"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2922,6 +3210,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
+name = "rfd"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed"
+dependencies = [
+ "ashpd",
+ "block2 0.6.1",
+ "dispatch2",
+ "js-sys",
+ "log",
+ "objc2 0.6.2",
+ "objc2-app-kit 0.3.1",
+ "objc2-core-foundation",
+ "objc2-foundation 0.3.1",
+ "pollster",
+ "raw-window-handle",
+ "urlencoding",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
name = "roxmltree"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3017,6 +3329,7 @@ dependencies = [
name = "schist_desktop_gui"
version = "0.1.0"
dependencies = [
+ "actualbudget_to_schist_transformer",
"anyhow",
"diesel",
"diesel_migrations",
@@ -3024,6 +3337,7 @@ dependencies = [
"iced",
"itertools",
"libsqlite3-sys",
+ "rfd",
"schist_fakes",
"schist_models",
"schist_queries",
@@ -3280,8 +3594,8 @@ dependencies = [
"js-sys",
"log",
"memmap2",
- "objc2",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
"objc2-quartz-core",
"raw-window-handle",
"redox_syscall 0.5.17",
@@ -3306,6 +3620,12 @@ dependencies = [
]
[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3363,6 +3683,17 @@ dependencies = [
]
[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
name = "sys-locale"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3504,6 +3835,16 @@ dependencies = [
]
[[package]]
+name = "tinystr"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
name = "tinyvec"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3701,6 +4042,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
+name = "url"
+version = "2.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "137a3c834eaf7139b73688502f3f1141a0337c5d8e4d9b536f9b8c796e26a7c4"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4477,7 +4842,7 @@ dependencies = [
"android-activity",
"atomic-waker",
"bitflags 2.9.1",
- "block2",
+ "block2 0.5.1",
"bytemuck",
"calloop",
"cfg_aliases 0.2.1",
@@ -4490,9 +4855,9 @@ dependencies = [
"libc",
"memmap2",
"ndk",
- "objc2",
- "objc2-app-kit",
- "objc2-foundation",
+ "objc2 0.5.2",
+ "objc2-app-kit 0.2.2",
+ "objc2-foundation 0.2.2",
"objc2-ui-kit",
"orbclient",
"percent-encoding",
@@ -4547,6 +4912,12 @@ dependencies = [
]
[[package]]
+name = "writeable"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
+
+[[package]]
name = "x11-dl"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4626,6 +4997,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1"
[[package]]
+name = "yoke"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "synstructure",
+]
+
+[[package]]
name = "zbus"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4647,9 +5042,9 @@ dependencies = [
"futures-sink",
"futures-util",
"hex",
- "nix",
+ "nix 0.29.0",
"ordered-stream",
- "rand",
+ "rand 0.8.5",
"serde",
"serde_repr",
"sha1",
@@ -4658,9 +5053,42 @@ dependencies = [
"uds_windows",
"windows-sys 0.52.0",
"xdg-home",
- "zbus_macros",
- "zbus_names",
- "zvariant",
+ "zbus_macros 4.4.0",
+ "zbus_names 3.0.0",
+ "zvariant 4.2.0",
+]
+
+[[package]]
+name = "zbus"
+version = "5.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "nix 0.30.1",
+ "ordered-stream",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "windows-sys 0.59.0",
+ "winnow",
+ "zbus_macros 5.9.0",
+ "zbus_names 4.2.0",
+ "zvariant 5.6.0",
]
[[package]]
@@ -4673,7 +5101,22 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
- "zvariant_utils",
+ "zvariant_utils 2.1.0",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "zbus_names 4.2.0",
+ "zvariant 5.6.0",
+ "zvariant_utils 3.2.0",
]
[[package]]
@@ -4684,7 +5127,19 @@ checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c"
dependencies = [
"serde",
"static_assertions",
- "zvariant",
+ "zvariant 4.2.0",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97"
+dependencies = [
+ "serde",
+ "static_assertions",
+ "winnow",
+ "zvariant 5.6.0",
]
[[package]]
@@ -4714,6 +5169,60 @@ dependencies = [
]
[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "synstructure",
+]
+
+[[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
name = "zvariant"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4723,7 +5232,22 @@ dependencies = [
"enumflags2",
"serde",
"static_assertions",
- "zvariant_derive",
+ "zvariant_derive 4.2.0",
+]
+
+[[package]]
+name = "zvariant"
+version = "5.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "url",
+ "winnow",
+ "zvariant_derive 5.6.0",
+ "zvariant_utils 3.2.0",
]
[[package]]
@@ -4736,7 +5260,20 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
- "zvariant_utils",
+ "zvariant_utils 2.1.0",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208"
+dependencies = [
+ "proc-macro-crate",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+ "zvariant_utils 3.2.0",
]
[[package]]
@@ -4749,3 +5286,17 @@ dependencies = [
"quote",
"syn 2.0.104",
]
+
+[[package]]
+name = "zvariant_utils"
+version = "3.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "static_assertions",
+ "syn 2.0.104",
+ "winnow",
+]
diff --git a/Cargo.toml b/Cargo.toml
index f9bb9fc..a763f80 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,20 +1,21 @@
[workspace]
members = [
- "schist_core/schist_fakes",
- "schist_core/schist_models",
- "schist_core/schist_queries",
- "schist_core/schist_schema",
- "schist_core/schist_traits",
- "schist_desktop_gui",
- "transformers/actualbudget_transformer/actualbudget_fakes",
- "transformers/actualbudget_transformer/actualbudget_models",
- "transformers/actualbudget_transformer/actualbudget_queries",
- "transformers/actualbudget_transformer/actualbudget_schema",
- "transformers/actualbudget_transformer/actualbudget_to_schist_transformer",
+ "schist_core/schist_fakes",
+ "schist_core/schist_models",
+ "schist_core/schist_queries",
+ "schist_core/schist_schema",
+ "schist_core/schist_traits",
+ "schist_desktop_gui",
+ "transformers/actualbudget_transformer/actualbudget_fakes",
+ "transformers/actualbudget_transformer/actualbudget_models",
+ "transformers/actualbudget_transformer/actualbudget_queries",
+ "transformers/actualbudget_transformer/actualbudget_schema",
+ "transformers/actualbudget_transformer/actualbudget_to_schist_transformer",
]
resolver = "2"
[workspace.dependencies]
+actualbudget_to_schist_transformer = { path = "transformers/actualbudget_transformer/actualbudget_to_schist_transformer" }
anyhow = { version = "1.0.0", features = ["backtrace"] }
chrono = { version = "0.4.38" }
derive_builder = { version = "0.20.2" }
@@ -25,6 +26,7 @@ dotenvy = "0.15.7"
iced = "0.13.1"
itertools = "0.13.0"
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
+rfd = "0.15.4"
schist_fakes = { path = "schist_core/schist_fakes" }
schist_models = { path = "schist_core/schist_models" }
schist_queries = { path = "schist_core/schist_queries" }
diff --git a/requirements/kanban.md b/requirements/kanban.md
index 52a6d7a..1ceaaef 100644
--- a/requirements/kanban.md
+++ b/requirements/kanban.md
@@ -4,7 +4,8 @@
| -------- | -------- | -------- |
| | | epic-000 |
| ======== | ======== | ======== |
-| | task-072 | |
+| | task-089 | |
+| | task-088 | |
| | task-085 | |
| | task-001 | |
| | task-064 | |
@@ -24,6 +25,7 @@
| | task-081 | |
| | task-077 | |
| | task-079 | |
+| | task-087 | |
| ======== | ======== | ======== |
| epic-008 | | |
diff --git a/requirements/tasks/epic-000.md b/requirements/tasks/epic-000.md
index 9e8d91a..c8be413 100644
--- a/requirements/tasks/epic-000.md
+++ b/requirements/tasks/epic-000.md
@@ -11,6 +11,8 @@ Status: in progress
- [x] task-073 (UI layout & nav)
- [x] task-012 (SQLite)
- [ ] task-072 (import from Actualbudget)
+- [ ] task-089 (keyboard nav in files screen)
+- [ ] task-088 (caching open file)
- [ ] task-085 (view transactions)
- [ ] task-001 (inserting transactions)
- [ ] task-064 (deleting transactions)
@@ -32,3 +34,4 @@ Status: in progress
- [ ] task-082 (Overview in Balances view)
- [ ] task-084 (groups in Balances view)
- [ ] task-083 (hidden buckets in Balances view)
+- [ ] task-087 (closing files)
diff --git a/requirements/tasks/task-072.md b/requirements/tasks/task-072.md
index a5626e0..96a7c20 100644
--- a/requirements/tasks/task-072.md
+++ b/requirements/tasks/task-072.md
@@ -3,4 +3,6 @@
Schist must have a desktop interface for importing transactions from an
Actualbudget SQLite file.
+Status: done
+
Epic: epic-000
diff --git a/requirements/tasks/task-087.md b/requirements/tasks/task-087.md
new file mode 100644
index 0000000..04ee514
--- /dev/null
+++ b/requirements/tasks/task-087.md
@@ -0,0 +1,8 @@
+# task-087
+
+Schist's desktop GUI should enable the user to close their current file and
+return to the files screen.
+
+Source: task-072
+
+Epic: epic-000
diff --git a/requirements/tasks/task-088.md b/requirements/tasks/task-088.md
new file mode 100644
index 0000000..0d0ed59
--- /dev/null
+++ b/requirements/tasks/task-088.md
@@ -0,0 +1,8 @@
+# task-088
+
+Schist's desktop GUI should open the same file the user last had open without
+passing through the files screen.
+
+Source: task-072
+
+Epic: epic-000
diff --git a/requirements/tasks/task-089.md b/requirements/tasks/task-089.md
new file mode 100644
index 0000000..2020eae
--- /dev/null
+++ b/requirements/tasks/task-089.md
@@ -0,0 +1,8 @@
+# task-089
+
+Schist's desktop GUI should have keyboard navigation for operating the files
+screen.
+
+Source: task-072
+
+Epic: epic-000
diff --git a/schist_core/schist_fakes/src/drip.rs b/schist_core/schist_fakes/src/drip.rs
index c2aa7a0..f7d0222 100644
--- a/schist_core/schist_fakes/src/drip.rs
+++ b/schist_core/schist_fakes/src/drip.rs
@@ -7,6 +7,12 @@ pub fn make_fake_drip(id: i32) -> Drip {
pub fn make_fake_drip_builder(id: i32) -> DripBuilder {
let mut builder = DripBuilder::default();
- builder.id(id).amount(0).bucket_id(0).date(DateUtc::now());
+ builder
+ .id(id)
+ .amount(0)
+ .bucket_id(0)
+ .date(DateUtc::now())
+ .inserted_at_unix_seconds(0)
+ .pipe_id(0);
builder
}
diff --git a/schist_core/schist_fakes/src/pipe.rs b/schist_core/schist_fakes/src/pipe.rs
index 765ad78..df77396 100644
--- a/schist_core/schist_fakes/src/pipe.rs
+++ b/schist_core/schist_fakes/src/pipe.rs
@@ -11,7 +11,8 @@ pub fn make_fake_pipe_builder(id: i32) -> PipeBuilder {
.id(id)
.amount(0)
.bucket_id(0)
- .period(BudgetPeriodUnit::Day)
+ .period_unit(BudgetPeriodUnit::Day)
+ .period_length(1)
.start_date(DateUtc::now());
builder
}
diff --git a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql
index 275f280..b37c626 100644
--- a/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql
+++ b/schist_core/schist_models/migrations/2024-08-31-084439_initial_setup/up.sql
@@ -66,7 +66,8 @@ CREATE TABLE pipes(
id INTEGER NOT NULL PRIMARY KEY,
amount INTEGER NOT NULL,
bucket_id INTEGER NOT NULL,
- period TEXT NOT NULL,
+ period_length INTEGER NOT NULL,
+ period_unit TEXT NOT NULL,
start_date TEXT NOT NULL,
FOREIGN KEY (bucket_id)
REFERENCES buckets (id)
diff --git a/schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/down.sql b/schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/down.sql
deleted file mode 100644
index 8050572..0000000
--- a/schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/down.sql
+++ /dev/null
@@ -1,39 +0,0 @@
-DELETE FROM accounts
- WHERE id=0
- AND name="Nationwide bank account"
- AND opening_balance=0
- AND opening_date="2024-01-01";
-
-DELETE FROM accounts
- WHERE id=1
- AND name="cash account"
- AND opening_balance=0
- AND opening_date="2024-01-01";
-
-DELETE FROM accounts
- WHERE id=2
- AND name="YBS savings account"
- AND opening_balance=0
- AND opening_date="2024-01-01";
-
-DELETE FROM buckets
- WHERE id=0
- AND balance IS NULL
- AND balance_cache_key IS NULL
- AND bucket_group="normal expenses"
- AND name="groceries";
-
-DELETE FROM buckets
- WHERE id=1
- AND balance IS NULL
- AND balance_cache_key IS NULL
- AND bucket_group="luxuries"
- AND name="bevvy";
-
-DELETE FROM buckets
- WHERE id=2
- AND balance IS NULL
- AND balance_cache_key IS NULL
- AND bucket_group="income"
- AND name="salary";
-
diff --git a/schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/up.sql b/schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/up.sql
deleted file mode 100644
index f5c97d0..0000000
--- a/schist_core/schist_models/migrations/2025-08-15-090859_dummy_data/up.sql
+++ /dev/null
@@ -1,14 +0,0 @@
-INSERT INTO accounts
- (id, name, opening_balance, opening_date)
- VALUES
- (0, "Nationwide bank account", 0, "2024-01-01"),
- (1, "cash account", 0, "2024-01-01"),
- (2, "YBS savings account", 0, "2024-01-01");
-
-INSERT INTO buckets
- (id, balance, balance_cache_key, bucket_group, name)
- VALUES
- (0, NULL, NULL, "normal expenses", "groceries"),
- (1, NULL, NULL, "luxuries", "bevvy"),
- (2, NULL, NULL, "income", "salary");
-
diff --git a/schist_core/schist_models/src/bucket.rs b/schist_core/schist_models/src/bucket.rs
index 1a78628..a28aa8f 100644
--- a/schist_core/schist_models/src/bucket.rs
+++ b/schist_core/schist_models/src/bucket.rs
@@ -2,6 +2,8 @@ use derive_builder::Builder;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
+use crate::{Drip, Transaction};
+
#[derive(
Builder,
Clone,
@@ -19,7 +21,16 @@ pub struct Bucket {
pub id: i32,
pub balance: Option<i32>,
pub balance_cache_key: Option<String>,
- #[column_name = "bucket_group"]
+ #[diesel(column_name = "bucket_group")]
pub group: String,
pub name: String,
}
+
+impl Bucket {
+ pub fn is_empty(&self, drips: &[Drip], transactions: &[Transaction]) -> bool {
+ drips.iter().all(|drip| drip.bucket_id != self.id)
+ && transactions
+ .iter()
+ .all(|t| t.bucket_id.is_none_or(|id| id != self.id))
+ }
+}
diff --git a/schist_core/schist_models/src/pipe.rs b/schist_core/schist_models/src/pipe.rs
index fb66a20..9354c20 100644
--- a/schist_core/schist_models/src/pipe.rs
+++ b/schist_core/schist_models/src/pipe.rs
@@ -22,6 +22,7 @@ pub struct Pipe {
pub id: i32,
pub amount: i32,
pub bucket_id: i32,
- pub period: BudgetPeriodUnit,
+ pub period_length: i32,
+ pub period_unit: BudgetPeriodUnit,
pub start_date: DateUtc,
}
diff --git a/schist_core/schist_models/src/schema.rs b/schist_core/schist_models/src/schema.rs
index 3799479..d16e636 100644
--- a/schist_core/schist_models/src/schema.rs
+++ b/schist_core/schist_models/src/schema.rs
@@ -56,7 +56,8 @@ diesel::table! {
id -> Integer,
amount -> Integer,
bucket_id -> Integer,
- period -> Text,
+ period_length -> Integer,
+ period_unit -> Text,
start_date -> Text,
}
}
diff --git a/schist_core/schist_queries/src/clear.rs b/schist_core/schist_queries/src/clear.rs
index 04d22d9..a5a82f3 100644
--- a/schist_core/schist_queries/src/clear.rs
+++ b/schist_core/schist_queries/src/clear.rs
@@ -2,17 +2,16 @@ use anyhow::Result;
use diesel::SqliteConnection;
use crate::{
- accounts::delete_all_accounts, bucket_transfers::delete_all_bucket_transfers,
- buckets::delete_all_buckets, drips::delete_all_drips, pipes::delete_all_pipes,
- transactions::delete_all_transactions,
+ account_transfers::delete_all_account_transfers, accounts::delete_all_accounts, bucket_transfers::delete_all_bucket_transfers, buckets::delete_all_buckets, drips::delete_all_drips, pipes::delete_all_pipes, transactions::delete_all_transactions
};
pub fn clear(connection: &mut SqliteConnection) -> Result<()> {
- delete_all_accounts(connection)?;
+ delete_all_account_transfers(connection)?;
delete_all_drips(connection)?;
- delete_all_buckets(connection)?;
- delete_all_bucket_transfers(connection)?;
- delete_all_transactions(connection)?;
delete_all_pipes(connection)?;
+ delete_all_transactions(connection)?;
+ delete_all_accounts(connection)?;
+ delete_all_bucket_transfers(connection)?;
+ delete_all_buckets(connection)?;
Ok(())
}
diff --git a/schist_core/schist_queries/src/utils/mod.rs b/schist_core/schist_queries/src/utils/mod.rs
index 6b47b48..3a81c8c 100644
--- a/schist_core/schist_queries/src/utils/mod.rs
+++ b/schist_core/schist_queries/src/utils/mod.rs
@@ -2,7 +2,5 @@ mod calculate_bucket_balance;
mod find_by_id_or;
mod sum_drips_for_bucket;
+use find_by_id_or::find_by_id_or;
use sum_drips_for_bucket::sum_drips_for_bucket;
-
-pub use calculate_bucket_balance::calculate_bucket_balance;
-pub use find_by_id_or::find_by_id_or;
diff --git a/schist_core/schist_queries/tests/category_transfers.rs b/schist_core/schist_queries/tests/category_transfers.rs
index 13b9866..f7c75a7 100644
--- a/schist_core/schist_queries/tests/category_transfers.rs
+++ b/schist_core/schist_queries/tests/category_transfers.rs
@@ -184,6 +184,7 @@ fn given_bucket_transfers_when_sum_quantity_per_to_bucket_id_then_returns_sum_pe
make_fake_bucket_transfer_builder(2)
.from_bucket_id(1)
.to_bucket_id(0)
+ .amount(4)
.build()
.unwrap(),
];
@@ -196,7 +197,5 @@ fn given_bucket_transfers_when_sum_quantity_per_to_bucket_id_then_returns_sum_pe
assert!(sums.is_ok());
let mut sums = sums.unwrap();
sums.sort_by_key(|sum| sum.0);
- assert_eq!(buckets.len(), sums.len());
- assert_eq!(4, sums[0].1);
- assert_eq!(3, sums[1].1);
+ assert_eq!([(0, 4), (1, 3)], sums.as_slice());
}
diff --git a/schist_desktop_gui/Cargo.toml b/schist_desktop_gui/Cargo.toml
index 35e6521..0ee5764 100644
--- a/schist_desktop_gui/Cargo.toml
+++ b/schist_desktop_gui/Cargo.toml
@@ -4,6 +4,7 @@ version = "0.1.0"
edition = "2021"
[dependencies]
+actualbudget_to_schist_transformer = { workspace = true }
anyhow = { workspace = true }
diesel = { workspace = true }
diesel_migrations = { workspace = true }
@@ -11,6 +12,7 @@ dirs = { workspace = true }
iced = { workspace = true }
itertools = { workspace = true }
libsqlite3-sys = { workspace = true }
+rfd = { workspace = true }
schist_fakes = { workspace = true }
schist_models = { workspace = true }
schist_queries = { workspace = true }
diff --git a/schist_desktop_gui/src/connection.rs b/schist_desktop_gui/src/connection.rs
deleted file mode 100644
index 5a88083..0000000
--- a/schist_desktop_gui/src/connection.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-use std::{fs::create_dir_all, path::Path};
-
-use anyhow::Context;
-use diesel::{Connection, SqliteConnection};
-
-const SCHIST_DATA_DIR: &'static str = "schist";
-const SCHIST_DATABASE_FILENAME: &'static str = "schist.sqlite";
-
-pub fn establish_connection() -> anyhow::Result<SqliteConnection> {
- let data_dir = dirs::data_dir().context("Failed to get local app data directory")?;
- let schist_local_dir = data_dir.join(Path::new(SCHIST_DATA_DIR));
- create_dir_all(&schist_local_dir)
- .context(format!("Failed to create local files directory at {}", schist_local_dir.display()))?;
- let db_path = schist_local_dir.join(Path::new(SCHIST_DATABASE_FILENAME));
- let db_path = db_path
- .to_str()
- .context("Failed to construct database path")?;
- Ok(SqliteConnection::establish(db_path)
- .with_context(|| format!("Failed to connect to database at {}", db_path))?)
-}
diff --git a/schist_desktop_gui/src/database_connection.rs b/schist_desktop_gui/src/database_connection.rs
new file mode 100644
index 0000000..99fbe2d
--- /dev/null
+++ b/schist_desktop_gui/src/database_connection.rs
@@ -0,0 +1,71 @@
+use std::{fmt, path::PathBuf};
+
+use anyhow::Context;
+use diesel::{Connection, SqliteConnection};
+use diesel_migrations::MigrationHarness;
+use schist_models::migrations::MIGRATIONS;
+
+pub struct DatabaseConnection {
+ pub connection: diesel::SqliteConnection,
+ pub file_path: PathBuf,
+}
+
+impl DatabaseConnection {
+ pub fn establish(file_path: PathBuf) -> DatabaseConnectionResult {
+ DatabaseConnectionResult {
+ connection: Self::establish_sqlite_connection(&file_path),
+ file_path: file_path,
+ }
+ }
+
+ fn establish_sqlite_connection(file_path: &PathBuf) -> anyhow::Result<SqliteConnection> {
+ let file_path_str = file_path
+ .to_str()
+ .with_context(|| format!("Failed to get database path from {:?}", file_path))?;
+ let mut connection = diesel::SqliteConnection::establish(&file_path_str)
+ .with_context(|| format!("Failed to connect to database at {}", file_path_str))?;
+ connection
+ .run_pending_migrations(MIGRATIONS)
+ .map_err(anyhow::Error::from_boxed)
+ .with_context(|| {
+ format!(
+ "Failed to run pending migrations on database at {}",
+ file_path_str
+ )
+ })?;
+ Ok(connection)
+ }
+}
+
+impl fmt::Debug for DatabaseConnection {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.debug_struct("SqliteConnection")
+ .field("file_path", &self.file_path)
+ .finish()
+ }
+}
+
+pub struct DatabaseConnectionResult {
+ pub connection: anyhow::Result<SqliteConnection>,
+ pub file_path: PathBuf,
+}
+
+impl DatabaseConnectionResult {
+ pub fn ok(self) -> Result<DatabaseConnection, (PathBuf, anyhow::Error)> {
+ self.try_into()
+ }
+}
+
+impl TryFrom<DatabaseConnectionResult> for DatabaseConnection {
+ type Error = (PathBuf, anyhow::Error);
+
+ fn try_from(result: DatabaseConnectionResult) -> Result<Self, Self::Error> {
+ result
+ .connection
+ .map(|connection| DatabaseConnection {
+ connection,
+ file_path: result.file_path.clone(),
+ })
+ .map_err(|err| (result.file_path, err))
+ }
+}
diff --git a/schist_desktop_gui/src/dialog.rs b/schist_desktop_gui/src/dialog.rs
new file mode 100644
index 0000000..e2704be
--- /dev/null
+++ b/schist_desktop_gui/src/dialog.rs
@@ -0,0 +1,7 @@
+mod import_from_actualbudget_dialog;
+mod new_file_dialog;
+mod pick_file_dialog;
+
+pub use import_from_actualbudget_dialog::import_from_actualbudget_dialog;
+pub use new_file_dialog::new_file_dialog;
+pub use pick_file_dialog::pick_file_dialog;
diff --git a/schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs b/schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs
new file mode 100644
index 0000000..10d4ec6
--- /dev/null
+++ b/schist_desktop_gui/src/dialog/import_from_actualbudget_dialog.rs
@@ -0,0 +1,26 @@
+use actualbudget_to_schist_transformer::{
+ actualbudget_state::read_actualbudget_state, schist_state::SchistState,
+ transform_state::transform_state,
+};
+use anyhow::anyhow;
+use diesel::{Connection, SqliteConnection};
+
+pub fn import_from_actualbudget_dialog(
+) -> Option<Result<SchistState, (std::path::PathBuf, anyhow::Error)>> {
+ let path = rfd::FileDialog::new()
+ .add_filter("SQLite database", &["sqlite"])
+ .set_title("Import Actualbudget database")
+ .pick_file();
+
+ path.as_ref()
+ .map(|path| import_from_actualbudget(path).map_err(|err| (path.clone(), err)))
+}
+
+fn import_from_actualbudget(path: &std::path::PathBuf) -> anyhow::Result<SchistState> {
+ let path = path
+ .to_str()
+ .ok_or_else(|| anyhow!("Failed to stringify path: {:?}", path))?;
+ SqliteConnection::establish(path)
+ .map(|mut conn| read_actualbudget_state(&mut conn))?
+ .map(transform_state)?
+}
diff --git a/schist_desktop_gui/src/dialog/new_file_dialog.rs b/schist_desktop_gui/src/dialog/new_file_dialog.rs
new file mode 100644
index 0000000..b950f68
--- /dev/null
+++ b/schist_desktop_gui/src/dialog/new_file_dialog.rs
@@ -0,0 +1,15 @@
+use crate::{database_connection::DatabaseConnectionResult, DatabaseConnection};
+
+pub fn new_file_dialog<P>(starting_directory: Option<P>) -> Option<DatabaseConnectionResult>
+where
+ P: AsRef<std::path::Path>,
+{
+ let mut file_dialog = rfd::FileDialog::new()
+ .add_filter("SQLite database", &["sqlite"])
+ .set_file_name("schist.sqlite")
+ .set_title("New file");
+ if let Some(starting_directory) = starting_directory {
+ file_dialog = file_dialog.set_directory(starting_directory);
+ }
+ file_dialog.save_file().map(DatabaseConnection::establish)
+}
diff --git a/schist_desktop_gui/src/dialog/pick_file_dialog.rs b/schist_desktop_gui/src/dialog/pick_file_dialog.rs
new file mode 100644
index 0000000..53d36c6
--- /dev/null
+++ b/schist_desktop_gui/src/dialog/pick_file_dialog.rs
@@ -0,0 +1,14 @@
+use crate::{database_connection::DatabaseConnectionResult, DatabaseConnection};
+
+pub fn pick_file_dialog<P>(starting_directory: Option<P>) -> Option<DatabaseConnectionResult>
+where
+ P: AsRef<std::path::Path>,
+{
+ let mut file_dialog = rfd::FileDialog::new()
+ .add_filter("SQLite database", &["sqlite"])
+ .set_title("Open file");
+ if let Some(starting_directory) = starting_directory {
+ file_dialog = file_dialog.set_directory(starting_directory);
+ }
+ file_dialog.pick_file().map(DatabaseConnection::establish)
+}
diff --git a/schist_desktop_gui/src/gui.rs b/schist_desktop_gui/src/gui.rs
index 099ff74..b7405c6 100644
--- a/schist_desktop_gui/src/gui.rs
+++ b/schist_desktop_gui/src/gui.rs
@@ -8,7 +8,8 @@ use schist_queries::{accounts::get_all_accounts, buckets::get_all_buckets};
use crate::{
config::Config,
- gui::screens::{main_screen, MainScreen, Screen},
+ gui::screens::{files_screen, main_screen, FilesScreen, MainScreen, Screen},
+ paths::get_file_paths,
shortcut::KeyBind,
traits::{Component, Viewable},
};
@@ -16,6 +17,7 @@ use crate::{
#[derive(Clone, Debug)]
pub enum Message {
Event(iced::Event),
+ FilesScreenMessage(files_screen::Message),
MainScreenMessage(main_screen::Message),
RefetchedAccounts(FetchResult<Vec<Account>>),
RefetchedBuckets(FetchResult<Vec<Bucket>>),
@@ -38,38 +40,31 @@ impl FetchError {
pub struct Gui {
active_screen: Screen,
- buckets: Vec<Bucket>,
+ connection: Option<SqliteConnection>,
+ files_screen: FilesScreen,
main_screen: MainScreen,
config: Config,
}
impl Gui {
- pub fn new(connection: &mut SqliteConnection) -> (Self, iced::Task<Message>) {
- let buckets = Vec::new();
- let accounts = Vec::new();
- let mut gui = Self {
- active_screen: Screen::Main,
- buckets: buckets.clone(),
- main_screen: MainScreen::new(buckets, accounts),
+ pub fn new() -> (Self, iced::Task<Message>) {
+ let file_paths = get_file_paths();
+ let gui = Self {
+ active_screen: Screen::FilesScreen,
+ connection: None,
+ files_screen: FilesScreen::new(&file_paths),
+ main_screen: MainScreen::new(),
config: Config::default(),
};
-
- let buckets = get_all_buckets(connection).map_err(FetchError::new);
- let accounts = get_all_accounts(connection).map_err(FetchError::new);
-
- let tasks = iced::Task::batch(vec![
- gui.update(Message::RefetchedBuckets(buckets)),
- gui.update(Message::RefetchedAccounts(accounts)),
- ]);
-
- (gui, tasks)
+ (gui, iced::Task::none())
}
}
impl Gui {
pub fn view<'a>(&'a self) -> Element<'a, Message> {
match self.active_screen {
- Screen::Main => self.main_screen.view().map(Message::MainScreenMessage),
+ Screen::FilesScreen => self.files_screen.view().map(Message::FilesScreenMessage),
+ Screen::MainScreen => self.main_screen.view().map(Message::MainScreenMessage),
}
}
@@ -126,7 +121,6 @@ impl Gui {
Message::RefetchedBuckets(buckets) => {
match buckets {
Ok(buckets) => {
- self.buckets = buckets.clone();
self.main_screen
.update(main_screen::Message::SetBuckets(buckets));
}
@@ -144,6 +138,20 @@ impl Gui {
}
iced::Task::none()
}
+ Message::FilesScreenMessage(message) => match self.files_screen.update(message) {
+ files_screen::Action::None => iced::Task::none(),
+ files_screen::Action::OpenedFile(sqlite_connection) => {
+ let mut connection = sqlite_connection.connection;
+ let buckets = get_all_buckets(&mut connection).map_err(FetchError::new);
+ let accounts = get_all_accounts(&mut connection).map_err(FetchError::new);
+ self.connection = Some(connection);
+ self.active_screen = Screen::MainScreen;
+ iced::Task::batch(vec![
+ self.update(Message::RefetchedBuckets(buckets)),
+ self.update(Message::RefetchedAccounts(accounts)),
+ ])
+ }
+ },
}
}
}
diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs
index fc3c838..a242b0b 100644
--- a/schist_desktop_gui/src/gui/components.rs
+++ b/schist_desktop_gui/src/gui/components.rs
@@ -9,7 +9,7 @@ pub mod transactions_view;
pub use balances_view::BalancesView;
pub use bucket_name_and_balance::BucketNameAndBalance;
pub use buckets_view::BucketsView;
-pub use button::{active_button, inactive_button};
+pub use button::{active_button, inactive_button, panel_button};
pub use navigation::Navigation;
pub use text::Text;
pub use transactions_view::TransactionsView;
diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
index e8a1e46..6a64508 100644
--- a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
+++ b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
@@ -1,6 +1,7 @@
use iced::widget::{column, row, Container};
use iced::{alignment, Element, Length};
+use crate::gui::components::Text;
use crate::gui::components::{balances_view::Message, BalancesView};
use crate::style::SPACING_LG;
use crate::traits::Viewable;
@@ -10,7 +11,7 @@ pub fn view_balances_view<'a>(balances_view: &'a BalancesView) -> Element<'a, Me
.navigation
.view()
.map(Message::NavigationMessage);
- let main_content = column![iced::widget::text(balances_view.greeting.clone())];
+ let main_content = column![Text::default(&balances_view.greeting).as_element()];
row![
Container::new(navigation).width(Length::FillPortion(1)),
Container::new(main_content).width(Length::FillPortion(3)),
diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
index 271e76c..18ddf0b 100644
--- a/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
+++ b/schist_desktop_gui/src/gui/components/balances_view/view_group_names.rs
@@ -6,7 +6,7 @@ use crate::gui::components::Text;
pub fn view_group_names(buckets: Vec<Bucket>) -> Vec<Text> {
buckets
.iter()
- .map(|b| b.group.clone())
+ .map(|b| b.group.as_str())
.unique()
.map(Text::new)
.collect()
@@ -96,23 +96,23 @@ mod test {
];
let group_names = view_group_names(buckets);
assert_eq!(17, group_names.len());
- assert_eq!(group_names[0], Text::new(String::from("hydrogen")));
- assert_eq!(group_names[1], Text::new(String::from("helium")));
- assert_eq!(group_names[2], Text::new(String::from("lithium")));
- assert_eq!(group_names[3], Text::new(String::from("beryllium")));
- assert_eq!(group_names[4], Text::new(String::from("boron")));
- assert_eq!(group_names[5], Text::new(String::from("carbon")));
- assert_eq!(group_names[6], Text::new(String::from("nitrogen")));
- assert_eq!(group_names[7], Text::new(String::from("oxygen")));
- assert_eq!(group_names[8], Text::new(String::from("flourine")));
- assert_eq!(group_names[9], Text::new(String::from("neon")));
- assert_eq!(group_names[10], Text::new(String::from("magnesium")));
- assert_eq!(group_names[11], Text::new(String::from("aluminium")));
- assert_eq!(group_names[12], Text::new(String::from("silicon")));
- assert_eq!(group_names[13], Text::new(String::from("phosphorus")));
- assert_eq!(group_names[14], Text::new(String::from("sulphur")));
- assert_eq!(group_names[15], Text::new(String::from("chlorine")));
- assert_eq!(group_names[16], Text::new(String::from("argon")));
+ assert_eq!(group_names[0], Text::new("hydrogen"));
+ assert_eq!(group_names[1], Text::new("helium"));
+ assert_eq!(group_names[2], Text::new("lithium"));
+ assert_eq!(group_names[3], Text::new("beryllium"));
+ assert_eq!(group_names[4], Text::new("boron"));
+ assert_eq!(group_names[5], Text::new("carbon"));
+ assert_eq!(group_names[6], Text::new("nitrogen"));
+ assert_eq!(group_names[7], Text::new("oxygen"));
+ assert_eq!(group_names[8], Text::new("flourine"));
+ assert_eq!(group_names[9], Text::new("neon"));
+ assert_eq!(group_names[10], Text::new("magnesium"));
+ assert_eq!(group_names[11], Text::new("aluminium"));
+ assert_eq!(group_names[12], Text::new("silicon"));
+ assert_eq!(group_names[13], Text::new("phosphorus"));
+ assert_eq!(group_names[14], Text::new("sulphur"));
+ assert_eq!(group_names[15], Text::new("chlorine"));
+ assert_eq!(group_names[16], Text::new("argon"));
}
#[test]
@@ -134,8 +134,8 @@ mod test {
let group_names = view_group_names(buckets);
assert_eq!(
vec![
- Text::new(String::from("hydrogen")),
- Text::new(String::from("helium")),
+ Text::new("hydrogen"),
+ Text::new("helium"),
],
group_names
);
diff --git a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
index db3fc2e..24a3952 100644
--- a/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
+++ b/schist_desktop_gui/src/gui/components/bucket_name_and_balance.rs
@@ -1,10 +1,7 @@
-use iced::{
- widget::{row, text},
- Element,
-};
+use iced::{widget::row, Element};
use schist_models::Bucket;
-use crate::gui::components::navigation;
+use crate::gui::components::{navigation, Text};
#[derive(Clone, Debug, PartialEq)]
pub struct BucketNameAndBalance {
@@ -20,8 +17,8 @@ impl BucketNameAndBalance {
impl<'a> Into<Element<'a, navigation::Message<BucketNameAndBalance>>> for BucketNameAndBalance {
fn into(self) -> Element<'a, navigation::Message<BucketNameAndBalance>> {
row![
- text(self.bucket.name.clone()),
- text(self.bucket.balance.unwrap_or(0))
+ Text::new(&self.bucket.name).as_element(),
+ Text::new(&self.bucket.balance.unwrap_or(0).to_string()).as_element(),
]
.into()
}
diff --git a/schist_desktop_gui/src/gui/components/buckets_view.rs b/schist_desktop_gui/src/gui/components/buckets_view.rs
index bb4dbb4..e513a35 100644
--- a/schist_desktop_gui/src/gui/components/buckets_view.rs
+++ b/schist_desktop_gui/src/gui/components/buckets_view.rs
@@ -6,7 +6,7 @@ use iced::{
use schist_models::Bucket;
use crate::{
- gui::components::{navigation, BucketNameAndBalance, Navigation},
+ gui::components::{navigation, BucketNameAndBalance, Navigation, Text},
style::SPACING_LG,
traits::{Component, Viewable},
};
@@ -53,7 +53,7 @@ impl<'a> Viewable<'a, Message> for BucketsView {
Message: 'a,
{
let navigation = self.navigation.view().map(Message::NavigationMessage);
- let main_content = column![iced::widget::text(self.greeting.clone())];
+ let main_content = column![Text::default(&self.greeting).as_element()];
row![
Container::new(navigation).width(Length::FillPortion(1)),
Container::new(main_content).width(Length::FillPortion(3)),
diff --git a/schist_desktop_gui/src/gui/components/button.rs b/schist_desktop_gui/src/gui/components/button.rs
index bc9e5f9..e071f33 100644
--- a/schist_desktop_gui/src/gui/components/button.rs
+++ b/schist_desktop_gui/src/gui/components/button.rs
@@ -1,5 +1,7 @@
use iced::{widget::Button, Element};
+use crate::style::*;
+
pub fn active_button<'a, Content: Into<Element<'a, Message>>, Message>(
content: Content,
) -> Button<'a, Message> {
@@ -22,3 +24,22 @@ pub fn inactive_button<'a, Content: Into<Element<'a, Message>>, Message>(
.on_press(on_press)
.width(iced::Length::Fill)
}
+
+pub fn panel_button<'a, Content: Into<Element<'a, Message>>, Message>(
+ content: Content,
+ on_press: Message,
+) -> Button<'a, Message> {
+ iced::widget::button(content)
+ .on_press(on_press)
+ .style(|theme: &iced::Theme, _status| iced::widget::button::Style {
+ background: None,
+ border: iced::Border {
+ color: theme.extended_palette().primary.base.text,
+ radius: iced::border::Radius::new(0),
+ width: 2.0,
+ },
+ ..Default::default()
+ })
+ .width(iced::Length::Fill)
+ .padding(iced::Padding::new(SPACING_MD.into()))
+}
diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs
index d334de3..90cd7bc 100644
--- a/schist_desktop_gui/src/gui/components/text.rs
+++ b/schist_desktop_gui/src/gui/components/text.rs
@@ -1,22 +1,100 @@
-use iced::Element;
-
-use crate::gui::components::navigation;
+use crate::style::*;
#[derive(Clone, Debug, PartialEq)]
pub struct Text {
pub content: String,
+ colour: Option<Colour>,
+ size: Option<Size>,
+ strength: Option<Strength>,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum Colour {
+ Danger,
+ Primary,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum Strength {
+ Base,
+ Weak,
+}
+
+#[derive(Clone, Debug, PartialEq)]
+enum Size {
+ Small,
+ Base,
}
impl Text {
- pub fn new(group_name: String) -> Self {
+ pub fn default(content: &str) -> Self {
+ Self {
+ content: String::from(content),
+ colour: Some(Colour::Primary),
+ size: Some(Size::Base),
+ strength: Some(Strength::Base),
+ }
+ }
+
+ pub fn new(content: &str) -> Self {
+ Self {
+ content: String::from(content),
+ colour: None,
+ size: None,
+ strength: None,
+ }
+ }
+
+ pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> {
+ <Self as Into<iced::Element<'a, Message>>>::into(self)
+ }
+
+ pub fn small(&self) -> Self {
+ Self {
+ size: Some(Size::Small),
+ ..self.clone()
+ }
+ }
+
+ pub fn danger(&self) -> Self {
+ Self {
+ colour: Some(Colour::Danger),
+ ..self.clone()
+ }
+ }
+
+ pub fn weak(&self) -> Self {
Self {
- content: group_name.clone(),
+ strength: Some(Strength::Weak),
+ ..self.clone()
}
}
}
-impl<'a> Into<Element<'a, navigation::Message<Text>>> for Text {
- fn into(self) -> Element<'a, navigation::Message<Text>> {
- iced::widget::text(self.content.clone()).into()
+impl<'a, Message> Into<iced::Element<'a, Message>> for Text {
+ fn into(self) -> iced::Element<'a, Message> {
+ iced::widget::text(self.content.clone())
+ .size(match self.size {
+ Some(Size::Small) => TEXT_SIZE_SM,
+ Some(Size::Base) | None => TEXT_SIZE_BASE,
+ })
+ .style(move |theme: &iced::Theme| iced::widget::text::Style {
+ color: match (&self.colour, &self.strength) {
+ (Some(Colour::Danger), Some(Strength::Base) | None) => {
+ Some(theme.extended_palette().danger.base.text)
+ }
+ (Some(Colour::Danger), Some(Strength::Weak)) => {
+ Some(theme.extended_palette().danger.weak.text)
+ }
+ (Some(Colour::Primary), Some(Strength::Base) | None) => {
+ Some(theme.extended_palette().primary.base.text)
+ }
+ (Some(Colour::Primary), Some(Strength::Weak)) => {
+ Some(theme.extended_palette().primary.weak.text)
+ }
+ (None, _) => None,
+ },
+ })
+ .into()
}
}
diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs
index 88532a9..1956d38 100644
--- a/schist_desktop_gui/src/gui/components/transactions_view.rs
+++ b/schist_desktop_gui/src/gui/components/transactions_view.rs
@@ -45,7 +45,7 @@ impl TransactionsView {
impl<'a> Viewable<'a, Message> for TransactionsView {
fn view(&'a self) -> Element<'a, Message> {
let navigation = self.navigation.view().map(Message::NavigationMessage);
- let main_content = column![iced::widget::text(self.greeting.clone())];
+ let main_content = column![Text::default(&self.greeting).as_element()];
row![
Container::new(navigation).width(Length::FillPortion(1)),
Container::new(main_content).width(Length::FillPortion(3)),
@@ -113,6 +113,6 @@ fn make_greeting(account_name: Text) -> String {
fn view_account_names(accounts: Vec<Account>) -> Vec<Text> {
accounts
.iter()
- .map(|acc| Text::new(acc.name.clone()))
+ .map(|acc| Text::new(acc.name.as_str()))
.collect()
}
diff --git a/schist_desktop_gui/src/gui/screens.rs b/schist_desktop_gui/src/gui/screens.rs
index 3ae97dd..abca2b6 100644
--- a/schist_desktop_gui/src/gui/screens.rs
+++ b/schist_desktop_gui/src/gui/screens.rs
@@ -1,9 +1,12 @@
+pub mod files_screen;
pub mod main_screen;
+pub use files_screen::FilesScreen;
pub use main_screen::MainScreen;
#[derive(Default)]
pub enum Screen {
#[default]
- Main,
+ FilesScreen,
+ MainScreen,
}
diff --git a/schist_desktop_gui/src/gui/screens/files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen.rs
new file mode 100644
index 0000000..68a9556
--- /dev/null
+++ b/schist_desktop_gui/src/gui/screens/files_screen.rs
@@ -0,0 +1,36 @@
+mod update_files_screen;
+mod view_files_screen;
+
+use std::{fmt::Debug, path::PathBuf};
+
+use crate::DatabaseConnection;
+
+pub struct FilesScreen {
+ new_file_err: Option<(PathBuf, String)>,
+ file_paths: Vec<PathBuf>,
+ open_file_err: Option<(PathBuf, String)>,
+}
+
+#[derive(Clone, Debug)]
+pub enum Message {
+ ImportFromActualbudget,
+ NewFile,
+ OpenFile(std::path::PathBuf),
+ PickFile,
+}
+
+#[derive(Debug)]
+pub enum Action {
+ None,
+ OpenedFile(DatabaseConnection),
+}
+
+impl<'a> FilesScreen {
+ pub fn new(file_paths: &'a [PathBuf]) -> Self {
+ Self {
+ file_paths: file_paths.to_vec(),
+ new_file_err: None,
+ open_file_err: None,
+ }
+ }
+}
diff --git a/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs
new file mode 100644
index 0000000..9c45d6e
--- /dev/null
+++ b/schist_desktop_gui/src/gui/screens/files_screen/update_files_screen.rs
@@ -0,0 +1,125 @@
+use actualbudget_to_schist_transformer::schist_state::{export_schist_state, SchistState};
+use itertools::Itertools;
+
+use crate::{
+ database_connection::DatabaseConnectionResult,
+ dialog::{import_from_actualbudget_dialog, new_file_dialog, pick_file_dialog},
+ paths::get_schist_database_directory,
+ traits::Component,
+ DatabaseConnection,
+};
+
+use super::{Action, FilesScreen, Message};
+
+impl<'a> Component<'a, Message, Action> for FilesScreen {
+ fn update(&mut self, message: Message) -> Action {
+ match message {
+ Message::NewFile => match self.new_file() {
+ Some(Ok(file)) => Action::OpenedFile(file),
+ Some(Err(_)) | None => Action::None,
+ },
+
+ Message::OpenFile(file_path) => self
+ .open_file(file_path)
+ .map_or(Action::None, Action::OpenedFile),
+
+ Message::PickFile => match self.pick_file() {
+ Some(Ok(file)) => Action::OpenedFile(file),
+ Some(Err(_)) | None => Action::None,
+ },
+
+ Message::ImportFromActualbudget => match self.import_from_actualbudget() {
+ Some(Ok(file)) => Action::OpenedFile(file),
+ Some(Err(_)) | None => Action::None,
+ },
+ }
+ }
+}
+
+impl FilesScreen {
+ fn pick_file(
+ &mut self,
+ ) -> Option<Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)>> {
+ pick_file_dialog(get_schist_database_directory().ok()).map(|db_connection_result| {
+ db_connection_result.ok().inspect_err(|(path, err)| {
+ self.open_file_err = Some((path.clone(), err.to_string()));
+ rfd::MessageDialog::new()
+ .set_title("Failed to import from Actualbudget")
+ .set_level(rfd::MessageLevel::Error)
+ .set_description(format!("{}", err.chain().join("\n | ")))
+ .show();
+ })
+ })
+ }
+
+ fn open_file(
+ &mut self,
+ file_path: std::path::PathBuf,
+ ) -> Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)> {
+ DatabaseConnection::establish(file_path.clone())
+ .ok()
+ .inspect_err(|(_path, err)| {
+ self.open_file_err = Some((file_path, err.to_string()));
+ rfd::MessageDialog::new()
+ .set_title("Failed to import from Actualbudget")
+ .set_level(rfd::MessageLevel::Error)
+ .set_description(format!("{}", err.chain().join("\n | ")))
+ .show();
+ })
+ }
+
+ fn new_file(
+ &mut self,
+ ) -> Option<Result<DatabaseConnection, (std::path::PathBuf, anyhow::Error)>> {
+ new_file_dialog(get_schist_database_directory().ok()).map(|result| {
+ result.ok().inspect_err(|(path, err)| {
+ self.new_file_err = Some((path.clone(), err.to_string()));
+ rfd::MessageDialog::new()
+ .set_title("Failed to import from Actualbudget")
+ .set_level(rfd::MessageLevel::Error)
+ .set_description(format!("{}", err.chain().join("\n | ")))
+ .show();
+ })
+ })
+ }
+
+ fn import_from_actualbudget(&mut self) -> Option<anyhow::Result<DatabaseConnection>> {
+ match import_from_actualbudget_dialog() {
+ Some(Ok(state)) => self.new_file_with_state(&state),
+ Some(Err((path, err))) => Some({
+ self.open_file_err = Some((path, err.to_string()));
+ rfd::MessageDialog::new()
+ .set_title("Failed to import from Actualbudget")
+ .set_level(rfd::MessageLevel::Error)
+ .set_description(format!("{}", err.chain().join("\n | ")))
+ .show();
+ Err(err)
+ }),
+ None => None,
+ }
+ }
+
+ fn new_file_with_state(
+ &mut self,
+ state: &SchistState,
+ ) -> Option<anyhow::Result<DatabaseConnection>> {
+ match new_file_dialog(get_schist_database_directory().ok())
+ .map(DatabaseConnectionResult::ok)
+ {
+ Some(Ok(mut file)) => Some({
+ export_schist_state(state, &mut file.connection).ok()?;
+ Ok(file)
+ }),
+ Some(Err((path, err))) => Some({
+ self.new_file_err = Some((path, err.to_string()));
+ rfd::MessageDialog::new()
+ .set_title("Failed to import from Actualbudget")
+ .set_level(rfd::MessageLevel::Error)
+ .set_description(format!("{}", err.chain().join("\n | ")))
+ .show();
+ Err(err)
+ }),
+ None => None,
+ }
+ }
+}
diff --git a/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs b/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs
new file mode 100644
index 0000000..3cbd939
--- /dev/null
+++ b/schist_desktop_gui/src/gui/screens/files_screen/view_files_screen.rs
@@ -0,0 +1,106 @@
+use std::path::PathBuf;
+
+use iced::widget::column;
+
+use crate::{
+ gui::components::{panel_button, Text},
+ style::*,
+ traits::Viewable,
+};
+
+use super::{FilesScreen, Message};
+
+impl<'a> Viewable<'a, Message> for FilesScreen {
+ fn view(&'a self) -> iced::Element<'a, Message> {
+ let mut cols = Vec::new();
+ if let Some(col) = self.view_new_file_err() {
+ cols.push(col);
+ };
+ if let Some(col) = self.view_open_file_err() {
+ cols.push(col);
+ };
+ cols.push(self.view_new_file_button());
+ cols.push(self.view_open_file_button());
+ cols.push(self.view_import_from_actualbudget_button());
+ cols.append(&mut self.view_files());
+ column(cols).padding(SPACING_LG).spacing(SPACING_MD).into()
+ }
+}
+
+impl<'a> FilesScreen {
+ fn view_new_file_err(&'a self) -> Option<iced::Element<'a, Message>> {
+ self.new_file_err.as_ref().map(|(path, err)| {
+ let msg = format!("Failed to open new file {:#?}: {:#?}", path, err);
+ Text::new(&msg).danger().into()
+ })
+ }
+
+ fn view_open_file_err(&'a self) -> Option<iced::Element<'a, Message>> {
+ self.open_file_err
+ .as_ref()
+ .filter(|(file_path, _err)| !self.file_paths.iter().any(|fp| *fp == *file_path))
+ .map(|(file_path, err)| {
+ let msg = format!("Failed to open new file {:#?}: {:#?}", file_path, err);
+ Text::new(&msg).danger().into()
+ })
+ }
+
+ fn view_files(&'a self) -> Vec<iced::Element<'a, Message>> {
+ self.file_paths
+ .iter()
+ .map(|f| {
+ view_file(
+ f,
+ self.open_file_err
+ .clone()
+ .filter(|(file, _err)| *file == *f)
+ .map(|(_file, err)| err),
+ )
+ })
+ .collect()
+ }
+
+ fn view_new_file_button(&'a self) -> iced::Element<'a, Message> {
+ panel_button::<Text, Message>(Text::default("New file").into(), Message::NewFile).into()
+ }
+
+ fn view_open_file_button(&'a self) -> iced::Element<'a, Message> {
+ panel_button::<Text, Message>(Text::default("Open file").into(), Message::PickFile).into()
+ }
+
+ fn view_import_from_actualbudget_button(&'a self) -> iced::Element<'a, Message> {
+ panel_button::<Text, Message>(
+ Text::default("Import from Actualbudget").into(),
+ Message::ImportFromActualbudget,
+ )
+ .into()
+ }
+}
+
+fn view_file<'a>(file_path: &'a PathBuf, err: Option<String>) -> iced::Element<'a, Message> {
+ let mut cols: Vec<iced::Element<'a, Message>> = err
+ .map(|err| {
+ let msg = format!("Failed to open new file {:#?}: {:#?}", file_path, err);
+ vec![Text::new(&msg).danger().small().into()]
+ })
+ .unwrap_or_else(Vec::new);
+
+ cols.push(
+ Text::default(file_path.to_str().unwrap_or(""))
+ .small()
+ .weak()
+ .into(),
+ );
+
+ cols.push(
+ Text::default(
+ file_path
+ .file_name()
+ .map(|os_str| os_str.to_str().unwrap_or("[invalid Unicode]"))
+ .unwrap_or("[invalid filename]"),
+ )
+ .into(),
+ );
+
+ panel_button(column(cols), Message::OpenFile(file_path.clone())).into()
+}
diff --git a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs
index 0af3c28..00f4ec3 100644
--- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs
+++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs
@@ -147,12 +147,12 @@ impl<'a> Component<'a, Message, Action> for MainScreen {
}
impl MainScreen {
- pub fn new(buckets: Vec<Bucket>, accounts: Vec<Account>) -> Self {
+ pub fn new() -> Self {
Self {
- balances_view: BalancesView::new(buckets.clone()),
- buckets: buckets.clone(),
- buckets_view: BucketsView::new(buckets, "Hello, buckets!"),
- transactions_view: TransactionsView::new(accounts, "Hello, transactions!"),
+ balances_view: BalancesView::new(Vec::new()),
+ buckets: Vec::new(),
+ buckets_view: BucketsView::new(Vec::new(), "Hello, buckets!"),
+ transactions_view: TransactionsView::new(Vec::new(), "Hello, transactions!"),
active_view: View::Balances,
view_navigation: Navigation::new(Some(View::Balances), View::views()),
}
diff --git a/schist_desktop_gui/src/gui/screens/main_screen/view.rs b/schist_desktop_gui/src/gui/screens/main_screen/view.rs
index eb9c88e..2be427b 100644
--- a/schist_desktop_gui/src/gui/screens/main_screen/view.rs
+++ b/schist_desktop_gui/src/gui/screens/main_screen/view.rs
@@ -1,4 +1,4 @@
-use iced::Element;
+use iced::{widget::Text, Element};
use crate::gui::components::navigation;
@@ -43,6 +43,6 @@ impl View {
impl<'a> Into<Element<'a, navigation::Message<View>>> for View {
fn into(self) -> Element<'a, navigation::Message<View>> {
- iced::widget::text(self.name()).into()
+ Text::new(self.name()).into()
}
}
diff --git a/schist_desktop_gui/src/main.rs b/schist_desktop_gui/src/main.rs
index 24aedc0..6dd2622 100644
--- a/schist_desktop_gui/src/main.rs
+++ b/schist_desktop_gui/src/main.rs
@@ -1,6 +1,8 @@
mod config;
-mod connection;
+mod database_connection;
+mod dialog;
mod gui;
+mod paths;
mod settings;
mod shortcut;
mod style;
@@ -8,31 +10,22 @@ mod theme;
mod traits;
mod window_settings;
+pub use database_connection::DatabaseConnection;
+
use anyhow::Context;
-use diesel_migrations::MigrationHarness;
-use schist_schema::migrations::MIGRATIONS;
use crate::{
- config::Config,
- connection::establish_connection,
- gui::Gui,
- settings::make_settings,
- theme::make_theme,
+ config::Config, gui::Gui, settings::make_settings, theme::make_theme,
window_settings::make_window_settings,
};
fn main() -> anyhow::Result<()> {
let config = Config::default();
- let mut connection = establish_connection()
- .context("Failed to establish database connection")?;
- connection.run_pending_migrations(MIGRATIONS)
- .expect("Failed to run pending database migrations");
-
-iced::application("Schist", Gui::update, Gui::view)
- .settings(make_settings(&config.clone()))
- .subscription(gui::subscription)
- .theme(make_theme)
- .window(make_window_settings(&config.clone()))
- .run_with(move || Gui::new(&mut connection))
- .context("Failed to run Schist GUI")
+ iced::application("Schist", Gui::update, Gui::view)
+ .settings(make_settings(&config.clone()))
+ .subscription(gui::subscription)
+ .theme(make_theme)
+ .window(make_window_settings(&config.clone()))
+ .run_with(Gui::new)
+ .context("Failed to run Schist GUI")
}
diff --git a/schist_desktop_gui/src/paths.rs b/schist_desktop_gui/src/paths.rs
new file mode 100644
index 0000000..f5cbf33
--- /dev/null
+++ b/schist_desktop_gui/src/paths.rs
@@ -0,0 +1,56 @@
+use std::{
+ fs::{create_dir_all, read_dir, FileType},
+ path::{Path, PathBuf},
+};
+
+use anyhow::Context;
+
+const SCHIST_DATA_DIR: &'static str = "schist";
+
+pub fn get_file_paths() -> Vec<PathBuf> {
+ get_file_paths_checked()
+ .inspect(|paths| println!("Found file paths: {:?}", paths))
+ .inspect_err(|err| println!("Couldn't find any file paths: {:?}", err))
+ .unwrap_or_else(|_| Vec::new())
+}
+
+fn get_file_paths_checked() -> anyhow::Result<Vec<PathBuf>> {
+ let schist_data_dir = get_and_make_schist_data_dir()?;
+ Ok(read_dir(schist_data_dir)?
+ .filter_map(|entry| match entry {
+ Ok(entry) => {
+ if is_sqlite_file(&entry) {
+ Some(entry.path().to_path_buf())
+ } else {
+ None
+ }
+ }
+ Err(_) => None,
+ })
+ .collect())
+}
+
+fn is_sqlite_file(entry: &std::fs::DirEntry) -> bool {
+ entry
+ .file_type()
+ .is_ok_and(|ft| (FileType::is_file(&ft) || FileType::is_symlink(&ft)))
+ && entry
+ .path()
+ .extension()
+ .is_some_and(|ext| ext.eq_ignore_ascii_case("sqlite"))
+}
+
+pub fn get_schist_database_directory() -> anyhow::Result<std::path::PathBuf> {
+ get_and_make_schist_data_dir()
+}
+
+fn get_and_make_schist_data_dir() -> anyhow::Result<PathBuf> {
+ let data_dir = dirs::data_dir().context("Failed to get app data directory")?;
+ let schist_data_dir = data_dir.join(Path::new(SCHIST_DATA_DIR));
+ create_dir_all(&schist_data_dir).context(format!(
+ "Failed to create app data directory at {}",
+ schist_data_dir.display()
+ ))?;
+ println!("Found app data directory at {:?}", schist_data_dir);
+ Ok(schist_data_dir)
+}
diff --git a/schist_desktop_gui/src/style.rs b/schist_desktop_gui/src/style.rs
index 01ec2ad..c2ecb02 100644
--- a/schist_desktop_gui/src/style.rs
+++ b/schist_desktop_gui/src/style.rs
@@ -1 +1,5 @@
+pub const SPACING_MD: u16 = 24;
pub const SPACING_LG: u16 = 32;
+
+pub const TEXT_SIZE_SM: u16 = 12;
+pub const TEXT_SIZE_BASE: u16 = 16;
diff --git a/schist_desktop_gui/src/theme.rs b/schist_desktop_gui/src/theme.rs
index 61d736b..98df36f 100644
--- a/schist_desktop_gui/src/theme.rs
+++ b/schist_desktop_gui/src/theme.rs
@@ -1,7 +1,71 @@
-use iced::Theme;
+use iced::{
+ theme::{palette, Palette},
+ Color, Theme,
+};
use crate::gui::Gui;
pub fn make_theme(_state: &Gui) -> Theme {
- Theme::GruvboxDark
+ Theme::custom_with_fn(
+ String::from("schist"),
+ Palette {
+ background: black(),
+ text: white(),
+ primary: accent_8(),
+ success: accent_8(),
+ danger: red_4(),
+ },
+ |palette| palette::Extended {
+ background: palette::Background {
+ base: palette::Pair::new(black(), palette.text),
+ weak: palette::Pair::new(grey_2(), palette.text),
+ strong: palette::Pair::new(grey_2(), palette.text),
+ },
+ primary: palette::Primary {
+ base: palette::Pair::new(palette.background, palette.text),
+ weak: palette::Pair::new(palette.background, palette.text),
+ strong: palette::Pair::new(palette.background, palette.text),
+ },
+ secondary: palette::Secondary {
+ base: palette::Pair::new(palette.background, palette.text),
+ weak: palette::Pair::new(palette.background, palette.text),
+ strong: palette::Pair::new(palette.background, palette.text),
+ },
+ success: palette::Success {
+ base: palette::Pair::new(palette.background, accent_8()),
+ weak: palette::Pair::new(palette.background, accent_8()),
+ strong: palette::Pair::new(palette.background, accent_8()),
+ },
+ danger: palette::Danger {
+ base: palette::Pair::new(palette.background, red_7()),
+ weak: palette::Pair::new(palette.background, red_7()),
+ strong: palette::Pair::new(palette.background, red_4()),
+ },
+ is_dark: true,
+ },
+ )
+}
+
+fn black() -> Color {
+ Color::from_rgb8(0, 6, 19)
+}
+
+fn grey_2() -> Color {
+ Color::from_rgb8(11, 31, 47)
+}
+
+fn white() -> Color {
+ Color::from_rgb8(225, 249, 255)
+}
+
+fn accent_8() -> Color {
+ Color::from_rgb8(129, 199, 255)
+}
+
+fn red_4() -> Color {
+ Color::from_rgb8(146, 0, 0)
+}
+
+fn red_7() -> Color {
+ Color::from_rgb8(251, 115, 98)
}
diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs
index 84fb32a..a02e672 100644
--- a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs
+++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category.rs
@@ -8,6 +8,7 @@ pub fn make_fake_actualbudget_category_builder(id: &str) -> ActualbudgetCategory
let mut builder = ActualbudgetCategoryBuilder::default();
builder
.id(String::from(id))
+ .group_id(None)
.name(String::new());
return builder
}
diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs
new file mode 100644
index 0000000..8695e04
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_category_group.rs
@@ -0,0 +1,13 @@
+use actualbudget_models::{ActualbudgetCategoryGroup, ActualbudgetCategoryGroupBuilder};
+
+pub fn make_fake_actualbudget_category_group(id: &str) -> ActualbudgetCategoryGroup {
+ make_fake_actualbudget_category_group_builder(id).build().unwrap()
+}
+
+pub fn make_fake_actualbudget_category_group_builder(id: &str) -> ActualbudgetCategoryGroupBuilder {
+ let mut builder = ActualbudgetCategoryGroupBuilder::default();
+ builder
+ .id(String::from(id))
+ .name(None);
+ return builder
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs
index 548424d..f897444 100644
--- a/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs
+++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/actualbudget_transaction.rs
@@ -17,6 +17,7 @@ pub fn make_fake_actualbudget_transaction_builder(id: &str) -> ActualbudgetTrans
.payee(None)
.notes(None)
.date(ActualbudgetDate::from_i32(19700101))
- .transfer_id(None);
+ .transfer_id(None)
+ .starting_balance_flag(0);
return builder
}
diff --git a/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs b/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs
index 815c962..dbd0849 100644
--- a/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs
+++ b/transformers/actualbudget_transformer/actualbudget_fakes/src/lib.rs
@@ -1,5 +1,6 @@
pub mod actualbudget_account;
pub mod actualbudget_category;
+pub mod actualbudget_category_group;
pub mod actualbudget_payee;
pub mod actualbudget_transaction;
pub mod actualbudget_zero_budget;
diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs
index bf35450..84ad4b0 100644
--- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs
+++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category.rs
@@ -5,5 +5,6 @@ use diesel::{prelude::Queryable, Selectable};
#[diesel(table_name = crate::actualbudget_schema::categories)]
pub struct ActualbudgetCategory {
pub id: String,
+ pub group_id: Option<String>,
pub name: String,
}
diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs
new file mode 100644
index 0000000..8ec21f4
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_category_group.rs
@@ -0,0 +1,9 @@
+use derive_builder::Builder;
+use diesel::prelude::*;
+
+#[derive(Builder, Clone, Queryable, Selectable)]
+#[diesel(table_name = crate::actualbudget_schema::category_groups)]
+pub struct ActualbudgetCategoryGroup {
+ pub id: String,
+ pub name: Option<String>,
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs
index 4e1ba24..3a26344 100644
--- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs
+++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_schema.rs
@@ -8,11 +8,21 @@ diesel::table! {
diesel::table! {
categories (id) {
id -> Text,
+ #[sql_name = "cat_group"]
+ group_id -> Nullable<Text>,
name -> Text,
}
}
diesel::table! {
+ category_groups (id) {
+ id -> Text,
+ name -> Nullable<Text>,
+ }
+}
+
+
+diesel::table! {
v_transactions (id) {
id -> Text,
is_parent -> Bool,
@@ -27,10 +37,10 @@ diesel::table! {
notes -> Nullable<Text>,
date -> Integer,
transfer_id -> Nullable<Text>,
+ starting_balance_flag -> Integer,
// imported_id -> Text,
// error -> Text,
// imported_payee -> Text,
- // starting_balance_flag -> Integer,
// sort_order -> Real,
// cleared -> Integer,
// reconciled -> Integer,
diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs
index c8ac522..d6270da 100644
--- a/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs
+++ b/transformers/actualbudget_transformer/actualbudget_models/src/actualbudget_transaction.rs
@@ -3,7 +3,9 @@ use diesel::prelude::{Identifiable, Queryable, QueryableByName, Selectable};
use crate::actualbudget_date::ActualbudgetDate;
-#[derive(Builder, Debug, PartialEq, Eq, Identifiable, QueryableByName, Queryable, Selectable, Clone)]
+#[derive(
+ Builder, Debug, PartialEq, Eq, Identifiable, QueryableByName, Queryable, Selectable, Clone,
+)]
#[diesel(table_name = crate::actualbudget_schema::v_transactions)]
pub struct ActualbudgetTransaction {
pub id: String,
@@ -17,4 +19,11 @@ pub struct ActualbudgetTransaction {
pub notes: Option<String>,
pub date: ActualbudgetDate,
pub transfer_id: Option<String>,
+ pub starting_balance_flag: i32,
+}
+
+impl ActualbudgetTransaction {
+ pub fn is_starting_balance(&self) -> bool {
+ self.starting_balance_flag != 0
+ }
}
diff --git a/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs b/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs
index 2d7d46c..b46b287 100644
--- a/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs
+++ b/transformers/actualbudget_transformer/actualbudget_models/src/lib.rs
@@ -1,5 +1,6 @@
mod actualbudget_account;
mod actualbudget_category;
+mod actualbudget_category_group;
mod actualbudget_date;
mod actualbudget_payee;
mod actualbudget_transaction;
@@ -11,6 +12,8 @@ pub use actualbudget_account::ActualbudgetAccount;
pub use actualbudget_account::ActualbudgetAccountBuilder;
pub use actualbudget_category::ActualbudgetCategory;
pub use actualbudget_category::ActualbudgetCategoryBuilder;
+pub use actualbudget_category_group::ActualbudgetCategoryGroup;
+pub use actualbudget_category_group::ActualbudgetCategoryGroupBuilder;
pub use actualbudget_date::ActualbudgetDate;
pub use actualbudget_payee::ActualbudgetPayee;
pub use actualbudget_payee::ActualbudgetPayeeBuilder;
diff --git a/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs b/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs
new file mode 100644
index 0000000..8d90765
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_queries/src/actualbudget_category_groups.rs
@@ -0,0 +1,11 @@
+use actualbudget_models::ActualbudgetCategoryGroup;
+use actualbudget_schema::actualbudget_schema::category_groups::dsl::category_groups;
+use diesel::{result::Error, QueryDsl, RunQueryDsl, SelectableHelper, SqliteConnection};
+
+pub fn get_all_actualbudget_category_groups(
+ connection: &mut SqliteConnection
+) -> Result<Vec<ActualbudgetCategoryGroup>, Error> {
+ category_groups
+ .select(ActualbudgetCategoryGroup::as_select())
+ .load(connection)
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs b/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs
index ca3f6ea..0fffcbb 100644
--- a/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs
+++ b/transformers/actualbudget_transformer/actualbudget_queries/src/lib.rs
@@ -1,5 +1,6 @@
pub mod actualbudget_accounts;
pub mod actualbudget_categories;
+pub mod actualbudget_category_groups;
pub mod actualbudget_payees;
pub mod actualbudget_transactions;
pub mod actualbudget_zero_budgets;
diff --git a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite
index 4146bb0..5f89924 100644
--- a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite
+++ b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db.sqlite
Binary files differ
diff --git a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json
index 006b23d..c88313a 100644
--- a/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json
+++ b/transformers/actualbudget_transformer/actualbudget_queries/tests/resources/actualbudget_test_db_metadata.json
@@ -1,7 +1,7 @@
{
- "id": "My-Finances-1-36ec11a",
- "budgetName": "My Finances 1",
- "userId": "fec9ba17-c260-4f55-8d44-676ef0ec5da9",
- "lastScheduleRun": "2024-11-21",
- "resetClock": true
+ "id": "My-Finances-1-36ec11a",
+ "budgetName": "My Finances 1",
+ "userId": "fec9ba17-c260-4f55-8d44-676ef0ec5da9",
+ "lastScheduleRun": "2025-08-16",
+ "resetClock": true
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml
index 51febe1..202d041 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/Cargo.toml
@@ -12,6 +12,7 @@ diesel = { workspace = true, features = ["sqlite"] }
diesel_migrations = { workspace = true }
itertools = { workspace = true }
libsqlite3-sys = { workspace = true, features = ["bundled"] }
+schist_fakes = { workspace = true }
schist_models = { workspace = true }
schist_queries = { workspace = true }
schist_traits = { workspace = true }
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs
index 5379b62..cda63b1 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/actualbudget_state.rs
@@ -1,7 +1,13 @@
use std::cmp::min;
-use actualbudget_models::{ActualbudgetAccount, ActualbudgetCategory, ActualbudgetDate, ActualbudgetPayee, ActualbudgetTransaction, ActualbudgetZeroBudget};
-use actualbudget_queries::{actualbudget_accounts::{get_all_actualbudget_accounts, get_category_transfers_account}, actualbudget_categories::get_all_actualbudget_categories, actualbudget_payees::get_all_actualbudget_payees, actualbudget_transactions::{get_all_actualbudget_transactions, get_first_actualbudget_transaction_date}, actualbudget_zero_budgets::{get_all_actualbudget_zero_budgets, get_first_zero_budget_date}};
+use actualbudget_models::{
+ ActualbudgetAccount, ActualbudgetCategory, ActualbudgetCategoryGroup, ActualbudgetDate, ActualbudgetPayee, ActualbudgetTransaction, ActualbudgetZeroBudget
+};
+use actualbudget_queries::{
+ actualbudget_accounts::get_all_actualbudget_accounts, actualbudget_categories::get_all_actualbudget_categories, actualbudget_category_groups::get_all_actualbudget_category_groups, actualbudget_payees::get_all_actualbudget_payees, actualbudget_transactions::{
+ get_all_actualbudget_transactions, get_first_actualbudget_transaction_date,
+ }, actualbudget_zero_budgets::{get_all_actualbudget_zero_budgets, get_first_zero_budget_date}
+};
use anyhow::Result;
use diesel::SqliteConnection;
@@ -9,23 +15,25 @@ pub struct ActualbudgetState {
pub actualbudget_first_activity_date: ActualbudgetDate,
pub actualbudget_accounts: Vec<ActualbudgetAccount>,
pub actualbudget_categories: Vec<ActualbudgetCategory>,
+ pub actualbudget_category_groups: Vec<ActualbudgetCategoryGroup>,
pub actualbudget_payees: Vec<ActualbudgetPayee>,
pub actualbudget_zero_budgets: Vec<ActualbudgetZeroBudget>,
- pub actualbudget_category_transfers_account: Option<ActualbudgetAccount>,
pub actualbudget_transactions: Vec<ActualbudgetTransaction>,
}
-pub fn read_actualbudget_state(actualbudget_connection: &mut SqliteConnection) -> Result<ActualbudgetState> {
+pub fn read_actualbudget_state(
+ actualbudget_connection: &mut SqliteConnection,
+) -> Result<ActualbudgetState> {
Ok(ActualbudgetState {
actualbudget_first_activity_date: min(
- get_first_actualbudget_transaction_date(actualbudget_connection)?,
- get_first_zero_budget_date(actualbudget_connection)?,
- ),
+ get_first_actualbudget_transaction_date(actualbudget_connection)?,
+ get_first_zero_budget_date(actualbudget_connection)?,
+ ),
actualbudget_accounts: get_all_actualbudget_accounts(actualbudget_connection)?,
actualbudget_categories: get_all_actualbudget_categories(actualbudget_connection)?,
+ actualbudget_category_groups: get_all_actualbudget_category_groups(actualbudget_connection)?,
actualbudget_payees: get_all_actualbudget_payees(actualbudget_connection)?,
actualbudget_zero_budgets: get_all_actualbudget_zero_budgets(actualbudget_connection)?,
- actualbudget_category_transfers_account: get_category_transfers_account(actualbudget_connection)?,
actualbudget_transactions: get_all_actualbudget_transactions(actualbudget_connection)?,
})
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/mod.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs
index 5fadc2c..a116649 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/mod.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state.rs
@@ -1,27 +1,27 @@
mod is_account_transfer;
-mod is_category_transfer;
+mod is_bucket_transfer;
mod transform_account_transfers;
mod transform_accounts;
-mod transform_budget_drips;
-mod transform_categories;
-mod transform_category_transfers;
+mod transform_categories_to_buckets;
mod transform_date;
mod transform_id;
mod transform_state;
mod transform_transactions;
-mod transform_zero_budgets;
+mod transform_transactions_to_bucket_transfers;
+mod transform_zero_budgets_to_drips;
+mod transform_zero_budgets_to_pipes;
use is_account_transfer::is_account_transfer;
use is_account_transfer::is_not_account_transfer;
-use is_category_transfer::is_category_transfer;
-use is_category_transfer::is_not_category_transfer;
+use is_bucket_transfer::is_bucket_transfer;
+use is_bucket_transfer::is_not_bucket_transfer;
use transform_account_transfers::transform_account_transfers;
use transform_accounts::transform_accounts;
-use transform_categories::transform_categories;
-use transform_category_transfers::transform_category_transfers;
+use transform_categories_to_buckets::transform_categories_to_buckets;
use transform_date::transform_date;
use transform_id::transform_id;
use transform_transactions::transform_transactions;
-use transform_zero_budgets::transform_zero_budgets;
+use transform_transactions_to_bucket_transfers::transform_transactions_to_bucket_transfers;
+use transform_zero_budgets_to_pipes::transform_zero_budgets_to_pipes;
pub use transform_state::transform_state;
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs
new file mode 100644
index 0000000..188697e
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_bucket_transfer.rs
@@ -0,0 +1,46 @@
+use actualbudget_models::ActualbudgetTransaction;
+
+pub fn is_bucket_transfer(
+ transaction: &ActualbudgetTransaction,
+ transactions: &[ActualbudgetTransaction],
+) -> bool {
+ is_parent_bucket_transfer(transaction, transactions)
+ || is_child_bucket_transfer(transaction, transactions)
+}
+
+fn is_parent_bucket_transfer(
+ transaction: &ActualbudgetTransaction,
+ transactions: &[ActualbudgetTransaction],
+) -> bool {
+ if !transaction.is_parent {
+ return false;
+ }
+
+ let sum_of_children_amounts: i32 = transactions
+ .iter()
+ .filter(|t| is_parent_and_child(transaction, t))
+ .map(|t| t.amount)
+ .sum();
+ sum_of_children_amounts == 0
+}
+
+fn is_child_bucket_transfer(
+ transaction: &ActualbudgetTransaction,
+ transactions: &[ActualbudgetTransaction],
+) -> bool {
+ transaction.is_child
+ && transactions
+ .iter()
+ .any(|t| is_parent_and_child(t, transaction))
+}
+
+fn is_parent_and_child(parent: &ActualbudgetTransaction, child: &ActualbudgetTransaction) -> bool {
+ parent.is_parent && child.is_child && child.parent_id.clone().is_some_and(|id| id == parent.id)
+}
+
+pub fn is_not_bucket_transfer(
+ transaction: &ActualbudgetTransaction,
+ transactions: &[ActualbudgetTransaction],
+) -> bool {
+ !is_bucket_transfer(transaction, transactions)
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs
deleted file mode 100644
index c82387c..0000000
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/is_category_transfer.rs
+++ /dev/null
@@ -1,18 +0,0 @@
-use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction};
-
-pub fn is_category_transfer(
- transaction: &ActualbudgetTransaction,
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>
-) -> bool {
- match actualbudget_category_transfers_account {
- None => false,
- Some(acc) => transaction.account_id.clone() == acc.id.clone(),
- }
-}
-
-pub fn is_not_category_transfer(
- transaction: &ActualbudgetTransaction,
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>
-) -> bool {
- !is_category_transfer(transaction, actualbudget_category_transfers_account)
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs
index c41b812..a116086 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_account_transfers.rs
@@ -112,7 +112,7 @@ fn transform_account_transfer(
mod test {
use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder;
use actualbudget_models::ActualbudgetDate;
- use schist_models::DateUtc;
+ use schist_models::{AccountTransfer, DateUtc};
use crate::transform_state::transform_id::transform_id;
@@ -147,14 +147,14 @@ mod test {
assert_eq!(0, remaining_transactions.len());
assert_eq!(1, account_transfers.len());
let account_transfer = account_transfers.get(0).unwrap();
- assert_eq!(
- "2024-08-01".parse::<DateUtc>().unwrap(),
- account_transfer.date
- );
- assert_eq!("My notes", account_transfer.description);
- assert_eq!(100, account_transfer.amount);
- assert_eq!(transform_id("0"), account_transfer.from_account_id);
- assert_eq!(transform_id("1"), account_transfer.to_account_id);
+ assert_eq!(AccountTransfer {
+ id: account_transfer.id,
+ amount: 100,
+ date: "2024-08-01".parse().unwrap(),
+ description: String::from("My notes"),
+ from_account_id: transform_id("0"),
+ to_account_id: transform_id("1"),
+ }, *account_transfer);
}
#[test]
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
index c900da7..76eaf0c 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_accounts.rs
@@ -1,32 +1,194 @@
-use actualbudget_models::ActualbudgetAccount;
+use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction};
use schist_models::{Account, DateUtc};
-use super::transform_id;
+use super::{is_bucket_transfer, transform_id};
pub fn transform_accounts(
actualbudget_accounts: &[ActualbudgetAccount],
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
+ actualbudget_transactions: &[ActualbudgetTransaction],
first_activity_date: &DateUtc,
) -> Vec<Account> {
actualbudget_accounts
.iter()
- .filter(|a| {
- actualbudget_category_transfers_account
- .clone()
- .is_some_and(|cta| cta.id != a.id)
- })
- .map(|a| transform_account(a, first_activity_date))
+ .filter(|a| !is_empty_account(a, actualbudget_transactions))
+ .map(|a| transform_account(a, actualbudget_transactions, first_activity_date))
.collect()
}
+fn is_empty_account(
+ actualbudget_account: &ActualbudgetAccount,
+ actualbudget_transactions: &[ActualbudgetTransaction],
+) -> bool {
+ actualbudget_transactions.iter().all(|t| {
+ t.is_starting_balance() && t.amount == 0
+ || t.account_id != actualbudget_account.id
+ || is_bucket_transfer(t, actualbudget_transactions)
+ })
+}
+
fn transform_account(
actualbudget_account: &ActualbudgetAccount,
+ actualbudget_transactions: &[ActualbudgetTransaction],
opening_date: &DateUtc,
) -> Account {
+ let opening_balance = actualbudget_transactions
+ .iter()
+ .filter(|t| t.is_starting_balance())
+ .filter(|t| t.account_id == actualbudget_account.id)
+ .map(|t| t.amount)
+ .sum();
+
Account {
id: transform_id(&actualbudget_account.id.clone()),
name: actualbudget_account.name.clone(),
- opening_balance: 0,
+ opening_balance,
opening_date: opening_date.clone(),
}
}
+
+#[cfg(test)]
+mod test {
+ use actualbudget_fakes::{
+ actualbudget_account::make_fake_actualbudget_account_builder,
+ actualbudget_transaction::make_fake_actualbudget_transaction_builder,
+ };
+ use schist_models::{Account, DateUtc};
+
+ use super::{transform_accounts, transform_id};
+
+ #[test]
+ fn given_starting_balances_then_sets_opening_balances() {
+ let actualbudget_accounts = vec![
+ make_fake_actualbudget_account_builder("0")
+ .name(String::from("Cash account"))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_account_builder("1")
+ .name(String::from("Current account"))
+ .build()
+ .unwrap(),
+ ];
+
+ let actualbudget_transactions = vec![
+ make_fake_actualbudget_transaction_builder("0")
+ .account_id(String::from("0"))
+ .amount(100)
+ .starting_balance_flag(1)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("1")
+ .account_id(String::from("1"))
+ .amount(200)
+ .starting_balance_flag(1)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("2")
+ .account_id(String::from("1"))
+ .amount(50)
+ .starting_balance_flag(1)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("3")
+ .account_id(String::from("0"))
+ .amount(300)
+ .starting_balance_flag(0)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("4")
+ .account_id(String::from("1"))
+ .amount(600)
+ .starting_balance_flag(0)
+ .build()
+ .unwrap(),
+ ];
+ let first_activity_date: DateUtc = "2024-08-01".parse().unwrap();
+
+ let result = transform_accounts(
+ &actualbudget_accounts,
+ &actualbudget_transactions,
+ &first_activity_date,
+ );
+
+ assert_eq!(2, result.len());
+ assert_eq!(
+ Some(Account {
+ id: transform_id("0"),
+ name: String::from("Cash account"),
+ opening_balance: 100,
+ opening_date: "2024-08-01".parse().unwrap(),
+ }),
+ result.iter().find(|a| a.name.eq("Cash account")).cloned()
+ );
+ assert_eq!(
+ Some(Account {
+ id: transform_id("1"),
+ name: String::from("Current account"),
+ opening_balance: 250,
+ opening_date: "2024-08-01".parse().unwrap(),
+ }),
+ result
+ .iter()
+ .find(|a| a.name.eq("Current account"))
+ .cloned()
+ );
+ }
+
+ #[test]
+ fn filters_out_empty_account() {
+ let actualbudget_accounts = vec![
+ make_fake_actualbudget_account_builder("0")
+ .name(String::from("Cash account"))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_account_builder("1")
+ .name(String::from("Current account"))
+ .build()
+ .unwrap(),
+ ];
+
+ let actualbudget_transactions = vec![
+ make_fake_actualbudget_transaction_builder("0")
+ .account_id(String::from("0"))
+ .amount(0)
+ .starting_balance_flag(1)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("1")
+ .account_id(String::from("1"))
+ .amount(200)
+ .starting_balance_flag(1)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("2")
+ .account_id(String::from("1"))
+ .amount(50)
+ .starting_balance_flag(1)
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_transaction_builder("3")
+ .account_id(String::from("1"))
+ .amount(300)
+ .starting_balance_flag(0)
+ .build()
+ .unwrap(),
+ ];
+ let first_activity_date: DateUtc = "2024-08-01".parse().unwrap();
+
+ let result = transform_accounts(
+ &actualbudget_accounts,
+ &actualbudget_transactions,
+ &first_activity_date,
+ );
+
+ assert_eq!(1, result.len());
+ assert_eq!(
+ Some(Account {
+ id: transform_id("1"),
+ name: String::from("Current account"),
+ opening_balance: 250,
+ opening_date: "2024-08-01".parse().unwrap(),
+ }),
+ result.first().cloned(),
+ );
+ }
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs
deleted file mode 100644
index 5bea17c..0000000
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories.rs
+++ /dev/null
@@ -1,143 +0,0 @@
-use actualbudget_models::{ActualbudgetCategory, ActualbudgetZeroBudget};
-use schist_models::Bucket;
-
-use super::transform_id;
-
-pub fn transform_categories(actualbudget_categories: &[ActualbudgetCategory]) -> Vec<Bucket> {
- actualbudget_categories
- .iter()
- .map(|actualbudget_category| Bucket {
- id: transform_id(&actualbudget_category.id.clone()),
- balance: None,
- balance_cache_key: None,
- group: String::from("un-named group"),
- name: actualbudget_category.name.clone(),
- })
- .collect()
-}
-
-fn get_budget_quantity(
- actualbudget_category: &ActualbudgetCategory,
- actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
-) -> i32 {
- let latest_zero_budget_for_category = actualbudget_zero_budgets
- .iter()
- .filter(|zb| zb.category_id == actualbudget_category.id)
- .max_by_key(|zb| zb.month.clone());
-
- match latest_zero_budget_for_category {
- Some(latest_zero_budget_for_category) => latest_zero_budget_for_category.amount,
- None => 0,
- }
-}
-
-#[cfg(test)]
-mod test {
- use actualbudget_fakes::actualbudget_category::make_fake_actualbudget_category_builder;
-
- use crate::transform_state::transform_id::transform_id;
-
- use super::transform_categories;
-
- #[test]
- fn when_one_category_one_zero_budget_then_transforms_category() {
- let actualbudget_categories = [make_fake_actualbudget_category_builder("0")
- .name(String::from("Food"))
- .build()
- .unwrap()];
-
- let buckets = transform_categories(&actualbudget_categories);
-
- assert_eq!(1, buckets.len());
- let bucket = buckets.get(0).unwrap();
- assert_eq!(None, bucket.balance);
- assert_eq!(None, bucket.balance_cache_key);
- assert_eq!("Food", bucket.name);
- }
-
- #[test]
- fn when_one_category_no_zero_budget_then_transforms_category() {
- let actualbudget_categories = [make_fake_actualbudget_category_builder("0")
- .name(String::from("Food"))
- .build()
- .unwrap()];
-
- let buckets = transform_categories(&actualbudget_categories);
-
- assert_eq!(1, buckets.len());
- let bucket = buckets.get(0).unwrap();
- assert_eq!("Food", bucket.name);
- assert_eq!(None, bucket.balance);
- assert_eq!(None, bucket.balance_cache_key);
- assert_eq!("un-named group", bucket.group);
- }
-
- #[test]
- fn when_one_category_multiple_zero_budgets_then_transforms_category() {
- let actualbudget_categories = [make_fake_actualbudget_category_builder("0")
- .name(String::from("Food"))
- .build()
- .unwrap()];
-
- let buckets = transform_categories(&actualbudget_categories);
-
- assert_eq!(1, buckets.len());
- let bucket = buckets.get(0).unwrap();
- assert_eq!("Food", bucket.name);
- assert_eq!(None, bucket.balance);
- assert_eq!(None, bucket.balance_cache_key);
- assert_eq!("un-named group", bucket.group);
- }
-
- #[test]
- fn when_multiple_categories_multiple_zero_budgets_then_transforms_category() {
- let actualbudget_categories = [
- make_fake_actualbudget_category_builder("0")
- .name(String::from("Food"))
- .build()
- .unwrap(),
- make_fake_actualbudget_category_builder("1")
- .name(String::from("Clothes"))
- .build()
- .unwrap(),
- make_fake_actualbudget_category_builder("2")
- .name(String::from("Rent"))
- .build()
- .unwrap(),
- ];
-
- let buckets = transform_categories(&actualbudget_categories);
-
- assert_eq!(3, buckets.len());
-
- let bucket_0 = buckets
- .iter()
- .filter(|c| c.id == transform_id("0"))
- .last()
- .unwrap();
- assert_eq!("Food", bucket_0.name);
- assert_eq!(None, bucket_0.balance);
- assert_eq!(None, bucket_0.balance_cache_key);
- assert_eq!("un-named group", bucket_0.group);
-
- let bucket_1 = buckets
- .iter()
- .filter(|c| c.id == transform_id("1"))
- .last()
- .unwrap();
- assert_eq!("Clothes", bucket_1.name);
- assert_eq!(None, bucket_1.balance);
- assert_eq!(None, bucket_1.balance_cache_key);
- assert_eq!("un-named group", bucket_1.group);
-
- let bucket_2 = buckets
- .iter()
- .filter(|c| c.id == transform_id("2"))
- .last()
- .unwrap();
- assert_eq!("Rent", bucket_2.name);
- assert_eq!(None, bucket_2.balance);
- assert_eq!(None, bucket_2.balance_cache_key);
- assert_eq!("un-named group", bucket_2.group);
- }
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs
new file mode 100644
index 0000000..5653c26
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_categories_to_buckets.rs
@@ -0,0 +1,263 @@
+use actualbudget_models::{ActualbudgetCategory, ActualbudgetCategoryGroup};
+use schist_models::{Bucket, Drip, Transaction};
+
+use super::transform_id;
+
+pub fn transform_categories_to_buckets(
+ actualbudget_categories: &[ActualbudgetCategory],
+ actualbudget_category_groups: &[ActualbudgetCategoryGroup],
+ drips: &[Drip],
+ transactions: &[Transaction],
+) -> Vec<Bucket> {
+ actualbudget_categories
+ .iter()
+ .map(|actualbudget_category| Bucket {
+ id: transform_id(&actualbudget_category.id.clone()),
+ balance: None,
+ balance_cache_key: None,
+ group: actualbudget_category.group_id.clone()
+ .map_or_else(
+ default_group,
+ |group_id| actualbudget_category_groups
+ .iter()
+ .find(|&group| group.id == group_id)
+ .map_or_else(default_group, |group| group.name.clone().unwrap_or_else(default_group)),
+ ),
+ name: actualbudget_category.name.clone(),
+ })
+ .filter(|bucket| !bucket.is_empty(drips, transactions))
+ .collect()
+}
+
+fn default_group() -> String {
+ String::from("un-named group")
+}
+
+#[cfg(test)]
+mod test {
+ use actualbudget_fakes::actualbudget_category::make_fake_actualbudget_category_builder;
+ use actualbudget_models::ActualbudgetCategoryGroup;
+ use schist_fakes::{drip::make_fake_drip_builder, transaction::make_fake_transaction_builder};
+ use schist_models::Bucket;
+
+ use crate::transform_state::transform_id::transform_id;
+
+ use super::transform_categories_to_buckets;
+
+ #[test]
+ fn when_one_category_one_drip_then_transforms_category() {
+ let actualbudget_categories = [make_fake_actualbudget_category_builder("0")
+ .name(String::from("Food"))
+ .build()
+ .unwrap()];
+ let actualbudget_category_groups = vec![];
+ let drips = [make_fake_drip_builder(0)
+ .bucket_id(transform_id("0"))
+ .build()
+ .unwrap()];
+ let transactions = [make_fake_transaction_builder(0)
+ .bucket_id(Some(transform_id("1")))
+ .build()
+ .unwrap()];
+
+ let buckets = transform_categories_to_buckets(
+ &actualbudget_categories,
+ &actualbudget_category_groups,
+ &drips,
+ &transactions);
+
+ assert_eq!(1, buckets.len());
+ let bucket = buckets.get(0).unwrap();
+ assert_eq!(None, bucket.balance);
+ assert_eq!(None, bucket.balance_cache_key);
+ assert_eq!("Food", bucket.name);
+ }
+
+ #[test]
+ fn when_one_category_one_transaction_then_transforms_category() {
+ let actualbudget_categories = [make_fake_actualbudget_category_builder("0")
+ .name(String::from("Food"))
+ .build()
+ .unwrap()];
+ let actualbudget_category_groups = vec![];
+ let drips = [make_fake_drip_builder(0)
+ .bucket_id(transform_id("2"))
+ .build()
+ .unwrap()];
+ let transactions = [make_fake_transaction_builder(0)
+ .bucket_id(Some(transform_id("0")))
+ .build()
+ .unwrap()];
+
+ let buckets = transform_categories_to_buckets(
+ &actualbudget_categories,
+ &actualbudget_category_groups,
+ &drips,
+ &transactions);
+
+ assert_eq!(1, buckets.len());
+ let bucket = buckets.get(0).unwrap();
+ assert_eq!("Food", bucket.name);
+ assert_eq!(None, bucket.balance);
+ assert_eq!(None, bucket.balance_cache_key);
+ assert_eq!("un-named group", bucket.group);
+ }
+
+ #[test]
+ fn when_one_category_no_drip_no_transaction_then_ignores_category() {
+ let actualbudget_categories = [make_fake_actualbudget_category_builder("0")
+ .name(String::from("Food"))
+ .build()
+ .unwrap()];
+ let actualbudget_category_groups = vec![];
+ let drips = [make_fake_drip_builder(0)
+ .bucket_id(transform_id("2"))
+ .build()
+ .unwrap()];
+ let transactions = [make_fake_transaction_builder(0)
+ .bucket_id(Some(transform_id("1")))
+ .build()
+ .unwrap()];
+
+ let buckets = transform_categories_to_buckets(
+ &actualbudget_categories,
+ &actualbudget_category_groups,
+ &drips,
+ &transactions);
+
+ assert_eq!(0, buckets.len());
+ }
+
+ #[test]
+ fn when_multiple_categories_then_transforms_categories() {
+ let actualbudget_categories = [
+ make_fake_actualbudget_category_builder("0")
+ .name(String::from("Food"))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_category_builder("1")
+ .name(String::from("Clothes"))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_category_builder("2")
+ .name(String::from("Rent"))
+ .build()
+ .unwrap(),
+ make_fake_actualbudget_category_builder("3")
+ .name(String::from("Fragrance"))
+ .build()
+ .unwrap(),
+ ];
+ let actualbudget_category_groups = vec![];
+ let drips = [
+ make_fake_drip_builder(0)
+ .bucket_id(transform_id("0"))
+ .build()
+ .unwrap(),
+ make_fake_drip_builder(0)
+ .bucket_id(transform_id("1"))
+ .build()
+ .unwrap(),
+ make_fake_drip_builder(0)
+ .bucket_id(transform_id("1"))
+ .build()
+ .unwrap(),
+ ];
+ let transactions = [
+ make_fake_transaction_builder(0)
+ .bucket_id(Some(transform_id("0")))
+ .build()
+ .unwrap(),
+ make_fake_transaction_builder(0)
+ .bucket_id(Some(transform_id("0")))
+ .build()
+ .unwrap(),
+ make_fake_transaction_builder(0)
+ .bucket_id(Some(transform_id("2")))
+ .build()
+ .unwrap(),
+ ];
+
+ let buckets = transform_categories_to_buckets(
+ &actualbudget_categories,
+ &actualbudget_category_groups,
+ &drips,
+ &transactions);
+
+ assert_eq!(3, buckets.len());
+
+ let bucket_0 = buckets
+ .iter()
+ .filter(|c| c.id == transform_id("0"))
+ .last()
+ .unwrap();
+ assert_eq!("Food", bucket_0.name);
+ assert_eq!(None, bucket_0.balance);
+ assert_eq!(None, bucket_0.balance_cache_key);
+ assert_eq!("un-named group", bucket_0.group);
+
+ let bucket_1 = buckets
+ .iter()
+ .filter(|c| c.id == transform_id("1"))
+ .last()
+ .unwrap();
+ assert_eq!("Clothes", bucket_1.name);
+ assert_eq!(None, bucket_1.balance);
+ assert_eq!(None, bucket_1.balance_cache_key);
+ assert_eq!("un-named group", bucket_1.group);
+
+ let bucket_2 = buckets
+ .iter()
+ .filter(|c| c.id == transform_id("2"))
+ .last()
+ .unwrap();
+ assert_eq!("Rent", bucket_2.name);
+ assert_eq!(None, bucket_2.balance);
+ assert_eq!(None, bucket_2.balance_cache_key);
+ assert_eq!("un-named group", bucket_2.group);
+ }
+
+ #[test]
+ fn when_groups_include_match_then_includes_group_name() {
+ let actualbudget_categories = [make_fake_actualbudget_category_builder("0")
+ .name(String::from("Food"))
+ .group_id(Some(String::from("group-1")))
+ .build()
+ .unwrap()];
+ let actualbudget_category_groups = vec![
+ ActualbudgetCategoryGroup {
+ id: String::from("group-0"),
+ name: Some(String::from("Income")),
+ },
+ ActualbudgetCategoryGroup {
+ id: String::from("group-1"),
+ name: Some(String::from("Essentials")),
+ }
+ ];
+ let drips = [make_fake_drip_builder(0)
+ .bucket_id(transform_id("0"))
+ .build()
+ .unwrap()];
+ let transactions = [make_fake_transaction_builder(0)
+ .bucket_id(Some(transform_id("1")))
+ .build()
+ .unwrap()];
+
+ let buckets = transform_categories_to_buckets(
+ &actualbudget_categories,
+ &actualbudget_category_groups,
+ &drips,
+ &transactions);
+
+ assert_eq!(1, buckets.len());
+ let bucket = buckets.get(0).unwrap();
+ assert_eq!(Bucket {
+ id: transform_id("0"),
+ balance: None,
+ balance_cache_key: None,
+ group: String::from("Essentials"),
+ name: String::from("Food"),
+ }, *bucket);
+
+ }
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
index 7816df6..835ceff 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_state.rs
@@ -1,12 +1,12 @@
-use anyhow::{bail, Context, Result};
-use schist_models::{Account, Bucket, BucketTransfer, DateUtc, Pipe, Transaction};
+use anyhow::{Context, Result};
+use schist_models::{Account, Bucket, BucketTransfer, Pipe, Transaction};
use crate::{actualbudget_state::ActualbudgetState, schist_state::SchistState};
use super::{
- transform_account_transfers, transform_accounts,
- transform_budget_drips::transform_budget_drips, transform_categories,
- transform_category_transfers, transform_date, transform_transactions, transform_zero_budgets,
+ transform_account_transfers, transform_accounts, transform_categories_to_buckets,
+ transform_date, transform_transactions, transform_transactions_to_bucket_transfers,
+ transform_zero_budgets_to_drips::transform_budget_drips, transform_zero_budgets_to_pipes,
};
pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistState> {
@@ -17,25 +17,27 @@ pub fn transform_state(actualbudget_state: ActualbudgetState) -> Result<SchistSt
fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Result<SchistState> {
let (remaining_transactions, account_transfers) =
transform_account_transfers(&actualbudget_state.actualbudget_transactions)?;
+ let transactions: Vec<Transaction> = transform_transactions(
+ &remaining_transactions,
+ &actualbudget_state.actualbudget_payees,
+ )?;
let first_activity_date = transform_date(&actualbudget_state.actualbudget_first_activity_date)?;
- let before_first_activity_date = find_date_before(&first_activity_date)?;
let accounts: Vec<Account> = transform_accounts(
&actualbudget_state.actualbudget_accounts,
- &actualbudget_state.actualbudget_category_transfers_account,
+ &actualbudget_state.actualbudget_transactions,
&first_activity_date,
);
let budget_drips = transform_budget_drips(&actualbudget_state.actualbudget_zero_budgets)?;
- let buckets: Vec<Bucket> = transform_categories(&actualbudget_state.actualbudget_categories);
- let pipes: Vec<Pipe> = transform_zero_budgets(&actualbudget_state.actualbudget_zero_budgets);
- let transactions: Vec<Transaction> = transform_transactions(
- &remaining_transactions,
- &actualbudget_state.actualbudget_category_transfers_account,
- &actualbudget_state.actualbudget_payees,
- )?;
- let bucket_transfers: Vec<BucketTransfer> = transform_category_transfers(
- &remaining_transactions,
- &actualbudget_state.actualbudget_category_transfers_account,
- )?;
+ let buckets: Vec<Bucket> = transform_categories_to_buckets(
+ &actualbudget_state.actualbudget_categories,
+ &actualbudget_state.actualbudget_category_groups,
+ &budget_drips,
+ &transactions,
+ );
+ let pipes: Vec<Pipe> =
+ transform_zero_budgets_to_pipes(&actualbudget_state.actualbudget_zero_budgets)?;
+ let bucket_transfers: Vec<BucketTransfer> =
+ transform_transactions_to_bucket_transfers(&remaining_transactions)?;
Ok(SchistState {
first_transaction_date: first_activity_date,
@@ -48,14 +50,3 @@ fn transform_state_without_context(actualbudget_state: ActualbudgetState) -> Res
bucket_transfers,
})
}
-
-fn find_date_before(date: &DateUtc) -> Result<DateUtc> {
- for days_before in 1..31 {
- let result = date.sub_days(days_before);
- if result.is_ok() {
- return result;
- }
- }
-
- bail!("failed to find a date before {}", date)
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
index 7c4ece5..11765be 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions.rs
@@ -1,32 +1,27 @@
-use actualbudget_models::{ActualbudgetAccount, ActualbudgetPayee, ActualbudgetTransaction};
+use actualbudget_models::{ActualbudgetPayee, ActualbudgetTransaction};
use anyhow::{Context, Result};
use schist_models::Transaction;
-use super::{is_not_category_transfer, transform_date, transform_id};
+use super::{is_not_bucket_transfer, transform_date, transform_id};
pub fn transform_transactions(
actualbudget_transactions: &[ActualbudgetTransaction],
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
actualbudget_payees: &[ActualbudgetPayee],
) -> Result<Vec<Transaction>> {
- transform_transactions_without_context(
- actualbudget_transactions,
- actualbudget_category_transfers_account,
- actualbudget_payees,
- )
- .with_context(|| "failed to transform actualbudget transactions to schist transactions")
+ transform_transactions_without_context(actualbudget_transactions, actualbudget_payees)
+ .with_context(|| "failed to transform actualbudget transactions to schist transactions")
}
fn transform_transactions_without_context(
actualbudget_transactions: &[ActualbudgetTransaction],
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
actualbudget_payees: &[ActualbudgetPayee],
) -> Result<Vec<Transaction>> {
let mut transactions = Vec::<Transaction>::new();
let actualbudget_transactions = actualbudget_transactions
.iter()
- .filter(|t| is_not_category_transfer(t, actualbudget_category_transfers_account));
+ .filter(|t| !t.is_starting_balance())
+ .filter(|t| is_not_bucket_transfer(t, actualbudget_transactions));
let parent_actualbudget_transactions: Vec<&ActualbudgetTransaction> = actualbudget_transactions
.clone()
.filter(|t| t.is_parent)
@@ -60,7 +55,10 @@ fn transform_transactions_without_context(
amount: atomic_actualbudget_transaction.amount.try_into().unwrap(),
date: transform_date(&atomic_actualbudget_transaction.date)?,
account_id: transform_id(&atomic_actualbudget_transaction.account_id),
- bucket_id: None,
+ bucket_id: atomic_actualbudget_transaction
+ .category_id
+ .clone()
+ .map(|id| transform_id(id.as_str())),
});
}
@@ -90,37 +88,17 @@ fn transform_transactions_without_context(
#[cfg(test)]
mod test {
use actualbudget_fakes::{
- actualbudget_account::make_fake_actualbudget_account,
- actualbudget_payee::{make_fake_actualbudget_payee, make_fake_actualbudget_payee_builder},
+ actualbudget_payee::make_fake_actualbudget_payee_builder,
actualbudget_transaction::make_fake_actualbudget_transaction_builder,
};
use actualbudget_models::ActualbudgetDate;
- use schist_models::DateUtc;
+ use schist_models::{DateUtc, Transaction};
use crate::transform_state::transform_id::transform_id;
use super::transform_transactions;
#[test]
- fn when_one_category_transfer_transaction_then_returns_empty() {
- let actualbudget_transactions = [make_fake_actualbudget_transaction_builder("0")
- .category_id(Some(String::from("0")))
- .build()
- .unwrap()];
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
- let actualbudget_payees = [make_fake_actualbudget_payee("0")];
-
- let transactions = transform_transactions(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- &actualbudget_payees,
- )
- .unwrap();
-
- assert_eq!(0, transactions.len());
- }
-
- #[test]
fn when_one_atomic_transaction_then_returns_transformed_transaction() {
let actualbudget_transactions = [make_fake_actualbudget_transaction_builder("0")
.account_id(String::from("0"))
@@ -137,8 +115,7 @@ mod test {
.unwrap()];
let transactions =
- transform_transactions(&actualbudget_transactions, &None, &actualbudget_payees)
- .unwrap();
+ transform_transactions(&actualbudget_transactions, &actualbudget_payees).unwrap();
assert_eq!(1, transactions.len());
let transaction = transactions.get(0).unwrap();
@@ -153,11 +130,14 @@ mod test {
fn when_one_split_transaction_then_returns_transformed_transaction() {
let actualbudget_transactions = [
make_fake_actualbudget_transaction_builder("0")
+ .account_id(String::from("0"))
+ .date(ActualbudgetDate::from_i32(20240801))
.is_parent(true)
- .amount(100)
+ .amount(300)
.build()
.unwrap(),
make_fake_actualbudget_transaction_builder("1")
+ .account_id(String::from("0"))
.amount(200)
.category_id(Some(String::from("0")))
.is_child(true)
@@ -166,8 +146,9 @@ mod test {
.build()
.unwrap(),
make_fake_actualbudget_transaction_builder("2")
- .amount(300)
- .category_id(Some(String::from("0")))
+ .account_id(String::from("0"))
+ .amount(100)
+ .category_id(Some(String::from("1")))
.is_child(true)
.parent_id(Some(String::from("0")))
.notes(Some(String::from("notes B")))
@@ -175,11 +156,20 @@ mod test {
.unwrap(),
];
- let transactions = transform_transactions(&actualbudget_transactions, &None, &[]).unwrap();
+ let transactions = transform_transactions(&actualbudget_transactions, &[]).unwrap();
assert_eq!(1, transactions.len());
-
- let transaction = transactions.get(0).unwrap();
- assert_eq!(100, transaction.amount);
+ assert_eq!(
+ Some(Transaction {
+ id: transform_id("0"),
+ account_id: transform_id("0"),
+ amount: 300,
+ bucket_id: None,
+ counterparty: String::from(""),
+ date: "2024-08-01".parse().unwrap(),
+ description: String::from(""),
+ }),
+ transactions.get(0).cloned(),
+ );
}
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs
index 78b466d..51545c3 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_category_transfers.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_transactions_to_bucket_transfers.rs
@@ -1,32 +1,52 @@
-use actualbudget_models::{ActualbudgetAccount, ActualbudgetTransaction};
+use actualbudget_models::ActualbudgetTransaction;
use anyhow::{bail, Context, Result};
use itertools::Itertools;
use schist_models::BucketTransfer;
-use super::{is_category_transfer, transform_id};
+use super::transform_id;
-pub fn transform_category_transfers(
+pub fn transform_transactions_to_bucket_transfers(
actualbudget_transactions: &[ActualbudgetTransaction],
- actualbudget_category_transfers_account: &Option<ActualbudgetAccount>,
) -> Result<Vec<BucketTransfer>> {
- let actualbudget_category_transfers = actualbudget_transactions.iter().filter(|transaction| {
- is_category_transfer(transaction, actualbudget_category_transfers_account)
- });
- let actualbudget_parent_category_transfers: Vec<ActualbudgetTransaction> =
- actualbudget_category_transfers
- .clone()
+ let children: Vec<&ActualbudgetTransaction> = actualbudget_transactions
+ .into_iter()
+ .filter(|&t| t.is_child)
+ .collect();
+ let parent_children_pairs: Vec<(&ActualbudgetTransaction, Vec<&ActualbudgetTransaction>)> =
+ actualbudget_transactions
+ .iter()
.filter(|t| t.is_parent)
- .map(ActualbudgetTransaction::clone)
- .collect();
- let actualbudget_child_category_transfers: Vec<ActualbudgetTransaction> =
- actualbudget_category_transfers
- .filter(|t| t.is_child)
- .map(ActualbudgetTransaction::clone)
- .collect();
-
- Ok(actualbudget_parent_category_transfers
+ .map(|parent| {
+ (
+ parent,
+ children
+ .clone()
+ .into_iter()
+ .filter(|&c| c.parent_id.clone().is_some_and(|id| id == parent.id))
+ .collect::<Vec<&ActualbudgetTransaction>>(),
+ )
+ })
+ .map(|(parent, children)| {
+ let children_total_amount: i32 = children.iter().map(|child| child.amount).sum();
+ if parent.amount != children_total_amount {
+ bail!("parent transaction {} has amount: {}, but its {} children add up to a total amount of: {}",
+ parent.id,
+ parent.amount,
+ children.len(),
+ children_total_amount);
+ }
+ Ok((parent, children))
+ })
+ .filter(|result|
+ match result {
+ Ok((parent, _children)) => parent.amount == 0,
+ Err(_) => true,
+ })
+ .try_collect()?;
+
+ Ok(parent_children_pairs
.iter()
- .map(|pct| transform_category_transfer(pct, &actualbudget_child_category_transfers))
+ .map(|(parent, children)| transform_category_transfer(parent, children))
.filter_ok(Option::is_some)
.map_ok(Option::unwrap)
.collect::<Result<Vec<BucketTransfer>>>()
@@ -35,7 +55,7 @@ pub fn transform_category_transfers(
fn transform_category_transfer(
actualbudget_parent_category_transfer: &ActualbudgetTransaction,
- actualbudget_child_category_transfers: &[ActualbudgetTransaction],
+ actualbudget_child_category_transfers: &[&ActualbudgetTransaction],
) -> Result<Option<BucketTransfer>> {
transform_category_transfer_without_context(
actualbudget_parent_category_transfer,
@@ -46,20 +66,12 @@ fn transform_category_transfer(
fn transform_category_transfer_without_context(
actualbudget_parent_category_transfer: &ActualbudgetTransaction,
- actualbudget_child_category_transfers: &[ActualbudgetTransaction],
+ actualbudget_child_category_transfers: &[&ActualbudgetTransaction],
) -> Result<Option<BucketTransfer>> {
- let actualbudget_child_category_transfers = actualbudget_child_category_transfers
- .iter()
- .filter(|t| {
- t.parent_id
- .clone()
- .is_some_and(|parent_id| parent_id == actualbudget_parent_category_transfer.id)
- })
- .map(ActualbudgetTransaction::clone);
-
- let from_transfers: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers
- .clone()
- .filter(|t| t.amount < 0)
+ let from_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers
+ .into_iter()
+ .cloned()
+ .filter(|&t| t.amount < 0)
.collect();
if from_transfers.len() != 1 {
bail!(
@@ -78,9 +90,10 @@ fn transform_category_transfer_without_context(
}
let from_category_id: i32 = transform_id(&from_transfer.category_id.clone().unwrap());
- let to_transfers: Vec<ActualbudgetTransaction> = actualbudget_child_category_transfers
- .clone()
- .filter(|t| t.amount > 0)
+ let to_transfers: Vec<&ActualbudgetTransaction> = actualbudget_child_category_transfers
+ .iter()
+ .filter(|&t| t.amount > 0)
+ .cloned()
.collect();
if to_transfers.len() != 1 {
bail!(
@@ -117,30 +130,23 @@ fn transform_category_transfer_without_context(
#[cfg(test)]
mod test {
- use actualbudget_fakes::{
- actualbudget_account::make_fake_actualbudget_account,
- actualbudget_transaction::make_fake_actualbudget_transaction_builder,
- };
+ use actualbudget_fakes::actualbudget_transaction::make_fake_actualbudget_transaction_builder;
use actualbudget_models::{ActualbudgetTransaction, ActualbudgetTransactionBuilder};
use schist_models::BucketTransfer;
use crate::transform_state::transform_id::transform_id;
- use super::transform_category_transfers;
+ use super::transform_transactions_to_bucket_transfers;
#[test]
fn when_one_category_transfer_then_transforms() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions = [
make_fake_parent_transaction("0"),
make_fake_from_child_transaction("1", "0"),
make_fake_to_child_transaction("2", "0"),
];
- let result = transform_category_transfers(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- );
+ let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
assert!(result.is_ok());
let bucket_transfers = result.unwrap();
@@ -154,7 +160,6 @@ mod test {
#[test]
fn when_many_transactions_then_transforms() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions = [
make_fake_parent_transaction("0"),
make_fake_from_child_transaction_builder("1", 100, "0")
@@ -184,10 +189,7 @@ mod test {
.unwrap(),
];
- let result = transform_category_transfers(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- );
+ let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
assert!(result.is_ok());
let bucket_transfers = result.unwrap();
@@ -214,7 +216,6 @@ mod test {
#[test]
fn when_multiple_child_from_transactions_then_err() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions = [
make_fake_parent_transaction("0"),
make_fake_from_child_transaction_builder("1", 50, "0")
@@ -228,17 +229,13 @@ mod test {
.unwrap(),
];
- let result = transform_category_transfers(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- );
+ let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
assert!(result.is_err());
}
#[test]
fn when_multiple_child_to_transactions_then_err() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions = [
make_fake_parent_transaction("0"),
make_fake_from_child_transaction_builder("1", 100, "0")
@@ -252,49 +249,37 @@ mod test {
.unwrap(),
];
- let result = transform_category_transfers(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- );
+ let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
assert!(result.is_err());
}
#[test]
fn when_no_child_from_transactions_then_err() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions = [
make_fake_parent_transaction("0"),
make_fake_to_child_transaction("1", "0"),
];
- let result = transform_category_transfers(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- );
+ let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
assert!(result.is_err());
}
#[test]
fn when_no_child_to_transactions_then_err() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions = [
make_fake_parent_transaction("0"),
make_fake_from_child_transaction("1", "0"),
];
- let result = transform_category_transfers(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- );
+ let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
assert!(result.is_err());
}
#[test]
fn when_child_amounts_dont_add_up_to_zero_then_err() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions = [
make_fake_parent_transaction("0"),
make_fake_from_child_transaction_builder("1", 50, "0")
@@ -305,17 +290,13 @@ mod test {
.unwrap(),
];
- let result = transform_category_transfers(
- &actualbudget_transactions,
- &Some(actualbudget_category_transfers_account),
- );
+ let result = transform_transactions_to_bucket_transfers(&actualbudget_transactions);
assert!(result.is_err());
}
#[test]
fn when_child_category_transfer_has_no_category_then_err() {
- let actualbudget_category_transfers_account = make_fake_actualbudget_account("0");
let actualbudget_transactions_0 = [
make_fake_parent_transaction("0"),
make_fake_from_child_transaction_builder("1", 100, "0")
@@ -337,14 +318,8 @@ mod test {
.unwrap(),
];
- let result_0 = transform_category_transfers(
- &actualbudget_transactions_0,
- &Some(actualbudget_category_transfers_account.clone()),
- );
- let result_1 = transform_category_transfers(
- &actualbudget_transactions_1,
- &Some(actualbudget_category_transfers_account),
- );
+ let result_0 = transform_transactions_to_bucket_transfers(&actualbudget_transactions_0);
+ let result_1 = transform_transactions_to_bucket_transfers(&actualbudget_transactions_1);
assert!(result_0.is_err());
assert!(result_1.is_err());
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs
deleted file mode 100644
index 999c3e8..0000000
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-use actualbudget_models::ActualbudgetZeroBudget;
-use schist_models::Pipe;
-
-pub fn transform_zero_budgets(_actualbudget_zero_budgets: &[ActualbudgetZeroBudget]) -> Vec<Pipe> {
- todo!()
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs
index 76509cd..bfaef31 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_budget_drips.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_drips.rs
@@ -141,7 +141,7 @@ mod test {
use schist_models::Drip;
use schist_traits::Dateable;
- use crate::transform_state::transform_budget_drips::map_actualbudget_zero_budget_to_budget_drips;
+ use crate::transform_state::transform_zero_budgets_to_drips::map_actualbudget_zero_budget_to_budget_drips;
#[test]
fn when_zero_budget_with_integer_budget_per_day_then_evenly_divided_budget_drips() {
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs
new file mode 100644
index 0000000..2469ba9
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/src/transform_state/transform_zero_budgets_to_pipes.rs
@@ -0,0 +1,211 @@
+use actualbudget_models::ActualbudgetZeroBudget;
+use anyhow::Context;
+use itertools::Itertools;
+use schist_models::{BudgetPeriodUnit, Pipe};
+
+use super::{transform_date, transform_id};
+
+pub fn transform_zero_budgets_to_pipes(
+ actualbudget_zero_budgets: &[ActualbudgetZeroBudget],
+) -> anyhow::Result<Vec<Pipe>> {
+ let pipes_with_consecutive_duplicates = actualbudget_zero_budgets
+ .iter()
+ .filter(|zb| zb.amount != 0)
+ .map(transform_zero_budget_to_pipe)
+ .collect::<anyhow::Result<Vec<Pipe>>>()
+ .context("Failed to transform Actualbudget zero budgets to Schist pipes")?;
+
+ Ok(pipes_with_consecutive_duplicates
+ .into_iter()
+ .sorted_by_key(|pipe| pipe.start_date)
+ .sorted_by_key(|pipe| pipe.bucket_id)
+ .fold(Vec::new(), fold_pipes))
+}
+
+fn transform_zero_budget_to_pipe(
+ actualbudget_zero_budget: &ActualbudgetZeroBudget,
+) -> anyhow::Result<Pipe> {
+ Ok(Pipe {
+ id: transform_id(&actualbudget_zero_budget.id),
+ amount: actualbudget_zero_budget.amount,
+ bucket_id: transform_id(&actualbudget_zero_budget.category_id),
+ period_length: 1,
+ period_unit: BudgetPeriodUnit::Month,
+ start_date: transform_date(&actualbudget_zero_budget.month)?,
+ })
+}
+
+fn fold_pipes(mut pipes: Vec<Pipe>, pipe: Pipe) -> Vec<Pipe> {
+ if let Some(index) = pipes.iter().position(|other_pipe| {
+ other_pipe.bucket_id == pipe.bucket_id && other_pipe.amount == pipe.amount
+ }) {
+ if pipe.start_date < pipes[index].start_date {
+ pipes.remove(index);
+ pipes.push(pipe);
+ }
+ } else {
+ pipes.push(pipe);
+ }
+ pipes
+}
+
+#[cfg(test)]
+mod test {
+ use actualbudget_models::{ActualbudgetDate, ActualbudgetZeroBudget};
+ use schist_models::{BudgetPeriodUnit, Pipe};
+
+ use crate::transform_state::transform_id::transform_id;
+
+ #[test]
+ fn when_zero_budget_is_zero_then_does_not_transform() {
+ let actualbudget_zero_budgets = vec![ActualbudgetZeroBudget {
+ id: String::from("zb-0"),
+ month: ActualbudgetDate::from_i32(202408),
+ category_id: String::from("category-0"),
+ amount: 0,
+ do_carry_over: true,
+ }];
+
+ let pipes = super::transform_zero_budgets_to_pipes(&actualbudget_zero_budgets);
+
+ assert!(pipes.is_ok());
+ assert_eq!(0, pipes.unwrap().len());
+ }
+
+ #[test]
+ fn when_zero_budget_is_not_zero_then_does_transform() {
+ let actualbudget_zero_budgets = vec![
+ ActualbudgetZeroBudget {
+ id: String::from("zb-0"),
+ month: ActualbudgetDate::from_i32(202408),
+ category_id: String::from("category-0"),
+ amount: 100,
+ do_carry_over: true,
+ },
+ ActualbudgetZeroBudget {
+ id: String::from("zb-1"),
+ month: ActualbudgetDate::from_i32(202408),
+ category_id: String::from("category-1"),
+ amount: -200,
+ do_carry_over: false,
+ },
+ ];
+
+ let pipes = super::transform_zero_budgets_to_pipes(&actualbudget_zero_budgets);
+
+ assert!(pipes.is_ok());
+ let mut pipes = pipes.unwrap();
+ pipes.sort_by_key(|pipe| -pipe.amount);
+ assert_eq!(
+ vec![
+ Pipe {
+ id: transform_id("zb-0"),
+ amount: 100,
+ bucket_id: transform_id("category-0"),
+ period_length: 1,
+ period_unit: BudgetPeriodUnit::Month,
+ start_date: "2024-08-01".parse().unwrap(),
+ },
+ Pipe {
+ id: transform_id("zb-1"),
+ amount: -200,
+ bucket_id: transform_id("category-1"),
+ period_length: 1,
+ period_unit: BudgetPeriodUnit::Month,
+ start_date: "2024-08-01".parse().unwrap()
+ },
+ ],
+ pipes,
+ );
+ }
+
+ #[test]
+ fn when_zero_budgets_contain_adjacent_duplicates_then_merge() {
+ let actualbudget_zero_budgets = vec![
+ ActualbudgetZeroBudget {
+ id: String::from("zb-0"),
+ month: ActualbudgetDate::from_i32(202408),
+ category_id: String::from("category-0"),
+ amount: 100,
+ do_carry_over: true,
+ },
+ ActualbudgetZeroBudget {
+ id: String::from("zb-1"),
+ month: ActualbudgetDate::from_i32(202409),
+ category_id: String::from("category-1"),
+ amount: -200,
+ do_carry_over: false,
+ },
+ ActualbudgetZeroBudget {
+ id: String::from("zb-0"),
+ month: ActualbudgetDate::from_i32(202410),
+ category_id: String::from("category-0"),
+ amount: 100,
+ do_carry_over: true,
+ },
+ ActualbudgetZeroBudget {
+ id: String::from("zb-0"),
+ month: ActualbudgetDate::from_i32(202411),
+ category_id: String::from("category-0"),
+ amount: 200,
+ do_carry_over: false,
+ },
+ ActualbudgetZeroBudget {
+ id: String::from("zb-1"),
+ month: ActualbudgetDate::from_i32(202410),
+ category_id: String::from("category-1"),
+ amount: -200,
+ do_carry_over: true,
+ },
+ ActualbudgetZeroBudget {
+ id: String::from("zb-0"),
+ month: ActualbudgetDate::from_i32(202409),
+ category_id: String::from("category-0"),
+ amount: 100,
+ do_carry_over: false,
+ },
+ ActualbudgetZeroBudget {
+ id: String::from("zb-1"),
+ month: ActualbudgetDate::from_i32(202408),
+ category_id: String::from("category-1"),
+ amount: -200,
+ do_carry_over: true,
+ },
+ ];
+
+ let pipes = super::transform_zero_budgets_to_pipes(&actualbudget_zero_budgets);
+
+ assert!(pipes.is_ok());
+ let mut pipes = pipes.unwrap();
+ pipes.sort_by_key(|pipe| -pipe.amount);
+ assert_eq!(
+ vec![
+ Pipe {
+ id: transform_id("zb-0"),
+ amount: 200,
+ bucket_id: transform_id("category-0"),
+ period_length: 1,
+ period_unit: BudgetPeriodUnit::Month,
+ start_date: "2024-11-01".parse().unwrap(),
+ },
+ Pipe {
+ id: transform_id("zb-0"),
+ amount: 100,
+ bucket_id: transform_id("category-0"),
+ period_length: 1,
+ period_unit: BudgetPeriodUnit::Month,
+ start_date: "2024-08-01".parse().unwrap(),
+ },
+ Pipe {
+ id: transform_id("zb-1"),
+ amount: -200,
+ bucket_id: transform_id("category-1"),
+ period_length: 1,
+ period_unit: BudgetPeriodUnit::Month,
+ start_date: "2024-08-01".parse().unwrap()
+ },
+ ],
+ pipes,
+ );
+ }
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs
index eaa1805..358d894 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/account_matchers.rs
@@ -10,10 +10,10 @@ pub fn get_account_matchers() -> Vec<Box<dyn Fn(&Account) -> bool>> {
fn matches_bank_account(account: &Account) -> bool {
match account {
Account {
+ id: _id,
name,
- opening_balance: 0,
+ opening_balance: 1200_00,
opening_date,
- ..
} => name == "My bank account" && *opening_date == "2024-08-01".parse().unwrap(),
_ => false,
}
@@ -22,10 +22,10 @@ fn matches_bank_account(account: &Account) -> bool {
fn matches_cash_account(account: &Account) -> bool {
match account {
Account {
+ id: _id,
name,
- opening_balance: 0,
+ opening_balance: 45_10,
opening_date,
- ..
} => name == "My cash account" && *opening_date == "2024-08-01".parse().unwrap(),
_ => false,
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs
index 3821806..5753847 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_matchers.rs
@@ -1,16 +1,27 @@
use schist_models::Bucket;
-use super::bucket_names::BUCKET_NAMES;
+use super::bucket_names::BUCKET_NAMES_AND_GROUP_NAMES;
pub fn get_bucket_matchers() -> Vec<Box<dyn Fn(&Bucket) -> bool>> {
- BUCKET_NAMES
+ BUCKET_NAMES_AND_GROUP_NAMES
.iter()
- .map(|bucket_name| -> Box<dyn Fn(&Bucket) -> bool> {
- Box::new(|c| matches_bucket(c, bucket_name))
- })
+ .map(
+ |bucket_name_and_group_name| -> Box<dyn Fn(&Bucket) -> bool> {
+ Box::new(|c| matches_bucket(c, bucket_name_and_group_name))
+ },
+ )
.collect()
}
-fn matches_bucket(bucket: &Bucket, bucket_name: &str) -> bool {
- bucket.name == bucket_name && bucket.balance == None && bucket.balance_cache_key == None
+fn matches_bucket(bucket: &Bucket, (bucket_name, group_name): &(&str, &str)) -> bool {
+ match bucket {
+ Bucket {
+ id: _id,
+ balance: None,
+ balance_cache_key: None,
+ group,
+ name,
+ } => *name == *bucket_name && *group == *group_name,
+ _ => false,
+ }
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs
index 5f232a8..d8b5d3f 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/bucket_names.rs
@@ -3,17 +3,19 @@ pub const BUCKET_NAME_GENERAL: &str = "General";
pub const BUCKET_NAME_BILLS: &str = "Bills";
pub const BUCKET_NAME_BILLS_FLEXIBLE: &str = "Bills (Flexible)";
pub const BUCKET_NAME_SAVINGS: &str = "Savings";
-pub const BUCKET_NAME_STARTING_BALANCES: &str = "Starting Balances";
pub const BUCKET_NAME_INCOME: &str = "Income";
-pub const BUCKET_NAMES: &[&'static str] = &[
- BUCKET_NAME_FOOD,
- BUCKET_NAME_GENERAL,
- BUCKET_NAME_BILLS,
- BUCKET_NAME_BILLS_FLEXIBLE,
- BUCKET_NAME_SAVINGS,
- BUCKET_NAME_STARTING_BALANCES,
- BUCKET_NAME_INCOME,
+pub const GROUP_NAME_USUAL_EXPENSES: &str = "Usual Expenses";
+pub const GROUP_NAME_SAVINGS: &str = "Investments and Savings";
+pub const GROUP_NAME_INCOME: &str = "Income";
+
+pub const BUCKET_NAMES_AND_GROUP_NAMES: &[(&'static str, &'static str)] = &[
+ (BUCKET_NAME_FOOD, GROUP_NAME_USUAL_EXPENSES),
+ (BUCKET_NAME_GENERAL, GROUP_NAME_USUAL_EXPENSES),
+ (BUCKET_NAME_BILLS, GROUP_NAME_USUAL_EXPENSES),
+ (BUCKET_NAME_BILLS_FLEXIBLE, GROUP_NAME_USUAL_EXPENSES),
+ (BUCKET_NAME_SAVINGS, GROUP_NAME_SAVINGS),
+ (BUCKET_NAME_INCOME, GROUP_NAME_INCOME),
];
pub struct BucketNamesDict {
@@ -22,7 +24,6 @@ pub struct BucketNamesDict {
pub bills: &'static str,
pub bills_flexible: &'static str,
pub savings: &'static str,
- pub starting_balances: &'static str,
pub income: &'static str,
}
@@ -32,6 +33,5 @@ pub const BUCKET_NAMES_DICT: BucketNamesDict = BucketNamesDict {
bills: BUCKET_NAME_BILLS,
bills_flexible: BUCKET_NAME_BILLS_FLEXIBLE,
savings: BUCKET_NAME_SAVINGS,
- starting_balances: BUCKET_NAME_STARTING_BALANCES,
income: BUCKET_NAME_INCOME,
};
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/budget_drip_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs
index 85ad045..629a04b 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/budget_drip_matchers.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/drip_matchers.rs
@@ -8,80 +8,71 @@ use crate::common::bucket_names::{
BUCKET_NAME_SAVINGS,
};
-pub fn get_num_expected_budget_drips() -> usize {
+pub fn get_num_expected_drips() -> usize {
let num_expected_dates = DATES.len();
let num_expected_buckets = 5;
num_expected_dates * num_expected_buckets
}
-pub fn assert_budget_drip_matches(budget_drip: &Drip, buckets: &[Bucket]) {
+pub fn assert_drip_matches(drip: &Drip, buckets: &[Bucket]) {
assert!(
- DATES
- .iter()
- .any(|date| *LazyLock::force(date) == budget_drip.date),
- "budget drip had unexpected date: {:?}",
- budget_drip,
+ DATES.iter().any(|date| *LazyLock::force(date) == drip.date),
+ "drip had unexpected date: {:?}",
+ drip,
);
let bucket_name = buckets
.iter()
- .find(|c| c.id == budget_drip.bucket_id)
+ .find(|c| c.id == drip.bucket_id)
.map(|c| c.name.clone())
.expect(
format!(
- "budget drip {:?} had unexpected bucket ID. buckets were: {:?}",
- budget_drip, buckets,
+ "drip {:?} had unexpected bucket ID. buckets were: {:?}",
+ drip, buckets,
)
.as_str(),
);
- let expected_quantity = match (
- bucket_name.as_str(),
- budget_drip.date.month(),
- budget_drip.date.day(),
- ) {
+ let expected_quantity = match (bucket_name.as_str(), drip.date.month(), drip.date.day()) {
(BUCKET_NAME_FOOD, 8 | 10, 1) => 6_50,
(BUCKET_NAME_FOOD, 8 | 10, _) => 6_45,
(BUCKET_NAME_FOOD, 9 | 11, 1) => 6_86,
(BUCKET_NAME_FOOD, 9 | 11, _) => 6_66,
- (BUCKET_NAME_FOOD, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip),
+ (BUCKET_NAME_FOOD, _, _) => panic!("drip {:?} had unexpected date", drip),
(BUCKET_NAME_GENERAL, 8 | 10, 1) => 3_40,
(BUCKET_NAME_GENERAL, 8 | 10, _) => 3_22,
(BUCKET_NAME_GENERAL, 9 | 11, 1) => 3_43,
(BUCKET_NAME_GENERAL, 9 | 11, _) => 3_33,
- (BUCKET_NAME_GENERAL, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip),
+ (BUCKET_NAME_GENERAL, _, _) => panic!("drip {:?} had unexpected date", drip),
(BUCKET_NAME_BILLS, 8 | 10, 1) => 4_10,
(BUCKET_NAME_BILLS, 8 | 10, _) => 4_03,
(BUCKET_NAME_BILLS, 9 | 11, 1) => 4_36,
(BUCKET_NAME_BILLS, 9 | 11, _) => 4_16,
- (BUCKET_NAME_BILLS, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip),
+ (BUCKET_NAME_BILLS, _, _) => panic!("drip {:?} had unexpected date", drip),
(BUCKET_NAME_BILLS_FLEXIBLE, 8 | 10, 1) => 1_00,
(BUCKET_NAME_BILLS_FLEXIBLE, 8 | 10, _) => 80,
(BUCKET_NAME_BILLS_FLEXIBLE, 9 | 11, 1) => 93,
(BUCKET_NAME_BILLS_FLEXIBLE, 9 | 11, _) => 83,
(BUCKET_NAME_BILLS_FLEXIBLE, _, _) => {
- panic!("budget drip {:?} had unexpected date", budget_drip)
+ panic!("drip {:?} had unexpected date", drip)
}
(BUCKET_NAME_SAVINGS, 8 | 10, 1) => 13_00,
(BUCKET_NAME_SAVINGS, 8 | 10, _) => 12_90,
(BUCKET_NAME_SAVINGS, 9 | 11, 1) => 13_43,
(BUCKET_NAME_SAVINGS, 9 | 11, _) => 13_33,
- (BUCKET_NAME_SAVINGS, _, _) => panic!("budget drip {:?} had unexpected date", budget_drip),
+ (BUCKET_NAME_SAVINGS, _, _) => panic!("drip {:?} had unexpected date", drip),
- (_, _, _) => panic!(
- "budget drip {:?} had unexpected bucket name {}",
- budget_drip, bucket_name,
- ),
+ (_, _, _) => panic!("drip {:?} had unexpected bucket name {}", drip, bucket_name,),
};
assert_eq!(
- expected_quantity, budget_drip.amount,
- "budget drip {:?} had unexpected quantity",
- budget_drip,
+ expected_quantity, drip.amount,
+ "drip {:?} had unexpected quantity",
+ drip,
);
}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs
index f067d0d..82d7870 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/mod.rs
@@ -4,8 +4,9 @@ mod account_transfer_matchers;
mod bucket_matchers;
mod bucket_names;
mod bucket_transfer_matchers;
-mod budget_drip_matchers;
mod db_url;
+mod drip_matchers;
+mod pipe_matchers;
mod test_context;
mod transaction_matchers;
@@ -15,13 +16,12 @@ pub use account_transfer_matchers::get_account_transfer_matchers;
pub use bucket_matchers::get_bucket_matchers;
pub use bucket_names::BUCKET_NAMES_DICT;
pub use bucket_transfer_matchers::get_bucket_transfer_matchers;
-pub use budget_drip_matchers::assert_budget_drip_matches;
-pub use budget_drip_matchers::get_num_expected_budget_drips;
pub use db_url::TEST_IN_DB_URL;
pub use db_url::TEST_OUT_DB_URL;
+pub use drip_matchers::assert_drip_matches;
+pub use drip_matchers::get_num_expected_drips;
+pub use pipe_matchers::get_pipe_matchers;
pub use test_context::TestContext;
pub use transaction_matchers::get_transaction_matchers;
-pub use transaction_matchers::matches_bank_account_initial_transaction;
-pub use transaction_matchers::matches_cash_account_initial_transaction;
pub use transaction_matchers::matches_food_transaction;
pub use transaction_matchers::matches_income_transaction;
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs
new file mode 100644
index 0000000..3ca92c8
--- /dev/null
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/pipe_matchers.rs
@@ -0,0 +1,36 @@
+use schist_models::{Bucket, BudgetPeriodUnit, Pipe};
+
+use super::bucket_names::BUCKET_NAMES_DICT;
+
+macro_rules! matches_pipe {
+ ($pipe:ident, $amount:expr, $start_date:expr) => {
+ Box::new(|pipe: &Pipe, buckets: &[Bucket]| -> bool {
+ let bucket = buckets
+ .iter()
+ .find(|c| c.name == BUCKET_NAMES_DICT.$pipe)
+ .expect("failed to find $pipe bucket");
+
+ match pipe {
+ Pipe {
+ id: _id,
+ amount: $amount,
+ bucket_id,
+ period_length: 1,
+ period_unit: BudgetPeriodUnit::Month,
+ start_date,
+ } => *bucket_id == bucket.id && *start_date == $start_date.parse().unwrap(),
+ _ => false,
+ }
+ })
+ };
+}
+
+pub fn get_pipe_matchers() -> Vec<Box<dyn Fn(&Pipe, &[Bucket]) -> bool>> {
+ vec![
+ matches_pipe!(food, 200_00, "2024-08-01"),
+ matches_pipe!(general, 100_00, "2024-08-01"),
+ matches_pipe!(bills, 125_00, "2024-08-01"),
+ matches_pipe!(bills_flexible, 25_00, "2024-08-01"),
+ matches_pipe!(savings, 400_00, "2024-08-01"),
+ ]
+}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs
index e6c3f27..836a8a1 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/common/transaction_matchers.rs
@@ -1,23 +1,28 @@
-use schist_models::{Account, Transaction};
+use schist_models::{Account, Bucket, Transaction};
+
+use crate::common::bucket_names::{BUCKET_NAME_FOOD, BUCKET_NAME_INCOME};
use super::ACCOUNT_NAMES_DICT;
-pub fn get_transaction_matchers() -> Vec<Box<dyn Fn(&Transaction, &[Account]) -> bool>> {
+pub fn get_transaction_matchers() -> Vec<Box<dyn Fn(&Transaction, &[Account], &[Bucket]) -> bool>> {
vec![
- Box::new(|t, a| matches_bank_account_initial_transaction(t, a)),
- Box::new(|t, a| matches_cash_account_initial_transaction(t, a)),
- Box::new(|t, a| matches_income_transaction(t, a, "08")),
- Box::new(|t, a| matches_income_transaction(t, a, "09")),
- Box::new(|t, a| matches_income_transaction(t, a, "10")),
- Box::new(|t, a| matches_income_transaction(t, a, "11")),
- Box::new(|t, a| matches_food_transaction(t, a, "08")),
- Box::new(|t, a| matches_food_transaction(t, a, "09")),
- Box::new(|t, a| matches_food_transaction(t, a, "10")),
- Box::new(|t, a| matches_food_transaction(t, a, "11")),
+ Box::new(|t, a, b| matches_income_transaction(t, a, b, "08")),
+ Box::new(|t, a, b| matches_income_transaction(t, a, b, "09")),
+ Box::new(|t, a, b| matches_income_transaction(t, a, b, "10")),
+ Box::new(|t, a, b| matches_income_transaction(t, a, b, "11")),
+ Box::new(|t, a, b| matches_food_transaction(t, a, b, "08")),
+ Box::new(|t, a, b| matches_food_transaction(t, a, b, "09")),
+ Box::new(|t, a, b| matches_food_transaction(t, a, b, "10")),
+ Box::new(|t, a, b| matches_food_transaction(t, a, b, "11")),
]
}
-pub fn matches_bank_account_initial_transaction(t: &Transaction, accounts: &[Account]) -> bool {
+pub fn matches_income_transaction(
+ t: &Transaction,
+ accounts: &[Account],
+ buckets: &[Bucket],
+ month: &str,
+) -> bool {
let bank_account_id = accounts
.iter()
.find(|a| a.name == ACCOUNT_NAMES_DICT.bank_account)
@@ -25,66 +30,15 @@ pub fn matches_bank_account_initial_transaction(t: &Transaction, accounts: &[Acc
.id
.clone();
- match t {
- Transaction {
- account_id,
- bucket_id,
- date,
- id: _id,
- description,
- counterparty: payee,
- amount: 1200_00,
- } => {
- *account_id == bank_account_id
- && *bucket_id == get_bucket_id()
- && *date == "2024-08-01".parse().unwrap()
- && description == ""
- && payee == "Starting Balance"
- }
- _ => false,
- }
-}
-
-pub fn matches_cash_account_initial_transaction(t: &Transaction, accounts: &[Account]) -> bool {
- let bank_account_id = accounts
+ let income_bucket = buckets
.iter()
- .find(|a| a.name == ACCOUNT_NAMES_DICT.cash_account)
- .expect("failed to find cash account")
- .id
- .clone();
+ .find(|c| c.name == BUCKET_NAME_INCOME)
+ .expect("failed to find income bucket");
match t {
Transaction {
account_id,
- bucket_id,
- date,
- id: _id,
- description,
- counterparty: payee,
- amount: 45_10,
- } => {
- *account_id == bank_account_id
- && *bucket_id == get_bucket_id()
- && *date == "2024-08-01".parse().unwrap()
- && description == ""
- && payee == "Starting Balance"
- }
- _ => false,
- }
-}
-
-pub fn matches_income_transaction(t: &Transaction, accounts: &[Account], month: &str) -> bool {
- let bank_account_id = accounts
- .iter()
- .find(|a| a.name == ACCOUNT_NAMES_DICT.bank_account)
- .expect("failed to find bank account")
- .id
- .clone();
-
- match t {
- Transaction {
- account_id,
- bucket_id,
+ bucket_id: Some(bucket_id),
date,
description,
id: _id,
@@ -92,7 +46,7 @@ pub fn matches_income_transaction(t: &Transaction, accounts: &[Account], month:
amount: 846_46,
} => {
*account_id == bank_account_id
- && *bucket_id == get_bucket_id()
+ && *bucket_id == income_bucket.id
&& *date == format!("2024-{}-21", month).parse().unwrap()
&& description == "Salary"
&& payee == "Employer Ltd"
@@ -101,7 +55,12 @@ pub fn matches_income_transaction(t: &Transaction, accounts: &[Account], month:
}
}
-pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &str) -> bool {
+pub fn matches_food_transaction(
+ t: &Transaction,
+ accounts: &[Account],
+ buckets: &[Bucket],
+ month: &str,
+) -> bool {
let bank_account_id = accounts
.iter()
.find(|a| a.name == ACCOUNT_NAMES_DICT.bank_account)
@@ -109,10 +68,15 @@ pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &s
.id
.clone();
+ let food_bucket = buckets
+ .iter()
+ .find(|c| c.name == BUCKET_NAME_FOOD)
+ .expect("failed to find food bucket");
+
match t {
Transaction {
account_id,
- bucket_id,
+ bucket_id: Some(bucket_id),
date,
description,
id: _id,
@@ -120,7 +84,7 @@ pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &s
amount: -200_00,
} => {
*account_id == bank_account_id
- && *bucket_id == get_bucket_id()
+ && *bucket_id == food_bucket.id
&& *date == format!("2024-{}-08", month).parse().unwrap()
&& description == ""
&& payee == "Tesco"
@@ -128,7 +92,3 @@ pub fn matches_food_transaction(t: &Transaction, accounts: &[Account], month: &s
_ => false,
}
}
-
-fn get_bucket_id() -> Option<i32> {
- todo!()
-}
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite
index f8d3f5d..5f89924 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/resources/actualbudget_test_db.sqlite
Binary files differ
diff --git a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs
index aff034f..04c21ca 100644
--- a/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs
+++ b/transformers/actualbudget_transformer/actualbudget_to_schist_transformer/tests/run.rs
@@ -2,18 +2,20 @@ pub mod common;
use actualbudget_to_schist_transformer::run::run;
use common::{
- assert_budget_drip_matches, get_account_matchers, get_account_transfer_matchers,
- get_bucket_matchers, get_bucket_transfer_matchers, get_num_expected_budget_drips,
- get_transaction_matchers, TestContext, TEST_IN_DB_URL, TEST_OUT_DB_URL,
+ assert_drip_matches, get_account_matchers, get_account_transfer_matchers, get_bucket_matchers,
+ get_bucket_transfer_matchers, get_num_expected_drips, get_transaction_matchers, TestContext,
+ TEST_IN_DB_URL, TEST_OUT_DB_URL,
};
use diesel::{Connection, SqliteConnection};
-use schist_models::Pipe;
+
use schist_queries::{
account_transfers::get_all_account_transfers, accounts::get_all_accounts,
bucket_transfers::get_all_bucket_transfers, buckets::get_all_buckets, drips::get_all_drips,
pipes::get_all_pipes, transactions::get_all_transactions,
};
+use crate::common::get_pipe_matchers;
+
#[test]
fn given_test_database_when_run_then_exports_expected_schist_state() {
let _context = TestContext::new();
@@ -49,9 +51,9 @@ fn given_test_database_when_run_then_exports_expected_schist_state() {
);
}
- assert_eq!(get_num_expected_budget_drips(), drips.len());
+ assert_eq!(get_num_expected_drips(), drips.len());
for budget_drip in drips {
- assert_budget_drip_matches(&budget_drip, &buckets);
+ assert_drip_matches(&budget_drip, &buckets);
}
let bucket_matchers = get_bucket_matchers();
@@ -82,11 +84,19 @@ fn given_test_database_when_run_then_exports_expected_schist_state() {
assert!(
transaction_matchers
.iter()
- .any(|m| m(&transaction, &accounts)),
+ .any(|m| m(&transaction, &accounts, &buckets)),
"no match found for transaction {:?}",
transaction,
);
}
- assert_eq!(Vec::<Pipe>::new(), pipes);
+ let pipe_matchers = get_pipe_matchers();
+ assert_eq!(pipe_matchers.len(), pipes.len());
+ for pipe in pipes.clone() {
+ assert!(
+ pipe_matchers.iter().any(|p| p(&pipe, &buckets)),
+ "no match found for pipe: {:?}",
+ pipe
+ );
+ }
}