@actual-app/core 26.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/.swcrc +11 -0
  2. package/bin/build-browser +40 -0
  3. package/bin/copy-migrations +9 -0
  4. package/db.sqlite +0 -0
  5. package/default-db.sqlite +0 -0
  6. package/migrations/.force-copy-windows +0 -0
  7. package/migrations/1548957970627_remove-db-version.sql +5 -0
  8. package/migrations/1550601598648_payees.sql +23 -0
  9. package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
  10. package/migrations/1561751833510_indexes.sql +7 -0
  11. package/migrations/1567699552727_budget.sql +38 -0
  12. package/migrations/1582384163573_cleared.sql +6 -0
  13. package/migrations/1597756566448_rules.sql +10 -0
  14. package/migrations/1608652596043_parent_field.sql +13 -0
  15. package/migrations/1608652596044_trans_views.sql +56 -0
  16. package/migrations/1612625548236_optimize.sql +7 -0
  17. package/migrations/1614782639336_trans_views2.sql +33 -0
  18. package/migrations/1615745967948_meta.sql +10 -0
  19. package/migrations/1616167010796_accounts_order.sql +5 -0
  20. package/migrations/1618975177358_schedules.sql +28 -0
  21. package/migrations/1632571489012_remove_cache.js +136 -0
  22. package/migrations/1679728867040_rules_conditions.sql +5 -0
  23. package/migrations/1681115033845_add_schedule_name.sql +5 -0
  24. package/migrations/1682974838138_remove_payee_rules.sql +5 -0
  25. package/migrations/1685007876842_add_category_hidden.sql +6 -0
  26. package/migrations/1686139660866_remove_account_type.sql +5 -0
  27. package/migrations/1688749527273_transaction_filters.sql +10 -0
  28. package/migrations/1688841238000_add_account_type.sql +5 -0
  29. package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
  30. package/migrations/1694438752000_add_goal_targets.sql +7 -0
  31. package/migrations/1697046240000_add_reconciled.sql +5 -0
  32. package/migrations/1704572023730_add_account_sync_source.sql +5 -0
  33. package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
  34. package/migrations/1707267033000_reports.sql +28 -0
  35. package/migrations/1712784523000_unhide_input_group.sql +8 -0
  36. package/migrations/1716359441000_include_current.sql +5 -0
  37. package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
  38. package/migrations/1720664867241_add_payee_favorite.sql +5 -0
  39. package/migrations/1720665000000_goal_context.sql +6 -0
  40. package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
  41. package/migrations/1722804019000_create_dashboard_table.js +69 -0
  42. package/migrations/1723665565000_prefs.js +59 -0
  43. package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
  44. package/migrations/1736640000000_custom_report_sorting.sql +7 -0
  45. package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
  46. package/migrations/1738491452000_sorting_rename.sql +13 -0
  47. package/migrations/1739139550000_bank_sync_page.sql +7 -0
  48. package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
  49. package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
  50. package/migrations/1749799110000_add_tags.sql +10 -0
  51. package/migrations/1749799110001_tags_tombstone.sql +5 -0
  52. package/migrations/1754611200000_add_category_template_settings.sql +5 -0
  53. package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
  54. package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
  55. package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
  56. package/migrations/1765518577215_multiple_dashboards.js +30 -0
  57. package/migrations/1768872504000_add_payee_locations.sql +21 -0
  58. package/package.json +128 -0
  59. package/src/mocks/arbitrary-schema.ts +162 -0
  60. package/src/mocks/budget.ts +901 -0
  61. package/src/mocks/files/8859-1.qfx +63 -0
  62. package/src/mocks/files/best.data-ever$.QFX +124 -0
  63. package/src/mocks/files/big.data.QiF +91 -0
  64. package/src/mocks/files/budgets/.commit-to-git +0 -0
  65. package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
  66. package/src/mocks/files/camt/camt.053.xml +463 -0
  67. package/src/mocks/files/credit-card.ofx +11 -0
  68. package/src/mocks/files/data-multi-decimal.ofx +64 -0
  69. package/src/mocks/files/data-payee-memo.ofx +75 -0
  70. package/src/mocks/files/data-payee-memo.qif +17 -0
  71. package/src/mocks/files/data.ofx +124 -0
  72. package/src/mocks/files/data.qfx +124 -0
  73. package/src/mocks/files/data.qif +91 -0
  74. package/src/mocks/files/default-budget-template/db.sqlite +0 -0
  75. package/src/mocks/files/default-budget-template/metadata.json +6 -0
  76. package/src/mocks/files/html-vals.qfx +17 -0
  77. package/src/mocks/index.ts +221 -0
  78. package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
  79. package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
  80. package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
  81. package/src/mocks/random.ts +16 -0
  82. package/src/mocks/setup.ts +180 -0
  83. package/src/mocks/spreadsheet.ts +101 -0
  84. package/src/mocks/util.ts +82 -0
  85. package/src/platform/client/connection/README.md +3 -0
  86. package/src/platform/client/connection/__mocks__/index.ts +67 -0
  87. package/src/platform/client/connection/index-types.ts +95 -0
  88. package/src/platform/client/connection/index.browser.ts +213 -0
  89. package/src/platform/client/connection/index.ts +155 -0
  90. package/src/platform/client/undo/index.ts +59 -0
  91. package/src/platform/exceptions/__mocks__/index.ts +7 -0
  92. package/src/platform/exceptions/index.ts +9 -0
  93. package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
  94. package/src/platform/server/asyncStorage/index-types.ts +35 -0
  95. package/src/platform/server/asyncStorage/index.api.ts +2 -0
  96. package/src/platform/server/asyncStorage/index.electron.ts +88 -0
  97. package/src/platform/server/asyncStorage/index.ts +126 -0
  98. package/src/platform/server/connection/README.md +3 -0
  99. package/src/platform/server/connection/__mocks__/index.ts +15 -0
  100. package/src/platform/server/connection/index-types.ts +20 -0
  101. package/src/platform/server/connection/index.api.ts +13 -0
  102. package/src/platform/server/connection/index.electron.ts +102 -0
  103. package/src/platform/server/connection/index.ts +154 -0
  104. package/src/platform/server/fetch/__mocks__/index.ts +3 -0
  105. package/src/platform/server/fetch/index.api.ts +1 -0
  106. package/src/platform/server/fetch/index.electron.ts +18 -0
  107. package/src/platform/server/fetch/index.ts +20 -0
  108. package/src/platform/server/fs/index.api.ts +198 -0
  109. package/src/platform/server/fs/index.electron.ts +208 -0
  110. package/src/platform/server/fs/index.test.ts +117 -0
  111. package/src/platform/server/fs/index.ts +416 -0
  112. package/src/platform/server/fs/path-join.api.ts +1 -0
  113. package/src/platform/server/fs/path-join.electron.ts +1 -0
  114. package/src/platform/server/fs/path-join.ts +97 -0
  115. package/src/platform/server/fs/shared.ts +33 -0
  116. package/src/platform/server/indexeddb/index.ts +115 -0
  117. package/src/platform/server/log/index.ts +43 -0
  118. package/src/platform/server/sqlite/index.api.ts +2 -0
  119. package/src/platform/server/sqlite/index.electron.ts +134 -0
  120. package/src/platform/server/sqlite/index.test.ts +108 -0
  121. package/src/platform/server/sqlite/index.ts +241 -0
  122. package/src/platform/server/sqlite/normalise.ts +9 -0
  123. package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
  124. package/src/platform/server/sqlite/unicodeLike.ts +31 -0
  125. package/src/server/__mocks__/post.ts +9 -0
  126. package/src/server/__snapshots__/main.test.ts.snap +199 -0
  127. package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
  128. package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
  129. package/src/server/accounts/app-bank-sync.test.ts +136 -0
  130. package/src/server/accounts/app.ts +1294 -0
  131. package/src/server/accounts/link.ts +25 -0
  132. package/src/server/accounts/payees.ts +36 -0
  133. package/src/server/accounts/sync.test.ts +679 -0
  134. package/src/server/accounts/sync.ts +1168 -0
  135. package/src/server/accounts/title/index.ts +60 -0
  136. package/src/server/accounts/title/lower-case.ts +93 -0
  137. package/src/server/accounts/title/specials.ts +21 -0
  138. package/src/server/admin/app.ts +241 -0
  139. package/src/server/api-models.ts +244 -0
  140. package/src/server/api.test.ts +36 -0
  141. package/src/server/api.ts +1030 -0
  142. package/src/server/app.ts +91 -0
  143. package/src/server/aql/compiler.test.ts +966 -0
  144. package/src/server/aql/compiler.ts +1222 -0
  145. package/src/server/aql/exec.test.ts +289 -0
  146. package/src/server/aql/exec.ts +128 -0
  147. package/src/server/aql/index.ts +41 -0
  148. package/src/server/aql/schema/executors.test.ts +420 -0
  149. package/src/server/aql/schema/executors.ts +345 -0
  150. package/src/server/aql/schema/index.test.ts +67 -0
  151. package/src/server/aql/schema/index.ts +409 -0
  152. package/src/server/aql/schema-helpers.test.ts +242 -0
  153. package/src/server/aql/schema-helpers.ts +208 -0
  154. package/src/server/aql/views.test.ts +62 -0
  155. package/src/server/aql/views.ts +57 -0
  156. package/src/server/auth/app.ts +387 -0
  157. package/src/server/bench.ts +29 -0
  158. package/src/server/budget/actions.ts +686 -0
  159. package/src/server/budget/app.ts +469 -0
  160. package/src/server/budget/base.test.ts +340 -0
  161. package/src/server/budget/base.ts +339 -0
  162. package/src/server/budget/category-template-context.test.ts +1658 -0
  163. package/src/server/budget/category-template-context.ts +862 -0
  164. package/src/server/budget/cleanup-template.pegjs +27 -0
  165. package/src/server/budget/cleanup-template.ts +408 -0
  166. package/src/server/budget/envelope.ts +403 -0
  167. package/src/server/budget/goal-template.pegjs +110 -0
  168. package/src/server/budget/goal-template.ts +309 -0
  169. package/src/server/budget/report.ts +308 -0
  170. package/src/server/budget/schedule-template.test.ts +184 -0
  171. package/src/server/budget/schedule-template.ts +351 -0
  172. package/src/server/budget/statements.ts +60 -0
  173. package/src/server/budget/template-notes.test.ts +393 -0
  174. package/src/server/budget/template-notes.ts +323 -0
  175. package/src/server/budget/util.ts +25 -0
  176. package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
  177. package/src/server/budgetfiles/app.ts +672 -0
  178. package/src/server/budgetfiles/backups.test.ts +79 -0
  179. package/src/server/budgetfiles/backups.ts +251 -0
  180. package/src/server/cloud-storage.ts +467 -0
  181. package/src/server/dashboard/app.ts +373 -0
  182. package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
  183. package/src/server/db/index.test.ts +300 -0
  184. package/src/server/db/index.ts +855 -0
  185. package/src/server/db/mappings.ts +59 -0
  186. package/src/server/db/sort.ts +58 -0
  187. package/src/server/db/types/index.ts +342 -0
  188. package/src/server/db/util.ts +36 -0
  189. package/src/server/encryption/app.ts +133 -0
  190. package/src/server/encryption/encryption-internals.api.ts +2 -0
  191. package/src/server/encryption/encryption-internals.electron.ts +89 -0
  192. package/src/server/encryption/encryption-internals.ts +109 -0
  193. package/src/server/encryption/encryption.test.ts +19 -0
  194. package/src/server/encryption/index.test.ts +19 -0
  195. package/src/server/encryption/index.ts +89 -0
  196. package/src/server/errors.ts +110 -0
  197. package/src/server/filters/app.ts +191 -0
  198. package/src/server/importers/actual.ts +49 -0
  199. package/src/server/importers/index.ts +58 -0
  200. package/src/server/importers/ynab4-types.ts +163 -0
  201. package/src/server/importers/ynab4.ts +470 -0
  202. package/src/server/importers/ynab5-types.ts +290 -0
  203. package/src/server/importers/ynab5.ts +1193 -0
  204. package/src/server/main-app.ts +25 -0
  205. package/src/server/main.test.ts +392 -0
  206. package/src/server/main.ts +336 -0
  207. package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
  208. package/src/server/migrate/cli.ts +100 -0
  209. package/src/server/migrate/migrations.test.ts +81 -0
  210. package/src/server/migrate/migrations.ts +192 -0
  211. package/src/server/models.ts +184 -0
  212. package/src/server/mutators.ts +139 -0
  213. package/src/server/notes/app.ts +18 -0
  214. package/src/server/payees/app.ts +351 -0
  215. package/src/server/polyfills.ts +26 -0
  216. package/src/server/post.ts +219 -0
  217. package/src/server/preferences/app.ts +249 -0
  218. package/src/server/prefs.ts +91 -0
  219. package/src/server/reports/app.ts +187 -0
  220. package/src/server/rules/action.ts +344 -0
  221. package/src/server/rules/app.ts +193 -0
  222. package/src/server/rules/condition.ts +436 -0
  223. package/src/server/rules/customFunctions.ts +61 -0
  224. package/src/server/rules/formula-action.test.ts +175 -0
  225. package/src/server/rules/handlebars-helpers.ts +131 -0
  226. package/src/server/rules/index.test.ts +1095 -0
  227. package/src/server/rules/index.ts +22 -0
  228. package/src/server/rules/rule-indexer.ts +89 -0
  229. package/src/server/rules/rule-utils.ts +274 -0
  230. package/src/server/rules/rule.ts +193 -0
  231. package/src/server/schedules/app.test.ts +502 -0
  232. package/src/server/schedules/app.ts +644 -0
  233. package/src/server/schedules/find-schedules.ts +391 -0
  234. package/src/server/server-config.ts +59 -0
  235. package/src/server/sheet.test.ts +101 -0
  236. package/src/server/sheet.ts +280 -0
  237. package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
  238. package/src/server/spreadsheet/app.ts +54 -0
  239. package/src/server/spreadsheet/globals.ts +13 -0
  240. package/src/server/spreadsheet/graph-data-structure.ts +165 -0
  241. package/src/server/spreadsheet/scratch +60 -0
  242. package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
  243. package/src/server/spreadsheet/spreadsheet.ts +523 -0
  244. package/src/server/spreadsheet/util.ts +15 -0
  245. package/src/server/sql/init.sql +88 -0
  246. package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
  247. package/src/server/sync/app.ts +29 -0
  248. package/src/server/sync/encoder.ts +129 -0
  249. package/src/server/sync/index.ts +820 -0
  250. package/src/server/sync/make-test-message.ts +19 -0
  251. package/src/server/sync/migrate.test.ts +169 -0
  252. package/src/server/sync/migrate.ts +48 -0
  253. package/src/server/sync/repair.ts +39 -0
  254. package/src/server/sync/reset.ts +91 -0
  255. package/src/server/sync/sync.property.test.ts +385 -0
  256. package/src/server/sync/sync.test.ts +349 -0
  257. package/src/server/sync/utils.ts +3 -0
  258. package/src/server/tags/app.ts +101 -0
  259. package/src/server/tests/mockData.json +9352 -0
  260. package/src/server/tests/mockSyncServer.ts +119 -0
  261. package/src/server/tools/app.ts +152 -0
  262. package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
  263. package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
  264. package/src/server/transactions/app.ts +136 -0
  265. package/src/server/transactions/export/export-to-csv.ts +132 -0
  266. package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
  267. package/src/server/transactions/import/ofx2json.test.ts +33 -0
  268. package/src/server/transactions/import/ofx2json.ts +157 -0
  269. package/src/server/transactions/import/parse-file.test.ts +224 -0
  270. package/src/server/transactions/import/parse-file.ts +286 -0
  271. package/src/server/transactions/import/qif2json.ts +110 -0
  272. package/src/server/transactions/import/xmlcamt2json.ts +168 -0
  273. package/src/server/transactions/index.ts +196 -0
  274. package/src/server/transactions/merge.test.ts +370 -0
  275. package/src/server/transactions/merge.ts +139 -0
  276. package/src/server/transactions/transaction-rules.test.ts +994 -0
  277. package/src/server/transactions/transaction-rules.ts +1038 -0
  278. package/src/server/transactions/transfer.test.ts +221 -0
  279. package/src/server/transactions/transfer.ts +173 -0
  280. package/src/server/undo.ts +271 -0
  281. package/src/server/update.ts +37 -0
  282. package/src/server/util/budget-name.ts +61 -0
  283. package/src/server/util/custom-sync-mapping.ts +48 -0
  284. package/src/server/util/rschedule.ts +9 -0
  285. package/src/shared/__mocks__/platform.ts +7 -0
  286. package/src/shared/__snapshots__/months.test.ts.snap +21 -0
  287. package/src/shared/arithmetic.test.ts +112 -0
  288. package/src/shared/arithmetic.ts +170 -0
  289. package/src/shared/async.test.ts +135 -0
  290. package/src/shared/async.ts +76 -0
  291. package/src/shared/constants.ts +5 -0
  292. package/src/shared/currencies.ts +70 -0
  293. package/src/shared/dashboard.ts +260 -0
  294. package/src/shared/environment.ts +18 -0
  295. package/src/shared/errors.ts +195 -0
  296. package/src/shared/locale.ts +27 -0
  297. package/src/shared/location-utils.test.ts +69 -0
  298. package/src/shared/location-utils.ts +49 -0
  299. package/src/shared/months.test.ts +5 -0
  300. package/src/shared/months.ts +485 -0
  301. package/src/shared/normalisation.ts +6 -0
  302. package/src/shared/platform.electron.ts +21 -0
  303. package/src/shared/platform.ts +20 -0
  304. package/src/shared/query.ts +176 -0
  305. package/src/shared/rules.test.ts +56 -0
  306. package/src/shared/rules.ts +371 -0
  307. package/src/shared/schedules.test.ts +570 -0
  308. package/src/shared/schedules.ts +560 -0
  309. package/src/shared/test-helpers.ts +156 -0
  310. package/src/shared/transactions.test.ts +275 -0
  311. package/src/shared/transactions.ts +433 -0
  312. package/src/shared/transfer.test.ts +75 -0
  313. package/src/shared/transfer.ts +16 -0
  314. package/src/shared/user.ts +4 -0
  315. package/src/shared/util.test.ts +240 -0
  316. package/src/shared/util.ts +633 -0
  317. package/src/types/api-handlers.ts +287 -0
  318. package/src/types/budget.ts +8 -0
  319. package/src/types/file.ts +47 -0
  320. package/src/types/handlers.ts +46 -0
  321. package/src/types/models/account.ts +24 -0
  322. package/src/types/models/bank-sync.ts +23 -0
  323. package/src/types/models/bank.ts +6 -0
  324. package/src/types/models/category-group.ts +11 -0
  325. package/src/types/models/category.ts +13 -0
  326. package/src/types/models/dashboard.ts +199 -0
  327. package/src/types/models/gocardless.ts +84 -0
  328. package/src/types/models/import-transaction.ts +56 -0
  329. package/src/types/models/index.ts +23 -0
  330. package/src/types/models/nearby-payee.ts +7 -0
  331. package/src/types/models/note.ts +4 -0
  332. package/src/types/models/openid.ts +8 -0
  333. package/src/types/models/payee-location.ts +8 -0
  334. package/src/types/models/payee.ts +10 -0
  335. package/src/types/models/pluggyai.ts +19 -0
  336. package/src/types/models/reports.ts +144 -0
  337. package/src/types/models/rule.ts +174 -0
  338. package/src/types/models/schedule.ts +49 -0
  339. package/src/types/models/simplefin.ts +28 -0
  340. package/src/types/models/tags.ts +6 -0
  341. package/src/types/models/templates.ts +135 -0
  342. package/src/types/models/transaction-filter.ts +9 -0
  343. package/src/types/models/transaction.ts +39 -0
  344. package/src/types/models/user-access.ts +10 -0
  345. package/src/types/models/user.ts +25 -0
  346. package/src/types/prefs.ts +167 -0
  347. package/src/types/server-events.ts +86 -0
  348. package/src/types/server-handlers.ts +27 -0
  349. package/src/types/util.ts +26 -0
  350. package/tsconfig.json +34 -0
  351. package/typings/pegjs.ts +1 -0
  352. package/typings/process-worker.ts +12 -0
  353. package/typings/vite-plugin-peggy-loader.ts +1 -0
  354. package/typings/window.ts +62 -0
  355. package/vite.config.ts +109 -0
  356. package/vite.desktop.config.ts +59 -0
  357. package/vitest.config.ts +43 -0
  358. package/vitest.web.config.ts +38 -0
