@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,309 @@
1
+ // @ts-strict-ignore
2
+ import * as monthUtils from '../../shared/months';
3
+ import { q } from '../../shared/query';
4
+ import type { CategoryEntity, CategoryGroupEntity } from '../../types/models';
5
+ import type { Template } from '../../types/models/templates';
6
+ import { aqlQuery } from '../aql';
7
+ import * as db from '../db';
8
+ import { batchMessages } from '../sync';
9
+
10
+ import { getSheetValue, isReflectBudget, setBudget, setGoal } from './actions';
11
+ import { CategoryTemplateContext } from './category-template-context';
12
+ import { checkTemplateNotes, storeNoteTemplates } from './template-notes';
13
+
14
+ type Notification = {
15
+ type?: 'message' | 'error' | 'warning' | undefined;
16
+ pre?: string | undefined;
17
+ title?: string | undefined;
18
+ message: string;
19
+ sticky?: boolean | undefined;
20
+ };
21
+
22
+ export async function storeTemplates({
23
+ categoriesWithTemplates,
24
+ source,
25
+ }: {
26
+ categoriesWithTemplates: {
27
+ id: string;
28
+ templates: Template[];
29
+ }[];
30
+ source: 'notes' | 'ui';
31
+ }): Promise<void> {
32
+ await batchMessages(async () => {
33
+ for (const { id, templates } of categoriesWithTemplates) {
34
+ const goalDefs = JSON.stringify(templates);
35
+
36
+ await db.updateWithSchema('categories', {
37
+ id,
38
+ goal_def: goalDefs,
39
+ template_settings: { source },
40
+ });
41
+ }
42
+ });
43
+ }
44
+
45
+ export async function applyTemplate({
46
+ month,
47
+ }: {
48
+ month: string;
49
+ }): Promise<Notification> {
50
+ await storeNoteTemplates();
51
+ const categoryTemplates = await getTemplates();
52
+ const ret = await processTemplate(month, false, categoryTemplates, []);
53
+ return ret;
54
+ }
55
+
56
+ export async function overwriteTemplate({
57
+ month,
58
+ }: {
59
+ month: string;
60
+ }): Promise<Notification> {
61
+ await storeNoteTemplates();
62
+ const categoryTemplates = await getTemplates();
63
+ const ret = await processTemplate(month, true, categoryTemplates, []);
64
+ return ret;
65
+ }
66
+
67
+ export async function applyMultipleCategoryTemplates({
68
+ month,
69
+ categoryIds,
70
+ }: {
71
+ month: string;
72
+ categoryIds: Array<CategoryEntity['id']>;
73
+ }) {
74
+ const { data: categoryData }: { data: CategoryEntity[] } = await aqlQuery(
75
+ q('categories')
76
+ .filter({ id: { $oneof: categoryIds } })
77
+ .select('*'),
78
+ );
79
+ await storeNoteTemplates();
80
+ const categoryTemplates = await getTemplates(c => categoryIds.includes(c.id));
81
+ const ret = await processTemplate(
82
+ month,
83
+ true,
84
+ categoryTemplates,
85
+ categoryData,
86
+ );
87
+ return ret;
88
+ }
89
+
90
+ export async function applySingleCategoryTemplate({
91
+ month,
92
+ category,
93
+ }: {
94
+ month: string;
95
+ category: CategoryEntity['id'];
96
+ }) {
97
+ const { data: categoryData }: { data: CategoryEntity[] } = await aqlQuery(
98
+ q('categories').filter({ id: category }).select('*'),
99
+ );
100
+ await storeNoteTemplates();
101
+ const categoryTemplates = await getTemplates(c => c.id === category);
102
+ const ret = await processTemplate(
103
+ month,
104
+ true,
105
+ categoryTemplates,
106
+ categoryData,
107
+ );
108
+ return ret;
109
+ }
110
+
111
+ export function runCheckTemplates() {
112
+ return checkTemplateNotes();
113
+ }
114
+
115
+ async function getCategories(): Promise<CategoryEntity[]> {
116
+ const { data: categoryGroups }: { data: CategoryGroupEntity[] } =
117
+ await aqlQuery(q('category_groups').filter({ hidden: false }).select('*'));
118
+
119
+ return categoryGroups.flatMap(g => g.categories || []).filter(c => !c.hidden);
120
+ }
121
+
122
+ async function getTemplates(
123
+ filter: (category: CategoryEntity) => boolean = () => true,
124
+ ): Promise<Record<CategoryEntity['id'], Template[]>> {
125
+ //retrieves template definitions from the database
126
+ const { data: categoriesWithGoalDef }: { data: CategoryEntity[] } =
127
+ await aqlQuery(
128
+ q('categories')
129
+ .filter({ goal_def: { $ne: null } })
130
+ .select('*'),
131
+ );
132
+
133
+ const categoryTemplates: Record<CategoryEntity['id'], Template[]> = {};
134
+ for (const categoryWithGoalDef of categoriesWithGoalDef.filter(filter)) {
135
+ categoryTemplates[categoryWithGoalDef.id] = JSON.parse(
136
+ categoryWithGoalDef.goal_def,
137
+ );
138
+ }
139
+ return categoryTemplates;
140
+ }
141
+
142
+ export async function getTemplatesForCategory(
143
+ categoryId: CategoryEntity['id'],
144
+ ): Promise<Record<CategoryEntity['id'], Template[]>> {
145
+ return getTemplates(c => c.id === categoryId);
146
+ }
147
+
148
+ type TemplateBudget = {
149
+ category: CategoryEntity['id'];
150
+ budgeted: number;
151
+ };
152
+
153
+ async function setBudgets(month: string, templateBudget: TemplateBudget[]) {
154
+ await batchMessages(async () => {
155
+ templateBudget.forEach(element => {
156
+ void setBudget({
157
+ category: element.category,
158
+ month,
159
+ amount: element.budgeted,
160
+ });
161
+ });
162
+ });
163
+ }
164
+
165
+ type TemplateGoal = {
166
+ category: CategoryEntity['id'];
167
+ goal: number | null;
168
+ longGoal: number | null;
169
+ };
170
+
171
+ async function setGoals(month: string, templateGoal: TemplateGoal[]) {
172
+ await batchMessages(async () => {
173
+ templateGoal.forEach(element => {
174
+ void setGoal({
175
+ month,
176
+ category: element.category,
177
+ goal: element.goal,
178
+ long_goal: element.longGoal,
179
+ });
180
+ });
181
+ });
182
+ }
183
+
184
+ async function processTemplate(
185
+ month: string,
186
+ force: boolean,
187
+ categoryTemplates: Record<CategoryEntity['id'], Template[]>,
188
+ categories: CategoryEntity[] = [],
189
+ ): Promise<Notification> {
190
+ // setup categories
191
+ const isReflect = isReflectBudget();
192
+ if (!categories.length) {
193
+ categories = (await getCategories()).filter(c => isReflect || !c.is_income);
194
+ }
195
+
196
+ // setup categories to process
197
+ const templateContexts: CategoryTemplateContext[] = [];
198
+ let availBudget = await getSheetValue(
199
+ monthUtils.sheetForMonth(month),
200
+ `to-budget`,
201
+ );
202
+ const prioritiesSet = new Set<number>();
203
+ const errors: string[] = [];
204
+ const budgetList: TemplateBudget[] = [];
205
+ const goalList: TemplateGoal[] = [];
206
+ for (const category of categories) {
207
+ const { id } = category;
208
+ const sheetName = monthUtils.sheetForMonth(month);
209
+ const templates = categoryTemplates[id];
210
+ const budgeted = await getSheetValue(sheetName, `budget-${id}`);
211
+ const existingGoal = await getSheetValue(sheetName, `goal-${id}`);
212
+
213
+ // only run categories that are unbudgeted or if we are forcing it
214
+ if ((budgeted === 0 || force) && templates) {
215
+ try {
216
+ const templateContext = await CategoryTemplateContext.init(
217
+ templates,
218
+ category,
219
+ month,
220
+ budgeted,
221
+ );
222
+ // don't use the funds that are not from templates
223
+ if (!templateContext.isGoalOnly()) {
224
+ availBudget += budgeted;
225
+ }
226
+ availBudget += templateContext.getLimitExcess();
227
+ templateContext.getPriorities().forEach(p => prioritiesSet.add(p));
228
+ templateContexts.push(templateContext);
229
+ } catch (e) {
230
+ errors.push(`${category.name}: ${e.message}`);
231
+ }
232
+
233
+ // do a reset of the goals that are orphaned
234
+ } else if (existingGoal !== null && !templates) {
235
+ goalList.push({
236
+ category: id,
237
+ goal: null,
238
+ longGoal: null,
239
+ });
240
+ }
241
+ }
242
+
243
+ //break early if nothing to do, or there are errors
244
+ if (templateContexts.length === 0 && errors.length === 0) {
245
+ if (goalList.length > 0) {
246
+ void setGoals(month, goalList);
247
+ }
248
+ return {
249
+ type: 'message',
250
+ message: 'Everything is up to date',
251
+ };
252
+ }
253
+ if (errors.length > 0) {
254
+ return {
255
+ sticky: true,
256
+ message: 'There were errors interpreting some templates:',
257
+ pre: errors.join(`\n\n`),
258
+ };
259
+ }
260
+
261
+ const priorities = new Int32Array([...prioritiesSet]).sort((a, b) => a - b);
262
+ // run each priority level
263
+ for (const priority of priorities) {
264
+ const availStart = availBudget;
265
+ for (const templateContext of templateContexts) {
266
+ const budget = await templateContext.runTemplatesForPriority(
267
+ priority,
268
+ availBudget,
269
+ availStart,
270
+ );
271
+ availBudget -= budget;
272
+ }
273
+ }
274
+
275
+ // run remainder
276
+ let remainderContexts = templateContexts.filter(c => c.hasRemainder());
277
+ while (availBudget > 0 && remainderContexts.length > 0) {
278
+ let remainderWeight = 0;
279
+ remainderContexts.forEach(
280
+ context => (remainderWeight += context.getRemainderWeight()),
281
+ );
282
+ const perWeight = availBudget / remainderWeight;
283
+ remainderContexts.forEach(context => {
284
+ availBudget -= context.runRemainder(availBudget, perWeight);
285
+ });
286
+ remainderContexts = templateContexts.filter(c => c.hasRemainder());
287
+ }
288
+
289
+ // finish
290
+ templateContexts.forEach(context => {
291
+ const values = context.getValues();
292
+ budgetList.push({
293
+ category: context.category.id,
294
+ budgeted: values.budgeted,
295
+ });
296
+ goalList.push({
297
+ category: context.category.id,
298
+ goal: values.goal,
299
+ longGoal: values.longGoal ? 1 : null,
300
+ });
301
+ });
302
+ await setBudgets(month, budgetList);
303
+ await setGoals(month, goalList);
304
+
305
+ return {
306
+ type: 'message',
307
+ message: `Successfully applied templates to ${templateContexts.length} categories`,
308
+ };
309
+ }
@@ -0,0 +1,308 @@
1
+ // @ts-strict-ignore
2
+ import * as monthUtils from '../../shared/months';
3
+ import { safeNumber } from '../../shared/util';
4
+ import * as db from '../db';
5
+ import * as sheet from '../sheet';
6
+ import { resolveName } from '../spreadsheet/util';
7
+
8
+ import { createCategory as createCategoryFromBase } from './base';
9
+ import { number, sumAmounts } from './util';
10
+
11
+ export async function createCategory(cat, sheetName, prevSheetName) {
12
+ sheet.get().createStatic(sheetName, `budget-${cat.id}`, 0);
13
+
14
+ // This makes the app more robust by "fixing up" null budget values.
15
+ // Those should not be allowed, but in case somehow a null value
16
+ // ends up there, we are resilient to it. Preferrably the
17
+ // spreadsheet would have types and be more strict about what is
18
+ // allowed to be set.
19
+ if (sheet.get().getCellValue(sheetName, `budget-${cat.id}`) == null) {
20
+ sheet.get().set(resolveName(sheetName, `budget-${cat.id}`), 0);
21
+ }
22
+
23
+ sheet.get().createDynamic(sheetName, `leftover-${cat.id}`, {
24
+ initialValue: 0,
25
+ dependencies: [
26
+ `budget-${cat.id}`,
27
+ `sum-amount-${cat.id}`,
28
+ `${prevSheetName}!carryover-${cat.id}`,
29
+ `${prevSheetName}!leftover-${cat.id}`,
30
+ ],
31
+ run: (budgeted, sumAmount, prevCarryover, prevLeftover) => {
32
+ if (cat.is_income) {
33
+ return safeNumber(
34
+ number(budgeted) -
35
+ number(sumAmount) +
36
+ (prevCarryover ? number(prevLeftover) : 0),
37
+ );
38
+ }
39
+
40
+ return safeNumber(
41
+ number(budgeted) +
42
+ number(sumAmount) +
43
+ (prevCarryover ? number(prevLeftover) : 0),
44
+ );
45
+ },
46
+ });
47
+ sheet.get().createDynamic(sheetName, `spent-with-carryover-${cat.id}`, {
48
+ initialValue: 0,
49
+ dependencies: [
50
+ `budget-${cat.id}`,
51
+ `sum-amount-${cat.id}`,
52
+ `carryover-${cat.id}`,
53
+ ],
54
+ // TODO: Why refresh??
55
+ refresh: true,
56
+ run: (budgeted, sumAmount, carryover) => {
57
+ return carryover
58
+ ? Math.max(0, safeNumber(number(budgeted) + number(sumAmount)))
59
+ : sumAmount;
60
+ },
61
+ });
62
+
63
+ sheet.get().createStatic(sheetName, `carryover-${cat.id}`, false);
64
+ }
65
+
66
+ export function createCategoryGroup(group, sheetName) {
67
+ // different sum amount dependencies
68
+ sheet.get().createDynamic(sheetName, 'group-sum-amount-' + group.id, {
69
+ initialValue: 0,
70
+ dependencies: group.categories
71
+ .filter(cat => !cat.hidden)
72
+ .map(cat => `sum-amount-${cat.id}`),
73
+ run: sumAmounts,
74
+ });
75
+ sheet.get().createDynamic(sheetName, 'group-budget-' + group.id, {
76
+ initialValue: 0,
77
+ dependencies: group.categories
78
+ .filter(cat => !cat.hidden)
79
+ .map(cat => `budget-${cat.id}`),
80
+ run: sumAmounts,
81
+ });
82
+ sheet.get().createDynamic(sheetName, 'group-leftover-' + group.id, {
83
+ initialValue: 0,
84
+ dependencies: group.categories
85
+ .filter(cat => !cat.hidden)
86
+ .map(cat => `leftover-${cat.id}`),
87
+ run: sumAmounts,
88
+ });
89
+ }
90
+
91
+ export function createSummary(groups, sheetName) {
92
+ const incomeGroup = groups.filter(group => group.is_income)[0];
93
+ const expenseGroups = groups.filter(
94
+ group => !group.is_income && !group.hidden,
95
+ );
96
+
97
+ sheet.get().createDynamic(sheetName, 'total-budgeted', {
98
+ initialValue: 0,
99
+ dependencies: expenseGroups.map(group => `group-budget-${group.id}`),
100
+ run: sumAmounts,
101
+ });
102
+
103
+ sheet.get().createDynamic(sheetName, 'total-spent', {
104
+ initialValue: 0,
105
+ refresh: true,
106
+ dependencies: expenseGroups.map(group => `group-sum-amount-${group.id}`),
107
+ run: sumAmounts,
108
+ });
109
+
110
+ sheet.get().createDynamic(sheetName, 'total-income', {
111
+ initialValue: 0,
112
+ dependencies: [`group-sum-amount-${incomeGroup.id}`],
113
+ run: amount => amount,
114
+ });
115
+
116
+ sheet.get().createDynamic(sheetName, 'total-leftover', {
117
+ initialValue: 0,
118
+ dependencies: expenseGroups.map(g => `group-leftover-${g.id}`),
119
+ run: sumAmounts,
120
+ });
121
+
122
+ sheet.get().createDynamic(sheetName, 'total-budget-income', {
123
+ initialValue: 0,
124
+ dependencies: [`group-budget-${incomeGroup.id}`],
125
+ run: amount => amount,
126
+ });
127
+
128
+ sheet.get().createDynamic(sheetName, 'total-saved', {
129
+ initialValue: 0,
130
+ dependencies: ['total-budget-income', 'total-budgeted'],
131
+ run: (income, budgeted) => {
132
+ return income - budgeted;
133
+ },
134
+ });
135
+
136
+ sheet.get().createDynamic(sheetName, 'real-saved', {
137
+ initialValue: 0,
138
+ dependencies: ['total-income', 'total-spent'],
139
+ run: (income, spent) => {
140
+ return safeNumber(income - -spent);
141
+ },
142
+ });
143
+ }
144
+
145
+ export function handleCategoryChange(months, oldValue, newValue) {
146
+ function addDeps(sheetName, groupId, catId) {
147
+ sheet
148
+ .get()
149
+ .addDependencies(sheetName, `group-sum-amount-${groupId}`, [
150
+ `sum-amount-${catId}`,
151
+ ]);
152
+ sheet
153
+ .get()
154
+ .addDependencies(sheetName, `group-budget-${groupId}`, [
155
+ `budget-${catId}`,
156
+ ]);
157
+ sheet
158
+ .get()
159
+ .addDependencies(sheetName, `group-leftover-${groupId}`, [
160
+ `leftover-${catId}`,
161
+ ]);
162
+ }
163
+
164
+ function removeDeps(sheetName, groupId, catId) {
165
+ sheet
166
+ .get()
167
+ .removeDependencies(sheetName, `group-sum-amount-${groupId}`, [
168
+ `sum-amount-${catId}`,
169
+ ]);
170
+ sheet
171
+ .get()
172
+ .removeDependencies(sheetName, `group-budget-${groupId}`, [
173
+ `budget-${catId}`,
174
+ ]);
175
+ sheet
176
+ .get()
177
+ .removeDependencies(sheetName, `group-leftover-${groupId}`, [
178
+ `leftover-${catId}`,
179
+ ]);
180
+ }
181
+
182
+ if (oldValue && oldValue.tombstone === 0 && newValue.tombstone === 1) {
183
+ const id = newValue.id;
184
+ const groupId = newValue.cat_group;
185
+
186
+ months.forEach(month => {
187
+ const sheetName = monthUtils.sheetForMonth(month);
188
+ removeDeps(sheetName, groupId, id);
189
+ });
190
+ } else if (
191
+ newValue.tombstone === 0 &&
192
+ (!oldValue || oldValue.tombstone === 1)
193
+ ) {
194
+ months.forEach(month => {
195
+ const prevMonth = monthUtils.prevMonth(month);
196
+ const prevSheetName = monthUtils.sheetForMonth(prevMonth);
197
+ const sheetName = monthUtils.sheetForMonth(month);
198
+ const { start, end } = monthUtils.bounds(month);
199
+
200
+ createCategoryFromBase(newValue, sheetName, prevSheetName, start, end);
201
+
202
+ const id = newValue.id;
203
+ const groupId = newValue.cat_group;
204
+
205
+ addDeps(sheetName, groupId, id);
206
+ });
207
+ } else if (oldValue && oldValue.cat_group !== newValue.cat_group) {
208
+ // The category moved so we need to update the dependencies
209
+ const id = newValue.id;
210
+
211
+ months.forEach(month => {
212
+ const sheetName = monthUtils.sheetForMonth(month);
213
+ removeDeps(sheetName, oldValue.cat_group, id);
214
+ addDeps(sheetName, newValue.cat_group, id);
215
+ });
216
+ } else if (oldValue && oldValue.hidden !== newValue.hidden) {
217
+ const id = newValue.id;
218
+ const groupId = newValue.cat_group;
219
+
220
+ months.forEach(month => {
221
+ const sheetName = monthUtils.sheetForMonth(month);
222
+ if (newValue.hidden) {
223
+ removeDeps(sheetName, groupId, id);
224
+ } else {
225
+ addDeps(sheetName, groupId, id);
226
+ }
227
+ });
228
+ }
229
+ }
230
+
231
+ export function handleCategoryGroupChange(months, oldValue, newValue) {
232
+ function addDeps(sheetName, groupId) {
233
+ sheet
234
+ .get()
235
+ .addDependencies(sheetName, 'total-budgeted', [
236
+ `group-budget-${groupId}`,
237
+ ]);
238
+ sheet
239
+ .get()
240
+ .addDependencies(sheetName, 'total-spent', [
241
+ `group-sum-amount-${groupId}`,
242
+ ]);
243
+ sheet
244
+ .get()
245
+ .addDependencies(sheetName, 'total-leftover', [
246
+ `group-leftover-${groupId}`,
247
+ ]);
248
+ }
249
+
250
+ function removeDeps(sheetName, groupId) {
251
+ sheet
252
+ .get()
253
+ .removeDependencies(sheetName, 'total-budgeted', [
254
+ `group-budget-${groupId}`,
255
+ ]);
256
+ sheet
257
+ .get()
258
+ .removeDependencies(sheetName, 'total-spent', [
259
+ `group-sum-amount-${groupId}`,
260
+ ]);
261
+ sheet
262
+ .get()
263
+ .removeDependencies(sheetName, 'total-leftover', [
264
+ `group-leftover-${groupId}`,
265
+ ]);
266
+ }
267
+
268
+ if (newValue.tombstone === 1 && oldValue && oldValue.tombstone === 0) {
269
+ const id = newValue.id;
270
+ months.forEach(month => {
271
+ const sheetName = monthUtils.sheetForMonth(month);
272
+ removeDeps(sheetName, id);
273
+ });
274
+ } else if (
275
+ newValue.tombstone === 0 &&
276
+ (!oldValue || oldValue.tombstone === 1)
277
+ ) {
278
+ const group = newValue;
279
+
280
+ months.forEach(month => {
281
+ const sheetName = monthUtils.sheetForMonth(month);
282
+
283
+ // Dirty, dirty hack. These functions should not be async, but this is
284
+ // OK because we're leveraging the sync nature of queries. Ideally we
285
+ // wouldn't be querying here. But I think we have to. At least for now
286
+ // we do
287
+ const categories = db.runQuery(
288
+ 'SELECT * FROM categories WHERE tombstone = 0 AND cat_group = ?',
289
+ [group.id],
290
+ true,
291
+ );
292
+ createCategoryGroup({ ...group, categories }, sheetName);
293
+
294
+ addDeps(sheetName, group.id);
295
+ });
296
+ } else if (oldValue && oldValue.hidden !== newValue.hidden) {
297
+ const group = newValue;
298
+
299
+ months.forEach(month => {
300
+ const sheetName = monthUtils.sheetForMonth(month);
301
+ if (newValue.hidden) {
302
+ removeDeps(sheetName, group.id);
303
+ } else {
304
+ addDeps(sheetName, group.id);
305
+ }
306
+ });
307
+ }
308
+ }