@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,29 @@
1
+ #!/usr/bin/env node
2
+ // @ts-strict-ignore
3
+ import * as fs from 'fs';
4
+
5
+ import { logger } from '../platform/server/log';
6
+
7
+ import * as db from './db';
8
+
9
+ const queries = fs
10
+ .readFileSync(__dirname + '/../../src/server/slow-queries.txt', 'utf8')
11
+ .split('___BOUNDARY')
12
+ .map(q => q.trim());
13
+
14
+ function runQueries() {
15
+ for (let i = 0; i < queries.length; i++) {
16
+ if (queries[i] !== '') {
17
+ db.runQuery(queries[i], [], true);
18
+ }
19
+ }
20
+ }
21
+
22
+ async function run() {
23
+ await db.openDatabase();
24
+ const start = Date.now();
25
+ runQueries();
26
+ logger.log(Date.now() - start);
27
+ }
28
+
29
+ void run();
@@ -0,0 +1,686 @@
1
+ // @ts-strict-ignore
2
+
3
+ import { getCurrency } from '#shared/currencies';
4
+ import * as asyncStorage from '../../platform/server/asyncStorage';
5
+ import { getLocale } from '../../shared/locale';
6
+ import * as monthUtils from '../../shared/months';
7
+ import { integerToCurrency, safeNumber } from '../../shared/util';
8
+ import type { IntegerAmount } from '../../shared/util';
9
+ import type { CategoryEntity } from '../../types/models';
10
+ import * as db from '../db';
11
+ import * as sheet from '../sheet';
12
+ import { batchMessages } from '../sync';
13
+
14
+ export async function getSheetValue(
15
+ sheetName: string,
16
+ cell: string,
17
+ ): Promise<number> {
18
+ const node = sheet.getCell(sheetName, cell);
19
+ return safeNumber(typeof node.value === 'number' ? node.value : 0);
20
+ }
21
+
22
+ export async function getSheetBoolean(
23
+ sheetName: string,
24
+ cell: string,
25
+ ): Promise<boolean> {
26
+ const node = sheet.getCell(sheetName, cell);
27
+ return typeof node.value === 'boolean' ? node.value : false;
28
+ }
29
+
30
+ // We want to only allow the positive movement of money back and
31
+ // forth. buffered should never be allowed to go into the negative,
32
+ // and you shouldn't be allowed to pull non-existent money from
33
+ // leftover.
34
+ function calcBufferedAmount(
35
+ toBudget: number,
36
+ buffered: number,
37
+ amount: number,
38
+ ): number {
39
+ amount = Math.min(Math.max(amount, -buffered), Math.max(toBudget, 0));
40
+ return buffered + amount;
41
+ }
42
+
43
+ type BudgetTable = 'reflect_budgets' | 'zero_budgets';
44
+
45
+ function getBudgetTable(): BudgetTable {
46
+ return isReflectBudget() ? 'reflect_budgets' : 'zero_budgets';
47
+ }
48
+
49
+ export function isReflectBudget(): boolean {
50
+ const budgetType = db.firstSync<Pick<db.DbPreference, 'value'>>(
51
+ `SELECT value FROM preferences WHERE id = ?`,
52
+ ['budgetType'],
53
+ );
54
+ const val = budgetType ? budgetType.value : 'envelope';
55
+ return val === 'tracking';
56
+ }
57
+
58
+ function dbMonth(month: string): number {
59
+ return parseInt(month.replace('-', ''));
60
+ }
61
+
62
+ // TODO: complete list of fields.
63
+ type BudgetData = {
64
+ is_income: 1 | 0;
65
+ hidden: 1 | 0;
66
+ group_hidden: 1 | 0;
67
+ category: string;
68
+ amount: number;
69
+ };
70
+
71
+ function getBudgetData<T extends BudgetTable>(
72
+ table: T,
73
+ month: string,
74
+ ): Promise<BudgetData[]> {
75
+ return db.all<
76
+ (db.DbReflectBudget | db.DbZeroBudget) &
77
+ Pick<
78
+ db.DbViewCategoryWithGroupHidden,
79
+ 'is_income' | 'hidden' | 'group_hidden'
80
+ >
81
+ >(
82
+ `
83
+ SELECT b.*, c.is_income, c.hidden, g.hidden AS group_hidden
84
+ FROM ${table} b
85
+ LEFT JOIN categories c ON b.category = c.id
86
+ LEFT JOIN category_groups g ON c.cat_group = g.id
87
+ WHERE c.tombstone = 0 AND b.month = ?
88
+ `,
89
+ [month],
90
+ );
91
+ }
92
+
93
+ function getAllMonths(startMonth: string): string[] {
94
+ const { createdMonths } = sheet.get().meta();
95
+ let latest = null;
96
+ for (const month of createdMonths) {
97
+ if (latest == null || month > latest) {
98
+ latest = month;
99
+ }
100
+ }
101
+ return monthUtils.rangeInclusive(startMonth, latest);
102
+ }
103
+
104
+ // TODO: Valid month format in all the functions below
105
+
106
+ export function getBudget({
107
+ category,
108
+ month,
109
+ }: {
110
+ category: string;
111
+ month: string;
112
+ }): number {
113
+ const table = getBudgetTable();
114
+ const existing = db.firstSync<db.DbZeroBudget | db.DbReflectBudget>(
115
+ `SELECT * FROM ${table} WHERE month = ? AND category = ?`,
116
+ [dbMonth(month), category],
117
+ );
118
+ return existing ? existing.amount || 0 : 0;
119
+ }
120
+
121
+ export function setBudget({
122
+ category,
123
+ month,
124
+ amount,
125
+ }: {
126
+ category: CategoryEntity['id'];
127
+ month: string;
128
+ amount: unknown;
129
+ }): Promise<void> {
130
+ amount = safeNumber(typeof amount === 'number' ? amount : 0);
131
+ const table = getBudgetTable();
132
+
133
+ const existing = db.firstSync<
134
+ Pick<db.DbZeroBudget | db.DbReflectBudget, 'id'>
135
+ >(`SELECT id FROM ${table} WHERE month = ? AND category = ?`, [
136
+ dbMonth(month),
137
+ category,
138
+ ]);
139
+ if (existing) {
140
+ return db.update(table, { id: existing.id, amount });
141
+ }
142
+ return db.insert(table, {
143
+ id: `${dbMonth(month)}-${category}`,
144
+ month: dbMonth(month),
145
+ category,
146
+ amount,
147
+ });
148
+ }
149
+
150
+ export function setGoal({ month, category, goal, long_goal }): Promise<void> {
151
+ const table = getBudgetTable();
152
+ const existing = db.firstSync<
153
+ Pick<db.DbZeroBudget | db.DbReflectBudget, 'id'>
154
+ >(`SELECT id FROM ${table} WHERE month = ? AND category = ?`, [
155
+ dbMonth(month),
156
+ category,
157
+ ]);
158
+ if (existing) {
159
+ return db.update(table, {
160
+ id: existing.id,
161
+ goal,
162
+ long_goal,
163
+ });
164
+ }
165
+ return db.insert(table, {
166
+ id: `${dbMonth(month)}-${category}`,
167
+ month: dbMonth(month),
168
+ category,
169
+ goal,
170
+ long_goal,
171
+ });
172
+ }
173
+
174
+ export function setBuffer(month: string, amount: unknown): Promise<void> {
175
+ const existing = db.firstSync<Pick<db.DbZeroBudget, 'id'>>(
176
+ `SELECT id FROM zero_budget_months WHERE id = ?`,
177
+ [month],
178
+ );
179
+ if (existing) {
180
+ return db.update('zero_budget_months', {
181
+ id: existing.id,
182
+ buffered: amount,
183
+ });
184
+ }
185
+ return db.insert('zero_budget_months', { id: month, buffered: amount });
186
+ }
187
+
188
+ function setCarryover(
189
+ table: string,
190
+ category: string,
191
+ month: string,
192
+ flag: boolean,
193
+ ): Promise<void> {
194
+ const existing = db.firstSync<
195
+ Pick<db.DbZeroBudget | db.DbReflectBudget, 'id'>
196
+ >(`SELECT id FROM ${table} WHERE month = ? AND category = ?`, [
197
+ month,
198
+ category,
199
+ ]);
200
+ if (existing) {
201
+ return db.update(table, { id: existing.id, carryover: flag ? 1 : 0 });
202
+ }
203
+ return db.insert(table, {
204
+ id: `${month}-${category}`,
205
+ month,
206
+ category,
207
+ carryover: flag ? 1 : 0,
208
+ });
209
+ }
210
+
211
+ // Actions
212
+
213
+ export async function copyPreviousMonth({
214
+ month,
215
+ }: {
216
+ month: string;
217
+ }): Promise<void> {
218
+ const prevMonth = dbMonth(monthUtils.prevMonth(month));
219
+ const table = getBudgetTable();
220
+ const budgetData = await getBudgetData(table, prevMonth.toString());
221
+
222
+ await batchMessages(async () => {
223
+ budgetData.forEach(prevBudget => {
224
+ if (prevBudget.is_income === 1 && !isReflectBudget()) {
225
+ return;
226
+ }
227
+ if (prevBudget.hidden === 1 || prevBudget.group_hidden === 1) {
228
+ return;
229
+ }
230
+ void setBudget({
231
+ category: prevBudget.category,
232
+ month,
233
+ amount: prevBudget.amount,
234
+ });
235
+ });
236
+ });
237
+ }
238
+
239
+ export async function copySinglePreviousMonth({
240
+ month,
241
+ category,
242
+ }: {
243
+ month: string;
244
+ category: string;
245
+ }): Promise<void> {
246
+ const prevMonth = monthUtils.prevMonth(month);
247
+ const newAmount = await getSheetValue(
248
+ monthUtils.sheetForMonth(prevMonth),
249
+ 'budget-' + category,
250
+ );
251
+ await batchMessages(async () => {
252
+ void setBudget({ category, month, amount: newAmount });
253
+ });
254
+ }
255
+
256
+ export async function setZero({ month }: { month: string }): Promise<void> {
257
+ const categories = await db.all<db.DbViewCategory>(
258
+ 'SELECT * FROM v_categories WHERE tombstone = 0',
259
+ );
260
+
261
+ await batchMessages(async () => {
262
+ categories.forEach(cat => {
263
+ if (cat.is_income === 1 && !isReflectBudget()) {
264
+ return;
265
+ }
266
+ void setBudget({ category: cat.id, month, amount: 0 });
267
+ });
268
+ });
269
+ }
270
+
271
+ export async function set3MonthAvg({
272
+ month,
273
+ }: {
274
+ month: string;
275
+ }): Promise<void> {
276
+ const categories = await db.all<db.DbViewCategoryWithGroupHidden>(
277
+ `
278
+ SELECT c.*
279
+ FROM categories c
280
+ LEFT JOIN category_groups g ON c.cat_group = g.id
281
+ WHERE c.tombstone = 0 AND c.hidden = 0 AND g.hidden = 0
282
+ `,
283
+ );
284
+
285
+ const prevMonth1 = monthUtils.prevMonth(month);
286
+ const prevMonth2 = monthUtils.prevMonth(prevMonth1);
287
+ const prevMonth3 = monthUtils.prevMonth(prevMonth2);
288
+
289
+ await batchMessages(async () => {
290
+ for (const cat of categories) {
291
+ if (cat.is_income === 1 && !isReflectBudget()) {
292
+ continue;
293
+ }
294
+
295
+ const spent1 = await getSheetValue(
296
+ monthUtils.sheetForMonth(prevMonth1),
297
+ 'sum-amount-' + cat.id,
298
+ );
299
+ const spent2 = await getSheetValue(
300
+ monthUtils.sheetForMonth(prevMonth2),
301
+ 'sum-amount-' + cat.id,
302
+ );
303
+ const spent3 = await getSheetValue(
304
+ monthUtils.sheetForMonth(prevMonth3),
305
+ 'sum-amount-' + cat.id,
306
+ );
307
+
308
+ let avg = Math.round((spent1 + spent2 + spent3) / 3);
309
+
310
+ if (cat.is_income === 0) {
311
+ avg *= -1;
312
+ }
313
+
314
+ void setBudget({ category: cat.id, month, amount: avg });
315
+ }
316
+ });
317
+ }
318
+
319
+ export async function set12MonthAvg({
320
+ month,
321
+ }: {
322
+ month: string;
323
+ }): Promise<void> {
324
+ const categories = await db.all<db.DbViewCategoryWithGroupHidden>(
325
+ `
326
+ SELECT c.*
327
+ FROM categories c
328
+ LEFT JOIN category_groups g ON c.cat_group = g.id
329
+ WHERE c.tombstone = 0 AND c.hidden = 0 AND g.hidden = 0
330
+ `,
331
+ );
332
+
333
+ await batchMessages(async () => {
334
+ for (const cat of categories) {
335
+ if (cat.is_income === 1 && !isReflectBudget()) {
336
+ continue;
337
+ }
338
+ void setNMonthAvg({ month, N: 12, category: cat.id });
339
+ }
340
+ });
341
+ }
342
+
343
+ export async function set6MonthAvg({
344
+ month,
345
+ }: {
346
+ month: string;
347
+ }): Promise<void> {
348
+ const categories = await db.all<db.DbViewCategoryWithGroupHidden>(
349
+ `
350
+ SELECT c.*
351
+ FROM categories c
352
+ LEFT JOIN category_groups g ON c.cat_group = g.id
353
+ WHERE c.tombstone = 0 AND c.hidden = 0 AND g.hidden = 0
354
+ `,
355
+ );
356
+
357
+ await batchMessages(async () => {
358
+ for (const cat of categories) {
359
+ if (cat.is_income === 1 && !isReflectBudget()) {
360
+ continue;
361
+ }
362
+ void setNMonthAvg({ month, N: 6, category: cat.id });
363
+ }
364
+ });
365
+ }
366
+
367
+ export async function setNMonthAvg({
368
+ month,
369
+ N,
370
+ category,
371
+ }: {
372
+ month: string;
373
+ N: number;
374
+ category: string;
375
+ }): Promise<void> {
376
+ const categoryFromDb = await db.first<Pick<db.DbViewCategory, 'is_income'>>(
377
+ 'SELECT is_income FROM v_categories WHERE id = ?',
378
+ [category],
379
+ );
380
+
381
+ let prevMonth = monthUtils.prevMonth(month);
382
+ let sumAmount = 0;
383
+ for (let l = 0; l < N; l++) {
384
+ sumAmount += await getSheetValue(
385
+ monthUtils.sheetForMonth(prevMonth),
386
+ 'sum-amount-' + category,
387
+ );
388
+ prevMonth = monthUtils.prevMonth(prevMonth);
389
+ }
390
+ await batchMessages(async () => {
391
+ let avg = Math.round(sumAmount / N);
392
+
393
+ if (categoryFromDb.is_income === 0) {
394
+ avg *= -1;
395
+ }
396
+
397
+ void setBudget({ category, month, amount: avg });
398
+ });
399
+ }
400
+
401
+ export async function holdForNextMonth({
402
+ month,
403
+ amount,
404
+ }: {
405
+ month: string;
406
+ amount: number;
407
+ }): Promise<boolean> {
408
+ const row = await db.first<Pick<db.DbZeroBudgetMonth, 'buffered'>>(
409
+ 'SELECT buffered FROM zero_budget_months WHERE id = ?',
410
+ [month],
411
+ );
412
+
413
+ const sheetName = monthUtils.sheetForMonth(month);
414
+ const toBudget = await getSheetValue(sheetName, 'to-budget');
415
+
416
+ if (toBudget > 0) {
417
+ const bufferedAmount = calcBufferedAmount(
418
+ toBudget,
419
+ (row && row.buffered) || 0,
420
+ amount,
421
+ );
422
+
423
+ await setBuffer(month, bufferedAmount);
424
+ return true;
425
+ }
426
+ return false;
427
+ }
428
+
429
+ export async function resetHold({ month }: { month: string }): Promise<void> {
430
+ await setBuffer(month, 0);
431
+ }
432
+
433
+ export async function coverOverspending({
434
+ month,
435
+ to,
436
+ from,
437
+ amount,
438
+ currencyCode,
439
+ }: {
440
+ month: string;
441
+ to: CategoryEntity['id'] | 'to-budget';
442
+ from: CategoryEntity['id'] | 'to-budget' | 'overbudgeted';
443
+ amount?: IntegerAmount;
444
+ currencyCode: string;
445
+ }): Promise<void> {
446
+ const sheetName = monthUtils.sheetForMonth(month);
447
+ const toBudgeted = await getSheetValue(sheetName, 'budget-' + to);
448
+ const leftoverFrom = await getSheetValue(
449
+ sheetName,
450
+ from === 'to-budget' ? 'to-budget' : 'leftover-' + from,
451
+ );
452
+
453
+ // Cover provided amount (can be partial) or full overspending amount.
454
+ const amountToCover = amount
455
+ ? // Covering in the app provides a positive amount to cover so we invert it here
456
+ -amount
457
+ : await getSheetValue(sheetName, 'leftover-' + to);
458
+
459
+ if (amountToCover >= 0 || leftoverFrom <= 0) {
460
+ return;
461
+ }
462
+
463
+ // Don't go over the leftover amount of the covering category
464
+ const coverableAmount = Math.min(Math.abs(amountToCover), leftoverFrom);
465
+
466
+ await batchMessages(async () => {
467
+ // If we are covering it from the to be budgeted amount, ignore this
468
+ if (from !== 'to-budget') {
469
+ const fromBudgeted = await getSheetValue(sheetName, 'budget-' + from);
470
+ await setBudget({
471
+ category: from,
472
+ month,
473
+ amount: fromBudgeted - coverableAmount,
474
+ });
475
+ }
476
+
477
+ await setBudget({
478
+ category: to,
479
+ month,
480
+ amount: toBudgeted + coverableAmount,
481
+ });
482
+
483
+ await addMovementNotes({
484
+ month,
485
+ amount: coverableAmount,
486
+ to,
487
+ from,
488
+ currencyCode,
489
+ });
490
+ });
491
+ }
492
+
493
+ export async function transferAvailable({
494
+ month,
495
+ amount,
496
+ category,
497
+ }: {
498
+ month: string;
499
+ amount: number;
500
+ category: string;
501
+ }): Promise<void> {
502
+ const sheetName = monthUtils.sheetForMonth(month);
503
+ const leftover = await getSheetValue(sheetName, 'to-budget');
504
+ amount = Math.max(Math.min(amount, leftover), 0);
505
+
506
+ const budgeted = await getSheetValue(sheetName, 'budget-' + category);
507
+ await setBudget({ category, month, amount: budgeted + amount });
508
+ }
509
+
510
+ export async function coverOverbudgeted({
511
+ month,
512
+ category,
513
+ amount,
514
+ currencyCode,
515
+ }: {
516
+ month: string;
517
+ category: string;
518
+ amount?: IntegerAmount;
519
+ currencyCode: string;
520
+ }): Promise<void> {
521
+ const sheetName = monthUtils.sheetForMonth(month);
522
+ const categoryBudget = await getSheetValue(sheetName, 'budget-' + category);
523
+
524
+ // Cover provided amount (can be partial) or full overbudgeted amount.
525
+ const amountToCover = amount
526
+ ? // Covering in the app provides a positive amount to cover so we invert it here
527
+ -amount
528
+ : await getSheetValue(sheetName, 'to-budget');
529
+
530
+ if (amountToCover >= 0 || categoryBudget <= 0) {
531
+ return;
532
+ }
533
+
534
+ // Don't allow the budget of the covering category to go negative.
535
+ const coverableAmount = Math.min(Math.abs(amountToCover), categoryBudget);
536
+
537
+ await batchMessages(async () => {
538
+ await setBudget({
539
+ category,
540
+ month,
541
+ amount: categoryBudget - coverableAmount,
542
+ });
543
+
544
+ await addMovementNotes({
545
+ month,
546
+ amount: coverableAmount,
547
+ from: category,
548
+ to: 'overbudgeted',
549
+ currencyCode,
550
+ });
551
+ });
552
+ }
553
+
554
+ export async function transferCategory({
555
+ month,
556
+ amount,
557
+ from,
558
+ to,
559
+ currencyCode,
560
+ }: {
561
+ month: string;
562
+ amount: number;
563
+ to: CategoryEntity['id'] | 'to-budget';
564
+ from: CategoryEntity['id'] | 'to-budget';
565
+ currencyCode: string;
566
+ }): Promise<void> {
567
+ const sheetName = monthUtils.sheetForMonth(month);
568
+ const fromBudgeted = await getSheetValue(sheetName, 'budget-' + from);
569
+
570
+ await batchMessages(async () => {
571
+ await setBudget({ category: from, month, amount: fromBudgeted - amount });
572
+
573
+ // If we are simply moving it back into available cash to budget,
574
+ // don't do anything else
575
+ if (to !== 'to-budget') {
576
+ const toBudgeted = await getSheetValue(sheetName, 'budget-' + to);
577
+ await setBudget({ category: to, month, amount: toBudgeted + amount });
578
+ }
579
+
580
+ await addMovementNotes({
581
+ month,
582
+ amount,
583
+ to,
584
+ from,
585
+ currencyCode,
586
+ });
587
+ });
588
+ }
589
+
590
+ export async function setCategoryCarryover({
591
+ startMonth,
592
+ category,
593
+ flag,
594
+ }: {
595
+ startMonth: string;
596
+ category: string;
597
+ flag: boolean;
598
+ }): Promise<void> {
599
+ const table = getBudgetTable();
600
+ const months = getAllMonths(startMonth);
601
+
602
+ await batchMessages(async () => {
603
+ for (const month of months) {
604
+ void setCarryover(table, category, dbMonth(month).toString(), flag);
605
+ }
606
+ });
607
+ }
608
+
609
+ function addNewLine(notes?: string) {
610
+ return !notes ? '' : `${notes}${notes && '\n'}`;
611
+ }
612
+
613
+ async function addMovementNotes({
614
+ month,
615
+ amount,
616
+ to,
617
+ from,
618
+ currencyCode,
619
+ }: {
620
+ month: string;
621
+ amount: number;
622
+ to: CategoryEntity['id'] | 'to-budget' | 'overbudgeted';
623
+ from: CategoryEntity['id'] | 'to-budget';
624
+ currencyCode: string;
625
+ }) {
626
+ const currency = getCurrency(currencyCode);
627
+ const displayAmount = integerToCurrency(
628
+ amount,
629
+ undefined,
630
+ currency.decimalPlaces,
631
+ );
632
+
633
+ const monthBudgetNotesId = `budget-${month}`;
634
+ const existingMonthBudgetNotes = addNewLine(
635
+ db.firstSync<Pick<db.DbNote, 'note'>>(
636
+ `SELECT n.note FROM notes n WHERE n.id = ?`,
637
+ [monthBudgetNotesId],
638
+ )?.note,
639
+ );
640
+
641
+ const locale = getLocale(await asyncStorage.getItem('language'));
642
+ const displayDay = monthUtils.format(
643
+ monthUtils.currentDate(),
644
+ 'MMMM dd',
645
+ locale,
646
+ );
647
+ const categories = await db.getCategories(
648
+ [from, to].filter(c => c !== 'to-budget' && c !== 'overbudgeted'),
649
+ );
650
+
651
+ const fromCategoryName =
652
+ from === 'to-budget'
653
+ ? 'To Budget'
654
+ : categories.find(c => c.id === from)?.name;
655
+
656
+ const toCategoryName =
657
+ to === 'to-budget'
658
+ ? 'To Budget'
659
+ : to === 'overbudgeted'
660
+ ? 'Overbudgeted'
661
+ : categories.find(c => c.id === to)?.name;
662
+
663
+ const note = `Reassigned ${displayAmount} from ${fromCategoryName} → ${toCategoryName} on ${displayDay}`;
664
+
665
+ await db.update('notes', {
666
+ id: monthBudgetNotesId,
667
+ note: `${existingMonthBudgetNotes}- ${note}`,
668
+ });
669
+ }
670
+
671
+ export async function resetIncomeCarryover({
672
+ month,
673
+ }: {
674
+ month: string;
675
+ }): Promise<void> {
676
+ const table = getBudgetTable();
677
+ const categories = await db.all<db.DbViewCategory>(
678
+ 'SELECT * FROM v_categories WHERE is_income = 1 AND tombstone = 0',
679
+ );
680
+
681
+ await batchMessages(async () => {
682
+ for (const category of categories) {
683
+ await setCarryover(table, category.id, dbMonth(month).toString(), false);
684
+ }
685
+ });
686
+ }