@@ -0,0 +1,340 @@
1
+ // @ts-strict-ignore
2
+ import * as monthUtils from '../../shared/months';
3
+ import * as db from '../db';
4
+ import * as sheet from '../sheet';
5
+
6
+ import { createAllBudgets } from './base';
7
+
8
+ beforeEach(() => {
9
+ return global.emptyDatabase()();
10
+ });
11
+
12
+ describe('Base budget', () => {
13
+ it('Recomputes budget cells when account fields change', async () => {
14
+ await sheet.loadSpreadsheet(db);
15
+
16
+ await db.insertCategoryGroup({ id: 'group1', name: 'group1' });
17
+ await db.insertCategoryGroup({
18
+ id: 'group2',
19
+ name: 'income',
20
+ is_income: 1,
21
+ });
22
+ const catId = await db.insertCategory({
23
+ name: 'foo',
24
+ cat_group: 'group1',
25
+ });
26
+
27
+ await createAllBudgets();
28
+
29
+ // Insert a transaction referencing an account that doesn't exist
30
+ // yet
31
+ await db.insertTransaction({
32
+ date: '2016-12-15',
33
+ amount: -5000,
34
+ account: '29eef937-9933-49ef-80d9-71627074cf31',
35
+ category: catId,
36
+ });
37
+
38
+ // Make sure that the spreadsheet finishes processing to make sure
39
+ // the next change doesn't get batched in with it
40
+ await sheet.waitOnSpreadsheet();
41
+
42
+ // The category should have nothing spent on it yet
43
+ expect(
44
+ sheet.getCellValue(
45
+ monthUtils.sheetForMonth('2016-12'),
46
+ `sum-amount-${catId}`,
47
+ ),
48
+ ).toBe(0);
49
+
50
+ // Create the referenced account
51
+ await db.insertAccount({
52
+ id: '29eef937-9933-49ef-80d9-71627074cf31',
53
+ name: 'foo',
54
+ });
55
+
56
+ // Make sure the spreadsheet finishes processing
57
+ await sheet.waitOnSpreadsheet();
58
+
59
+ // The category should see the transaction
60
+ expect(
61
+ sheet.getCellValue(
62
+ monthUtils.sheetForMonth('2016-12'),
63
+ `sum-amount-${catId}`,
64
+ ),
65
+ ).toBe(-5000);
66
+ });
67
+
68
+ it('Excludes hidden categories from group totals in Report Budget', async () => {
69
+ await sheet.loadSpreadsheet(db);
70
+ sheet.get().meta().budgetType = 'tracking';
71
+
72
+ // Create a group with multiple categories
73
+ await db.insertCategoryGroup({ id: 'group1', name: 'Test Group' });
74
+ await db.insertCategoryGroup({
75
+ id: 'group2',
76
+ name: 'Income',
77
+ is_income: 1,
78
+ });
79
+
80
+ const visibleCatId = await db.insertCategory({
81
+ name: 'Visible Category',
82
+ cat_group: 'group1',
83
+ });
84
+
85
+ const hiddenCatId = await db.insertCategory({
86
+ name: 'Hidden Category',
87
+ cat_group: 'group1',
88
+ hidden: 1,
89
+ });
90
+
91
+ await createAllBudgets();
92
+ const month = '2017-01';
93
+ const sheetName = monthUtils.sheetForMonth(month);
94
+
95
+ await db.insertAccount({ id: 'account1', name: 'Account 1' });
96
+
97
+ await db.insertTransaction({
98
+ date: '2017-01-15',
99
+ amount: -1000,
100
+ account: 'account1',
101
+ category: visibleCatId,
102
+ });
103
+
104
+ await db.insertTransaction({
105
+ date: '2017-01-15',
106
+ amount: -2000,
107
+ account: 'account1',
108
+ category: hiddenCatId,
109
+ });
110
+
111
+ await sheet.waitOnSpreadsheet();
112
+
113
+ // Verify individual category amounts
114
+ expect(sheet.getCellValue(sheetName, `sum-amount-${visibleCatId}`)).toBe(
115
+ -1000,
116
+ );
117
+
118
+ expect(sheet.getCellValue(sheetName, `sum-amount-${hiddenCatId}`)).toBe(
119
+ -2000,
120
+ );
121
+
122
+ // Verify group total only includes visible category
123
+ expect(sheet.getCellValue(sheetName, `group-sum-amount-group1`)).toBe(
124
+ -1000,
125
+ );
126
+
127
+ // Now toggle hidden status of the hidden category to make it visible
128
+ await db.updateCategory({
129
+ id: hiddenCatId,
130
+ name: 'Hidden Category',
131
+ cat_group: 'group1',
132
+ is_income: 0,
133
+ hidden: 0,
134
+ });
135
+
136
+ await sheet.waitOnSpreadsheet();
137
+
138
+ // After making hidden category visible, group total should include both
139
+ expect(sheet.getCellValue(sheetName, `group-sum-amount-group1`)).toBe(
140
+ -3000,
141
+ );
142
+ });
143
+
144
+ it('Excludes hidden category groups from budget totals in Report Budget', async () => {
145
+ await sheet.loadSpreadsheet(db);
146
+ sheet.get().meta().budgetType = 'tracking';
147
+
148
+ // Create two expense groups - one visible, one hidden
149
+ await db.insertCategoryGroup({
150
+ id: 'visible-group',
151
+ name: 'Visible Group',
152
+ });
153
+ await db.insertCategoryGroup({
154
+ id: 'hidden-group',
155
+ name: 'Hidden Group',
156
+ hidden: 1,
157
+ });
158
+
159
+ await db.insertCategoryGroup({
160
+ id: 'income-group',
161
+ name: 'Income',
162
+ is_income: 1,
163
+ });
164
+
165
+ const visibleGroupCatId = await db.insertCategory({
166
+ name: 'Visible Group Category',
167
+ cat_group: 'visible-group',
168
+ });
169
+
170
+ const hiddenGroupCatId = await db.insertCategory({
171
+ name: 'Hidden Group Category',
172
+ cat_group: 'hidden-group',
173
+ });
174
+
175
+ await createAllBudgets();
176
+ const month = '2017-01';
177
+ const sheetName = monthUtils.sheetForMonth(month);
178
+
179
+ await db.insertAccount({ id: 'account1', name: 'Account 1' });
180
+
181
+ await db.insertTransaction({
182
+ date: '2017-01-15',
183
+ amount: -1000,
184
+ account: 'account1',
185
+ category: visibleGroupCatId,
186
+ });
187
+
188
+ await db.insertTransaction({
189
+ date: '2017-01-15',
190
+ amount: -2000,
191
+ account: 'account1',
192
+ category: hiddenGroupCatId,
193
+ });
194
+
195
+ await sheet.waitOnSpreadsheet();
196
+
197
+ // Verify individual amounts
198
+ expect(
199
+ sheet.getCellValue(sheetName, `sum-amount-${visibleGroupCatId}`),
200
+ ).toBe(-1000);
201
+
202
+ expect(
203
+ sheet.getCellValue(sheetName, `sum-amount-${hiddenGroupCatId}`),
204
+ ).toBe(-2000);
205
+
206
+ expect(
207
+ sheet.getCellValue(sheetName, `group-sum-amount-visible-group`),
208
+ ).toBe(-1000);
209
+
210
+ expect(sheet.getCellValue(sheetName, `group-sum-amount-hidden-group`)).toBe(
211
+ -2000,
212
+ );
213
+
214
+ // Verify total spent only includes visible group
215
+ expect(sheet.getCellValue(sheetName, 'total-spent')).toBe(-1000);
216
+
217
+ // Now toggle hidden status of the hidden group to make it visible
218
+ await db.updateCategoryGroup({
219
+ id: 'hidden-group',
220
+ name: 'Hidden Group',
221
+ is_income: 0,
222
+ hidden: 0,
223
+ });
224
+
225
+ await sheet.waitOnSpreadsheet();
226
+
227
+ // After making hidden group visible, total should include both
228
+ expect(sheet.getCellValue(sheetName, 'total-spent')).toBe(-3000);
229
+ });
230
+
231
+ it('Includes hidden categories in group totals for Rollover Budget', async () => {
232
+ await sheet.loadSpreadsheet(db);
233
+ // Rollover is the default, but explicit for clarity
234
+ sheet.get().meta().budgetType = 'envelope';
235
+
236
+ // Create a group with multiple categories
237
+ await db.insertCategoryGroup({ id: 'group1', name: 'Test Group' });
238
+ await db.insertCategoryGroup({
239
+ id: 'group2',
240
+ name: 'Income',
241
+ is_income: 1,
242
+ });
243
+
244
+ const visibleCatId = await db.insertCategory({
245
+ name: 'Visible Category',
246
+ cat_group: 'group1',
247
+ });
248
+
249
+ const hiddenCatId = await db.insertCategory({
250
+ name: 'Hidden Category',
251
+ cat_group: 'group1',
252
+ hidden: 1,
253
+ });
254
+
255
+ await createAllBudgets();
256
+ const month = '2017-01';
257
+ const sheetName = monthUtils.sheetForMonth(month);
258
+
259
+ await db.insertAccount({ id: 'account1', name: 'Account 1' });
260
+
261
+ await db.insertTransaction({
262
+ date: '2017-01-15',
263
+ amount: -1000,
264
+ account: 'account1',
265
+ category: visibleCatId,
266
+ });
267
+
268
+ await db.insertTransaction({
269
+ date: '2017-01-15',
270
+ amount: -2000,
271
+ account: 'account1',
272
+ category: hiddenCatId,
273
+ });
274
+
275
+ await sheet.waitOnSpreadsheet();
276
+
277
+ // Verify group total includes both visible and hidden category amounts
278
+ expect(sheet.getCellValue(sheetName, `group-sum-amount-group1`)).toBe(
279
+ -3000,
280
+ );
281
+ });
282
+
283
+ it('Includes hidden category groups in budget totals for Rollover Budget', async () => {
284
+ await sheet.loadSpreadsheet(db);
285
+ // Rollover is the default, but explicit for clarity
286
+ sheet.get().meta().budgetType = 'envelope';
287
+
288
+ // Create two expense groups - one visible, one hidden
289
+ await db.insertCategoryGroup({
290
+ id: 'visible-group',
291
+ name: 'Visible Group',
292
+ });
293
+ await db.insertCategoryGroup({
294
+ id: 'hidden-group',
295
+ name: 'Hidden Group',
296
+ hidden: 1,
297
+ });
298
+
299
+ await db.insertCategoryGroup({
300
+ id: 'income-group',
301
+ name: 'Income',
302
+ is_income: 1,
303
+ });
304
+
305
+ const visibleGroupCatId = await db.insertCategory({
306
+ name: 'Visible Group Category',
307
+ cat_group: 'visible-group',
308
+ });
309
+
310
+ const hiddenGroupCatId = await db.insertCategory({
311
+ name: 'Hidden Group Category',
312
+ cat_group: 'hidden-group',
313
+ });
314
+
315
+ await createAllBudgets();
316
+ const month = '2017-01';
317
+ const sheetName = monthUtils.sheetForMonth(month);
318
+
319
+ await db.insertAccount({ id: 'account1', name: 'Account 1' });
320
+
321
+ await db.insertTransaction({
322
+ date: '2017-01-15',
323
+ amount: -1000,
324
+ account: 'account1',
325
+ category: visibleGroupCatId,
326
+ });
327
+
328
+ await db.insertTransaction({
329
+ date: '2017-01-15',
330
+ amount: -2000,
331
+ account: 'account1',
332
+ category: hiddenGroupCatId,
333
+ });
334
+
335
+ await sheet.waitOnSpreadsheet();
336
+
337
+ // Verify total spent includes both visible and hidden group amounts
338
+ expect(sheet.getCellValue(sheetName, 'total-spent')).toBe(-3000);
339
+ });
340
+ });
@@ -0,0 +1,339 @@
1
+ // @ts-strict-ignore
2
+ import * as monthUtils from '../../shared/months';
3
+ import { q } from '../../shared/query';
4
+ import { getChangedValues } from '../../shared/util';
5
+ import type { CategoryGroupEntity } from '../../types/models';
6
+ import { aqlQuery } from '../aql';
7
+ import * as db from '../db';
8
+ import * as sheet from '../sheet';
9
+ import { resolveName } from '../spreadsheet/util';
10
+
11
+ import * as budgetActions from './actions';
12
+ import * as envelopeBudget from './envelope';
13
+ import * as report from './report';
14
+
15
+ export function getBudgetType() {
16
+ const meta = sheet.get().meta();
17
+ return meta.budgetType || 'envelope';
18
+ }
19
+
20
+ export function getBudgetRange(start: string, end: string) {
21
+ start = monthUtils.getMonth(start);
22
+ end = monthUtils.getMonth(end);
23
+
24
+ // The start date should never be after the end date. If that
25
+ // happened, the month range might be a valid range and weird
26
+ // things happen
27
+ if (start > end) {
28
+ start = end;
29
+ }
30
+
31
+ // Budgets should exist 3 months before the earliest needed date
32
+ // (either the oldest transaction or the current month if no
33
+ // transactions yet), and a year from the current date. There's no
34
+ // need to ever have budgets outside that range.
35
+ start = monthUtils.subMonths(start, 3);
36
+ end = monthUtils.addMonths(end, 12);
37
+
38
+ return { start, end, range: monthUtils.rangeInclusive(start, end) };
39
+ }
40
+
41
+ export function createCategory(cat, sheetName, prevSheetName, start, end) {
42
+ sheet.get().createDynamic(sheetName, 'sum-amount-' + cat.id, {
43
+ initialValue: 0,
44
+ run: () => {
45
+ // Making this sync is faster!
46
+ const rows = db.runQuery<{ amount: number }>(
47
+ `SELECT SUM(amount) as amount FROM v_transactions_internal_alive t
48
+ LEFT JOIN accounts a ON a.id = t.account
49
+ WHERE t.date >= ${start} AND t.date <= ${end}
50
+ AND category = '${cat.id}' AND a.offbudget = 0`,
51
+ [],
52
+ true,
53
+ );
54
+ const row = rows[0];
55
+ const amount = row ? row.amount : 0;
56
+ return amount || 0;
57
+ },
58
+ });
59
+
60
+ if (getBudgetType() === 'envelope') {
61
+ envelopeBudget.createCategory(cat, sheetName, prevSheetName);
62
+ } else {
63
+ void report.createCategory(cat, sheetName, prevSheetName);
64
+ }
65
+ }
66
+
67
+ function handleAccountChange(months, oldValue, newValue) {
68
+ if (!oldValue || oldValue.offbudget !== newValue.offbudget) {
69
+ const rows = db.runQuery<Pick<db.DbTransaction, 'category'>>(
70
+ `
71
+ SELECT DISTINCT(category) as category FROM transactions
72
+ WHERE acct = ?
73
+ `,
74
+ [newValue.id],
75
+ true,
76
+ );
77
+
78
+ months.forEach(month => {
79
+ const sheetName = monthUtils.sheetForMonth(month);
80
+
81
+ rows.forEach(row => {
82
+ sheet
83
+ .get()
84
+ .recompute(resolveName(sheetName, 'sum-amount-' + row.category));
85
+ });
86
+ });
87
+ }
88
+ }
89
+
90
+ function handleTransactionChange(transaction, changedFields) {
91
+ if (
92
+ (changedFields.has('date') ||
93
+ changedFields.has('acct') ||
94
+ changedFields.has('amount') ||
95
+ changedFields.has('category') ||
96
+ changedFields.has('tombstone') ||
97
+ changedFields.has('isParent')) &&
98
+ transaction.date &&
99
+ transaction.category
100
+ ) {
101
+ const month = monthUtils.monthFromDate(db.fromDateRepr(transaction.date));
102
+ const sheetName = monthUtils.sheetForMonth(month);
103
+
104
+ sheet
105
+ .get()
106
+ .recompute(resolveName(sheetName, 'sum-amount-' + transaction.category));
107
+ }
108
+ }
109
+
110
+ function handleCategoryMappingChange(months, oldValue, newValue) {
111
+ months.forEach(month => {
112
+ const sheetName = monthUtils.sheetForMonth(month);
113
+ if (oldValue) {
114
+ sheet
115
+ .get()
116
+ .recompute(resolveName(sheetName, 'sum-amount-' + oldValue.transferId));
117
+ }
118
+ sheet
119
+ .get()
120
+ .recompute(resolveName(sheetName, 'sum-amount-' + newValue.transferId));
121
+ });
122
+ }
123
+
124
+ function handleBudgetMonthChange(budget) {
125
+ const sheetName = monthUtils.sheetForMonth(budget.id);
126
+ sheet.get().set(`${sheetName}!buffered`, budget.buffered);
127
+ }
128
+
129
+ function handleBudgetChange(budget) {
130
+ if (budget.category) {
131
+ const sheetName = monthUtils.sheetForMonth(budget.month.toString());
132
+ sheet
133
+ .get()
134
+ .set(`${sheetName}!budget-${budget.category}`, budget.amount || 0);
135
+ sheet
136
+ .get()
137
+ .set(
138
+ `${sheetName}!carryover-${budget.category}`,
139
+ budget.carryover === 1 ? true : false,
140
+ );
141
+ sheet.get().set(`${sheetName}!goal-${budget.category}`, budget.goal);
142
+ sheet
143
+ .get()
144
+ .set(`${sheetName}!long-goal-${budget.category}`, budget.long_goal);
145
+ }
146
+ }
147
+
148
+ export function triggerBudgetChanges(oldValues, newValues) {
149
+ const { createdMonths = new Set() } = sheet.get().meta();
150
+ const budgetType = getBudgetType();
151
+ sheet.startTransaction();
152
+
153
+ try {
154
+ newValues.forEach((items, table) => {
155
+ const old = oldValues.get(table);
156
+
157
+ items.forEach(newValue => {
158
+ const oldValue = old && old.get(newValue.id);
159
+
160
+ if (table === 'zero_budget_months') {
161
+ handleBudgetMonthChange(newValue);
162
+ } else if (table === 'zero_budgets' || table === 'reflect_budgets') {
163
+ handleBudgetChange(newValue);
164
+ } else if (table === 'transactions') {
165
+ const changed = new Set(
166
+ Object.keys(getChangedValues(oldValue || {}, newValue) || {}),
167
+ );
168
+
169
+ if (oldValue) {
170
+ handleTransactionChange(oldValue, changed);
171
+ }
172
+ handleTransactionChange(newValue, changed);
173
+ } else if (table === 'category_mapping') {
174
+ handleCategoryMappingChange(createdMonths, oldValue, newValue);
175
+ } else if (table === 'categories') {
176
+ if (budgetType === 'envelope') {
177
+ envelopeBudget.handleCategoryChange(
178
+ createdMonths,
179
+ oldValue,
180
+ newValue,
181
+ );
182
+ } else {
183
+ report.handleCategoryChange(createdMonths, oldValue, newValue);
184
+ }
185
+ } else if (table === 'category_groups') {
186
+ if (budgetType === 'envelope') {
187
+ envelopeBudget.handleCategoryGroupChange(
188
+ createdMonths,
189
+ oldValue,
190
+ newValue,
191
+ );
192
+ } else {
193
+ report.handleCategoryGroupChange(createdMonths, oldValue, newValue);
194
+ }
195
+ } else if (table === 'accounts') {
196
+ handleAccountChange(createdMonths, oldValue, newValue);
197
+ }
198
+ });
199
+ });
200
+ } finally {
201
+ sheet.endTransaction();
202
+ }
203
+ }
204
+
205
+ export async function doTransfer(categoryIds, transferId) {
206
+ const { createdMonths: months } = sheet.get().meta();
207
+
208
+ [...months].forEach(month => {
209
+ const totalValue = categoryIds
210
+ .map(id => {
211
+ return budgetActions.getBudget({ month, category: id });
212
+ })
213
+ .reduce((total, value) => total + value, 0);
214
+
215
+ const transferValue = budgetActions.getBudget({
216
+ month,
217
+ category: transferId,
218
+ });
219
+
220
+ void budgetActions.setBudget({
221
+ month,
222
+ category: transferId,
223
+ amount: totalValue + transferValue,
224
+ });
225
+ });
226
+ }
227
+
228
+ export async function createBudget(months) {
229
+ const { data: groups }: { data: CategoryGroupEntity[] } = await aqlQuery(
230
+ q('category_groups').select('*'),
231
+ );
232
+ const categories = groups.flatMap(group => group.categories);
233
+
234
+ sheet.startTransaction();
235
+ const meta = sheet.get().meta();
236
+ meta.createdMonths = meta.createdMonths || new Set();
237
+
238
+ const budgetType = getBudgetType();
239
+
240
+ if (budgetType === 'envelope') {
241
+ envelopeBudget.createBudget(meta, categories, months);
242
+ }
243
+
244
+ months.forEach(month => {
245
+ if (!meta.createdMonths.has(month)) {
246
+ const prevMonth = monthUtils.prevMonth(month);
247
+ const { start, end } = monthUtils.bounds(month);
248
+ const sheetName = monthUtils.sheetForMonth(month);
249
+ const prevSheetName = monthUtils.sheetForMonth(prevMonth);
250
+
251
+ categories.forEach(cat => {
252
+ createCategory(cat, sheetName, prevSheetName, start, end);
253
+ });
254
+ groups.forEach(group => {
255
+ if (budgetType === 'envelope') {
256
+ envelopeBudget.createCategoryGroup(group, sheetName);
257
+ } else {
258
+ report.createCategoryGroup(group, sheetName);
259
+ }
260
+ });
261
+
262
+ if (budgetType === 'envelope') {
263
+ envelopeBudget.createSummary(
264
+ groups,
265
+ categories,
266
+ prevSheetName,
267
+ sheetName,
268
+ );
269
+ } else {
270
+ report.createSummary(groups, sheetName);
271
+ }
272
+
273
+ meta.createdMonths.add(month);
274
+ }
275
+ });
276
+
277
+ sheet.get().setMeta(meta);
278
+ sheet.endTransaction();
279
+
280
+ // Wait for the spreadsheet to finish computing. Normally this won't
281
+ // do anything (as values are cached) but on first run this need to
282
+ // show the loading screen while it initially sets up.
283
+ await sheet.waitOnSpreadsheet();
284
+ }
285
+
286
+ export async function createAllBudgets() {
287
+ const earliestTransaction = await db.first<db.DbTransaction>(
288
+ 'SELECT * FROM transactions WHERE isChild=0 AND date IS NOT NULL ORDER BY date ASC LIMIT 1',
289
+ );
290
+ const earliestDate =
291
+ earliestTransaction && db.fromDateRepr(earliestTransaction.date);
292
+ const currentMonth = monthUtils.currentMonth();
293
+
294
+ // Get the range based off of the earliest transaction and the
295
+ // current month. If no transactions currently exist the current
296
+ // month is also used as the starting month
297
+ const { start, end, range } = getBudgetRange(
298
+ earliestDate || currentMonth,
299
+ currentMonth,
300
+ );
301
+
302
+ const meta = sheet.get().meta();
303
+ const createdMonths = meta.createdMonths || new Set();
304
+ const newMonths = range.filter(m => !createdMonths.has(m));
305
+
306
+ if (newMonths.length > 0) {
307
+ await createBudget(range);
308
+ }
309
+
310
+ return { start, end };
311
+ }
312
+
313
+ export async function setType(type) {
314
+ const meta = sheet.get().meta();
315
+ if (type === meta.budgetType) {
316
+ return;
317
+ }
318
+
319
+ meta.budgetType = type;
320
+ meta.createdMonths = new Set();
321
+
322
+ // Go through and force all the cells to be recomputed
323
+ const nodes = sheet.get().getNodes();
324
+ db.transaction(() => {
325
+ for (const name of nodes.keys()) {
326
+ const [sheetName, cellName] = name.split('!');
327
+ if (sheetName.match(/^budget\d+/)) {
328
+ sheet.get().deleteCell(sheetName, cellName);
329
+ }
330
+ }
331
+ });
332
+
333
+ sheet.get().startCacheBarrier();
334
+ void sheet.loadUserBudgets(db);
335
+ const bounds = await createAllBudgets();
336
+ sheet.get().endCacheBarrier();
337
+
338
+ return bounds;
339
+ }