@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,862 @@
1
+ import { getCurrency } from '#shared/currencies';
2
+ import type { Currency } from '#shared/currencies';
3
+ import { q } from '#shared/query';
4
+ import * as monthUtils from '../../shared/months';
5
+ import { amountToInteger, integerToAmount } from '../../shared/util';
6
+ import type { CategoryEntity } from '../../types/models';
7
+ import type {
8
+ AverageTemplate,
9
+ ByTemplate,
10
+ CopyTemplate,
11
+ GoalTemplate,
12
+ PercentageTemplate,
13
+ PeriodicTemplate,
14
+ RefillTemplate,
15
+ RemainderTemplate,
16
+ SimpleTemplate,
17
+ SpendTemplate,
18
+ Template,
19
+ } from '../../types/models/templates';
20
+ import { aqlQuery } from '../aql';
21
+ import * as db from '../db';
22
+
23
+ import { getSheetBoolean, getSheetValue, isReflectBudget } from './actions';
24
+ import { runSchedule } from './schedule-template';
25
+ import { getActiveSchedules } from './statements';
26
+
27
+ export class CategoryTemplateContext {
28
+ /*----------------------------------------------------------------------------
29
+ * Using This Class:
30
+ * 1. instantiate via `await categoryTemplate.init(templates, categoryID, month)`;
31
+ * templates: all templates for this category (including templates and goals)
32
+ * categoryID: the ID of the category that this Class will be for
33
+ * month: the month string of the month for templates being applied
34
+ * 2. gather needed data for external use. ex: remainder weights, priorities, limitExcess
35
+ * 3. run each priority level that is needed via runTemplatesForPriority
36
+ * 4. run the remainder templates via runRemainder()
37
+ * 5. finish processing by running getValues() and saving values for batch processing.
38
+ * Alternate:
39
+ * If the situation calls for it you can run all templates in a catagory in one go using the
40
+ * method runAll which will run all templates and goals for reference, and can optionally be saved
41
+ */
42
+
43
+ //-----------------------------------------------------------------------------
44
+ // Class interface
45
+
46
+ // set up the class and check all templates
47
+ static async init(
48
+ templates: Template[],
49
+ category: CategoryEntity,
50
+ month: string,
51
+ budgeted: number,
52
+ ) {
53
+ // get all the needed setup values
54
+ const lastMonthSheet = monthUtils.sheetForMonth(
55
+ monthUtils.subMonths(month, 1),
56
+ );
57
+ let fromLastMonth = await getSheetValue(
58
+ lastMonthSheet,
59
+ `leftover-${category.id}`,
60
+ );
61
+ const carryover = await getSheetBoolean(
62
+ lastMonthSheet,
63
+ `carryover-${category.id}`,
64
+ );
65
+
66
+ if (
67
+ (fromLastMonth < 0 && !carryover) || // overspend no carryover
68
+ category.is_income || // tracking budget income categories
69
+ (isReflectBudget() && !carryover) // tracking budget regular categories
70
+ ) {
71
+ fromLastMonth = 0;
72
+ }
73
+
74
+ // run all checks
75
+ await CategoryTemplateContext.checkByAndScheduleAndSpend(templates, month);
76
+ await CategoryTemplateContext.checkPercentage(templates);
77
+
78
+ const hideDecimal = await aqlQuery(
79
+ q('preferences').filter({ id: 'hideFraction' }).select('*'),
80
+ );
81
+
82
+ const currencyPref = await aqlQuery(
83
+ q('preferences').filter({ id: 'defaultCurrencyCode' }).select('*'),
84
+ );
85
+ const currencyCode =
86
+ currencyPref.data.length > 0 ? currencyPref.data[0].value : '';
87
+
88
+ // call the private constructor
89
+ return new CategoryTemplateContext(
90
+ templates,
91
+ category,
92
+ month,
93
+ fromLastMonth,
94
+ budgeted,
95
+ currencyCode,
96
+ hideDecimal.data.length > 0
97
+ ? hideDecimal.data[0].value === 'true'
98
+ : false,
99
+ );
100
+ }
101
+
102
+ isGoalOnly(): boolean {
103
+ // if there is only a goal
104
+ return (
105
+ this.templates.length === 0 &&
106
+ this.remainder.length === 0 &&
107
+ this.goals.length > 0
108
+ );
109
+ }
110
+ getPriorities(): number[] {
111
+ return Array.from(this.priorities);
112
+ }
113
+ hasRemainder(): boolean {
114
+ return this.remainderWeight > 0 && !this.limitMet;
115
+ }
116
+ getRemainderWeight(): number {
117
+ return this.remainderWeight;
118
+ }
119
+ getLimitExcess(): number {
120
+ return this.limitExcess;
121
+ }
122
+
123
+ // what is the full requested amount this month
124
+ async runAll(available: number) {
125
+ let toBudget: number = 0;
126
+ const prioritiesSorted = new Int32Array(
127
+ [...this.getPriorities()].sort((a, b) => a - b),
128
+ );
129
+ for (let i = 0; i < prioritiesSorted.length; i++) {
130
+ const p = prioritiesSorted[i];
131
+ toBudget += await this.runTemplatesForPriority(p, available, available);
132
+ }
133
+ return toBudget;
134
+ }
135
+
136
+ // run all templates in a given priority level
137
+ // return: amount budgeted in this priority level
138
+ async runTemplatesForPriority(
139
+ priority: number,
140
+ budgetAvail: number,
141
+ availStart: number,
142
+ ): Promise<number> {
143
+ if (!this.priorities.has(priority)) return 0;
144
+ if (this.limitMet) return 0;
145
+
146
+ const t = this.templates.filter(
147
+ t => t.directive === 'template' && t.priority === priority,
148
+ );
149
+ let available = budgetAvail || 0;
150
+ let toBudget = 0;
151
+ let byFlag = false;
152
+ let remainder = 0;
153
+ let scheduleFlag = false;
154
+ // switch on template type and calculate the amount for the line
155
+ for (const template of t) {
156
+ let newBudget = 0;
157
+ switch (template.type) {
158
+ case 'simple': {
159
+ newBudget = CategoryTemplateContext.runSimple(template, this);
160
+ break;
161
+ }
162
+ case 'refill': {
163
+ newBudget = CategoryTemplateContext.runRefill(template, this);
164
+ break;
165
+ }
166
+ case 'copy': {
167
+ newBudget = await CategoryTemplateContext.runCopy(template, this);
168
+ break;
169
+ }
170
+ case 'periodic': {
171
+ newBudget = CategoryTemplateContext.runPeriodic(template, this);
172
+ break;
173
+ }
174
+ case 'spend': {
175
+ newBudget = await CategoryTemplateContext.runSpend(template, this);
176
+ break;
177
+ }
178
+ case 'percentage': {
179
+ newBudget = await CategoryTemplateContext.runPercentage(
180
+ template,
181
+ availStart,
182
+ this,
183
+ );
184
+ break;
185
+ }
186
+ case 'by': {
187
+ // all by's get run at once
188
+ if (!byFlag) {
189
+ newBudget = CategoryTemplateContext.runBy(this);
190
+ } else {
191
+ newBudget = 0;
192
+ }
193
+ byFlag = true;
194
+ break;
195
+ }
196
+ case 'schedule': {
197
+ if (!scheduleFlag) {
198
+ const budgeted = this.fromLastMonth + toBudget;
199
+ const ret = await runSchedule(
200
+ t,
201
+ this.month,
202
+ budgeted,
203
+ remainder,
204
+ this.fromLastMonth,
205
+ toBudget,
206
+ [],
207
+ this.category,
208
+ this.currency,
209
+ );
210
+ // Schedules assume that its to budget value is the whole thing so this
211
+ // needs to remove the previous funds so they aren't double counted
212
+ newBudget = ret.to_budget - toBudget;
213
+ remainder = ret.remainder;
214
+ scheduleFlag = true;
215
+ }
216
+ break;
217
+ }
218
+ case 'average': {
219
+ newBudget = await CategoryTemplateContext.runAverage(template, this);
220
+ break;
221
+ }
222
+ default: {
223
+ break;
224
+ }
225
+ }
226
+
227
+ available = available - newBudget;
228
+ toBudget += newBudget;
229
+ }
230
+
231
+ //check limit
232
+ if (this.limitCheck) {
233
+ if (
234
+ toBudget + this.toBudgetAmount + this.fromLastMonth >=
235
+ this.limitAmount
236
+ ) {
237
+ const orig = toBudget;
238
+ toBudget = this.limitAmount - this.toBudgetAmount - this.fromLastMonth;
239
+ this.limitMet = true;
240
+ available = available + orig - toBudget;
241
+ }
242
+ }
243
+
244
+ //round all budget values if needed
245
+ if (this.hideDecimal) toBudget = this.removeFraction(toBudget);
246
+
247
+ // don't overbudget when using a priority unless income category
248
+ if (priority > 0 && available < 0 && !this.category.is_income) {
249
+ this.fullAmount = (this.fullAmount || 0) + toBudget;
250
+ toBudget = Math.max(0, toBudget + available);
251
+ this.toBudgetAmount += toBudget;
252
+ } else {
253
+ this.fullAmount = (this.fullAmount || 0) + toBudget;
254
+ this.toBudgetAmount += toBudget;
255
+ }
256
+ return this.category.is_income ? -toBudget : toBudget;
257
+ }
258
+
259
+ runRemainder(budgetAvail: number, perWeight: number) {
260
+ if (this.remainder.length === 0) return 0;
261
+ let toBudget = Math.round(this.remainderWeight * perWeight);
262
+
263
+ let smallest = 1;
264
+ if (this.hideDecimal) {
265
+ // handle hideDecimal
266
+ toBudget = this.removeFraction(toBudget);
267
+ smallest = 100;
268
+ }
269
+
270
+ //check possible overbudget from rounding, 1cent leftover
271
+ if (toBudget > budgetAvail || budgetAvail - toBudget <= smallest) {
272
+ toBudget = budgetAvail;
273
+ }
274
+
275
+ if (this.limitCheck) {
276
+ if (
277
+ toBudget + this.toBudgetAmount + this.fromLastMonth >=
278
+ this.limitAmount
279
+ ) {
280
+ toBudget = this.limitAmount - this.toBudgetAmount - this.fromLastMonth;
281
+ this.limitMet = true;
282
+ }
283
+ }
284
+
285
+ this.toBudgetAmount += toBudget;
286
+ return toBudget;
287
+ }
288
+
289
+ getValues() {
290
+ this.runGoal();
291
+ return {
292
+ budgeted: this.toBudgetAmount,
293
+ goal: this.goalAmount,
294
+ longGoal: this.isLongGoal,
295
+ };
296
+ }
297
+
298
+ //-----------------------------------------------------------------------------
299
+ // Implementation
300
+ readonly category: CategoryEntity; //readonly so we can double check the category this is using
301
+ private month: string;
302
+ private templates: Template[] = [];
303
+ private remainder: RemainderTemplate[] = [];
304
+ private goals: GoalTemplate[] = [];
305
+ private priorities: Set<number> = new Set();
306
+ readonly hideDecimal: boolean = false;
307
+ private remainderWeight: number = 0;
308
+ private toBudgetAmount: number = 0; // amount that will be budgeted by the templates
309
+ private fullAmount: number | null = null; // the full requested amount, start null for remainder only cats
310
+ private isLongGoal: boolean | null = null; //defaulting the goals to null so templates can be unset
311
+ private goalAmount: number | null = null;
312
+ private fromLastMonth = 0; // leftover from last month
313
+ private limitMet = false;
314
+ private limitExcess: number = 0;
315
+ private limitAmount = 0;
316
+ private limitCheck = false;
317
+ private limitHold = false;
318
+ readonly previouslyBudgeted: number = 0;
319
+ private currency: Currency;
320
+
321
+ protected constructor(
322
+ templates: Template[],
323
+ category: CategoryEntity,
324
+ month: string,
325
+ fromLastMonth: number,
326
+ budgeted: number,
327
+ currencyCode: string,
328
+ hideDecimal: boolean = false,
329
+ ) {
330
+ this.category = category;
331
+ this.month = month;
332
+ this.fromLastMonth = fromLastMonth;
333
+ this.previouslyBudgeted = budgeted;
334
+ this.currency = getCurrency(currencyCode);
335
+ this.hideDecimal = hideDecimal;
336
+ // sort the template lines into regular template, goals, and remainder templates
337
+ if (templates) {
338
+ templates.forEach(t => {
339
+ if (
340
+ t.directive === 'template' &&
341
+ t.type !== 'remainder' &&
342
+ t.type !== 'limit'
343
+ ) {
344
+ this.templates.push(t);
345
+ if (t.priority !== null) this.priorities.add(t.priority);
346
+ } else if (t.directive === 'template' && t.type === 'remainder') {
347
+ this.remainder.push(t);
348
+ this.remainderWeight += t.weight;
349
+ } else if (t.directive === 'goal' && t.type === 'goal') {
350
+ this.goals.push(t);
351
+ }
352
+ });
353
+ }
354
+
355
+ this.checkLimit(templates);
356
+ this.checkSpend();
357
+ this.checkGoal();
358
+ }
359
+
360
+ private runGoal() {
361
+ if (this.goals.length > 0) {
362
+ if (this.isGoalOnly()) this.toBudgetAmount = this.previouslyBudgeted;
363
+ this.isLongGoal = true;
364
+ this.goalAmount = amountToInteger(
365
+ this.goals[0].amount,
366
+ this.currency.decimalPlaces,
367
+ );
368
+ return;
369
+ }
370
+ this.goalAmount = this.fullAmount;
371
+ }
372
+
373
+ //-----------------------------------------------------------------------------
374
+ // Template Validation
375
+ static async checkByAndScheduleAndSpend(
376
+ templates: Template[],
377
+ month: string,
378
+ ) {
379
+ if (
380
+ templates.filter(t => t.type === 'schedule' || t.type === 'by').length ===
381
+ 0
382
+ ) {
383
+ return;
384
+ }
385
+ //check schedule names
386
+ const scheduleNames = (await getActiveSchedules()).map(({ name }) =>
387
+ name.trim(),
388
+ );
389
+ templates
390
+ .filter(t => t.type === 'schedule')
391
+ .forEach(t => {
392
+ if (!scheduleNames.includes(t.name.trim())) {
393
+ throw new Error(`Schedule ${t.name.trim()} does not exist`);
394
+ }
395
+ });
396
+ //find lowest priority
397
+ const lowestPriority = Math.min(
398
+ ...templates
399
+ .filter(t => t.type === 'schedule' || t.type === 'by')
400
+ .map(t => t.priority),
401
+ );
402
+ //warn if priority needs fixed
403
+ templates
404
+ .filter(t => t.type === 'schedule' || t.type === 'by')
405
+ .forEach(t => {
406
+ if (t.priority !== lowestPriority) {
407
+ throw new Error(
408
+ `Schedule and By templates must be the same priority level. Fix by setting all Schedule and By templates to priority level ${lowestPriority}`,
409
+ );
410
+ //t.priority = lowestPriority;
411
+ }
412
+ });
413
+ // check if the target date is past and not repeating
414
+ templates
415
+ .filter(t => t.type === 'by' || t.type === 'spend')
416
+ .forEach(t => {
417
+ const range = monthUtils.differenceInCalendarMonths(
418
+ `${t.month}`,
419
+ month,
420
+ );
421
+ if (range < 0 && !(t.repeat || t.annual)) {
422
+ throw new Error(
423
+ `Target month has passed, remove or update the target month`,
424
+ );
425
+ }
426
+ });
427
+ }
428
+
429
+ static async checkPercentage(templates: Template[]) {
430
+ const pt = templates.filter(t => t.type === 'percentage');
431
+ if (pt.length === 0) return;
432
+ const reqCategories = pt.map(t => t.category.toLowerCase());
433
+
434
+ const availCategories = await db.getCategories();
435
+ const availNames = availCategories
436
+ .filter(c => c.is_income)
437
+ .map(c => c.name.toLocaleLowerCase());
438
+
439
+ reqCategories.forEach(n => {
440
+ if (n === 'available funds' || n === 'all income') {
441
+ //skip the name check since these are special
442
+ } else if (!availNames.includes(n)) {
443
+ throw new Error(
444
+ `Category \x22${n}\x22 is not found in available income categories`,
445
+ );
446
+ }
447
+ });
448
+ }
449
+
450
+ private checkLimit(templates: Template[]) {
451
+ for (const template of templates.filter(
452
+ t =>
453
+ t.type === 'simple' ||
454
+ t.type === 'periodic' ||
455
+ t.type === 'limit' ||
456
+ t.type === 'remainder',
457
+ )) {
458
+ let limitDef;
459
+ if (template.type === 'limit') {
460
+ limitDef = template;
461
+ } else {
462
+ if (template.limit) {
463
+ limitDef = template.limit;
464
+ } else {
465
+ continue; // may not have a limit defined in the template
466
+ }
467
+ }
468
+
469
+ if (this.limitCheck) {
470
+ throw new Error('Only one `up to` allowed per category');
471
+ }
472
+
473
+ if (limitDef.period === 'daily') {
474
+ const numDays = monthUtils.differenceInCalendarDays(
475
+ monthUtils.addMonths(this.month, 1),
476
+ this.month,
477
+ );
478
+ this.limitAmount +=
479
+ amountToInteger(limitDef.amount, this.currency.decimalPlaces) *
480
+ numDays;
481
+ } else if (limitDef.period === 'weekly') {
482
+ if (!limitDef.start) {
483
+ throw new Error('Weekly limit requires a start date (YYYY-MM-DD)');
484
+ }
485
+ const nextMonth = monthUtils.nextMonth(this.month);
486
+ let week = limitDef.start;
487
+ const baseLimit = amountToInteger(
488
+ limitDef.amount,
489
+ this.currency.decimalPlaces,
490
+ );
491
+ while (week < nextMonth) {
492
+ if (week >= this.month) {
493
+ this.limitAmount += baseLimit;
494
+ }
495
+ week = monthUtils.addWeeks(week, 1);
496
+ }
497
+ } else if (limitDef.period === 'monthly') {
498
+ this.limitAmount = amountToInteger(
499
+ limitDef.amount,
500
+ this.currency.decimalPlaces,
501
+ );
502
+ } else {
503
+ throw new Error('Invalid limit period. Check template syntax');
504
+ }
505
+
506
+ //amount is good save the rest
507
+ this.limitCheck = true;
508
+ this.limitHold = limitDef.hold ? true : false;
509
+ // check if the limit is already met and save the excess
510
+ if (this.fromLastMonth >= this.limitAmount) {
511
+ this.limitMet = true;
512
+ if (this.limitHold) {
513
+ this.limitExcess = 0;
514
+ this.toBudgetAmount = 0;
515
+ this.fullAmount = 0;
516
+ } else {
517
+ this.limitExcess = this.fromLastMonth - this.limitAmount;
518
+ this.toBudgetAmount = -this.limitExcess;
519
+ this.fullAmount = -this.limitExcess;
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ private checkSpend() {
526
+ const st = this.templates.filter(t => t.type === 'spend');
527
+ if (st.length > 1) {
528
+ throw new Error('Only one spend template is allowed per category');
529
+ }
530
+ }
531
+
532
+ private checkGoal() {
533
+ if (this.goals.length > 1) {
534
+ throw new Error(`Only one #goal is allowed per category`);
535
+ }
536
+ }
537
+
538
+ private removeFraction(amount: number): number {
539
+ return amountToInteger(
540
+ Math.round(integerToAmount(amount, this.currency.decimalPlaces)),
541
+ this.currency.decimalPlaces,
542
+ );
543
+ }
544
+
545
+ //-----------------------------------------------------------------------------
546
+ // Processor Functions
547
+
548
+ static runSimple(
549
+ template: SimpleTemplate,
550
+ templateContext: CategoryTemplateContext,
551
+ ): number {
552
+ if (template.monthly != null) {
553
+ return amountToInteger(
554
+ template.monthly,
555
+ templateContext.currency.decimalPlaces,
556
+ );
557
+ } else {
558
+ return templateContext.limitAmount - templateContext.fromLastMonth;
559
+ }
560
+ }
561
+
562
+ static runRefill(
563
+ template: RefillTemplate,
564
+ templateContext: CategoryTemplateContext,
565
+ ): number {
566
+ return templateContext.limitAmount - templateContext.fromLastMonth;
567
+ }
568
+
569
+ static async runCopy(
570
+ template: CopyTemplate,
571
+ templateContext: CategoryTemplateContext,
572
+ ): Promise<number> {
573
+ const sheetName = monthUtils.sheetForMonth(
574
+ monthUtils.subMonths(templateContext.month, template.lookBack),
575
+ );
576
+ return await getSheetValue(
577
+ sheetName,
578
+ `budget-${templateContext.category.id}`,
579
+ );
580
+ }
581
+
582
+ static runPeriodic(
583
+ template: PeriodicTemplate,
584
+ templateContext: CategoryTemplateContext,
585
+ ): number {
586
+ let toBudget = 0;
587
+ const amount = amountToInteger(
588
+ template.amount,
589
+ templateContext.currency.decimalPlaces,
590
+ );
591
+ const period = template.period.period;
592
+ const numPeriods = template.period.amount;
593
+ let date =
594
+ template.starting ?? monthUtils.firstDayOfMonth(templateContext.month);
595
+
596
+ let dateShiftFunction;
597
+ switch (period) {
598
+ case 'day':
599
+ dateShiftFunction = monthUtils.addDays;
600
+ break;
601
+ case 'week':
602
+ dateShiftFunction = monthUtils.addWeeks;
603
+ break;
604
+ case 'month':
605
+ dateShiftFunction = monthUtils.addMonths;
606
+ break;
607
+ case 'year':
608
+ // the addYears function doesn't return the month number, so use addMonths
609
+ dateShiftFunction = (date: string | Date, numPeriods: number) =>
610
+ monthUtils.addMonths(date, numPeriods * 12);
611
+ break;
612
+ default:
613
+ throw new Error(`Unrecognized periodic period: ${String(period)}`);
614
+ }
615
+
616
+ //shift the starting date until its in our month or in the future
617
+ while (templateContext.month > date) {
618
+ date = dateShiftFunction(date, numPeriods);
619
+ }
620
+
621
+ if (
622
+ monthUtils.differenceInCalendarMonths(templateContext.month, date) < 0
623
+ ) {
624
+ return 0;
625
+ } // nothing needed this month
626
+
627
+ const nextMonth = monthUtils.addMonths(templateContext.month, 1);
628
+ while (date < nextMonth) {
629
+ toBudget += amount;
630
+ date = dateShiftFunction(date, numPeriods);
631
+ }
632
+
633
+ return toBudget;
634
+ }
635
+
636
+ static async runSpend(
637
+ template: SpendTemplate,
638
+ templateContext: CategoryTemplateContext,
639
+ ): Promise<number> {
640
+ let fromMonth = `${template.from}`;
641
+ let toMonth = `${template.month}`;
642
+ let alreadyBudgeted = templateContext.fromLastMonth;
643
+ let firstMonth = true;
644
+
645
+ //update months if needed
646
+ const repeat = template.annual
647
+ ? (template.repeat || 1) * 12
648
+ : template.repeat;
649
+ let m = monthUtils.differenceInCalendarMonths(
650
+ toMonth,
651
+ templateContext.month,
652
+ );
653
+ if (repeat && m < 0) {
654
+ while (m < 0) {
655
+ toMonth = monthUtils.addMonths(toMonth, repeat);
656
+ fromMonth = monthUtils.addMonths(fromMonth, repeat);
657
+ m = monthUtils.differenceInCalendarMonths(
658
+ toMonth,
659
+ templateContext.month,
660
+ );
661
+ }
662
+ }
663
+
664
+ for (
665
+ let m = fromMonth;
666
+ monthUtils.differenceInCalendarMonths(templateContext.month, m) > 0;
667
+ m = monthUtils.addMonths(m, 1)
668
+ ) {
669
+ const sheetName = monthUtils.sheetForMonth(m);
670
+ if (firstMonth) {
671
+ //TODO figure out if I already found these values and can pass them in
672
+ const spent = await getSheetValue(
673
+ sheetName,
674
+ `sum-amount-${templateContext.category.id}`,
675
+ );
676
+ const balance = await getSheetValue(
677
+ sheetName,
678
+ `leftover-${templateContext.category.id}`,
679
+ );
680
+ alreadyBudgeted = balance - spent;
681
+ firstMonth = false;
682
+ } else {
683
+ alreadyBudgeted += await getSheetValue(
684
+ sheetName,
685
+ `budget-${templateContext.category.id}`,
686
+ );
687
+ }
688
+ }
689
+
690
+ const numMonths = monthUtils.differenceInCalendarMonths(
691
+ toMonth,
692
+ templateContext.month,
693
+ );
694
+ const target = amountToInteger(
695
+ template.amount,
696
+ templateContext.currency.decimalPlaces,
697
+ );
698
+ if (numMonths < 0) {
699
+ return 0;
700
+ } else {
701
+ return Math.round((target - alreadyBudgeted) / (numMonths + 1));
702
+ }
703
+ }
704
+
705
+ static async runPercentage(
706
+ template: PercentageTemplate,
707
+ availableFunds: number,
708
+ templateContext: CategoryTemplateContext,
709
+ ): Promise<number> {
710
+ const percent = template.percent;
711
+ const cat = template.category.toLowerCase();
712
+ const prev = template.previous;
713
+ let sheetName;
714
+ let monthlyIncome = 1;
715
+
716
+ //choose the sheet to find income for
717
+ if (prev) {
718
+ sheetName = monthUtils.sheetForMonth(
719
+ monthUtils.subMonths(templateContext.month, 1),
720
+ );
721
+ } else {
722
+ sheetName = monthUtils.sheetForMonth(templateContext.month);
723
+ }
724
+ if (cat === 'all income') {
725
+ monthlyIncome = await getSheetValue(sheetName, `total-income`);
726
+ } else if (cat === 'available funds') {
727
+ monthlyIncome = availableFunds;
728
+ } else {
729
+ const incomeCat = (await db.getCategories()).find(
730
+ c => c.is_income && c.name.toLowerCase() === cat,
731
+ );
732
+ if (!incomeCat) {
733
+ throw new Error(
734
+ `Income category "${template.category}" not found for percentage template`,
735
+ );
736
+ }
737
+ monthlyIncome = await getSheetValue(
738
+ sheetName,
739
+ `sum-amount-${incomeCat.id}`,
740
+ );
741
+ }
742
+
743
+ return Math.max(0, Math.round(monthlyIncome * (percent / 100)));
744
+ }
745
+
746
+ static async runAverage(
747
+ template: AverageTemplate,
748
+ templateContext: CategoryTemplateContext,
749
+ ): Promise<number> {
750
+ let sum = 0;
751
+ for (let i = 1; i <= template.numMonths; i++) {
752
+ const sheetName = monthUtils.sheetForMonth(
753
+ monthUtils.subMonths(templateContext.month, i),
754
+ );
755
+ sum += await getSheetValue(
756
+ sheetName,
757
+ `sum-amount-${templateContext.category.id}`,
758
+ );
759
+ }
760
+
761
+ // negate as sheet value is cost ie negative
762
+ let average = -(sum / template.numMonths);
763
+
764
+ if (template.adjustment !== undefined && template.adjustmentType) {
765
+ switch (template.adjustmentType) {
766
+ case 'percent': {
767
+ const adjustmentFactor = 1 + template.adjustment / 100;
768
+ average = adjustmentFactor * average;
769
+ break;
770
+ }
771
+ case 'fixed': {
772
+ average += amountToInteger(
773
+ template.adjustment,
774
+ templateContext.currency.decimalPlaces,
775
+ );
776
+ break;
777
+ }
778
+
779
+ default:
780
+ //no valid adjustment was found
781
+ }
782
+ }
783
+
784
+ return Math.round(average);
785
+ }
786
+
787
+ static runBy(templateContext: CategoryTemplateContext): number {
788
+ const byTemplates: ByTemplate[] = templateContext.templates.filter(
789
+ t => t.type === 'by',
790
+ );
791
+ const savedInfo = [];
792
+ let totalNeeded = 0;
793
+ let workingShortNumMonths;
794
+ //find shortest time period
795
+ for (let i = 0; i < byTemplates.length; i++) {
796
+ const template = byTemplates[i];
797
+ let targetMonth = `${template.month}`;
798
+ const period = template.annual
799
+ ? (template.repeat || 1) * 12
800
+ : template.repeat != null
801
+ ? template.repeat
802
+ : null;
803
+ let numMonths = monthUtils.differenceInCalendarMonths(
804
+ targetMonth,
805
+ templateContext.month,
806
+ );
807
+ while (numMonths < 0 && period) {
808
+ targetMonth = monthUtils.addMonths(targetMonth, period);
809
+ numMonths = monthUtils.differenceInCalendarMonths(
810
+ targetMonth,
811
+ templateContext.month,
812
+ );
813
+ }
814
+ savedInfo.push({ numMonths, period });
815
+ if (
816
+ workingShortNumMonths === undefined ||
817
+ numMonths < workingShortNumMonths
818
+ ) {
819
+ workingShortNumMonths = numMonths;
820
+ }
821
+ }
822
+
823
+ // calculate needed funds per template
824
+ const shortNumMonths = workingShortNumMonths || 0;
825
+ for (let i = 0; i < byTemplates.length; i++) {
826
+ const template = byTemplates[i];
827
+ const numMonths = savedInfo[i].numMonths;
828
+ const period = savedInfo[i].period;
829
+ let amount;
830
+ // back interpolate what is needed in the short window
831
+ if (numMonths > shortNumMonths && period) {
832
+ amount = Math.round(
833
+ (amountToInteger(
834
+ template.amount,
835
+ templateContext.currency.decimalPlaces,
836
+ ) /
837
+ period) *
838
+ (period - numMonths + shortNumMonths),
839
+ );
840
+ // fallback to this. This matches what the prior math accomplished, just more round about
841
+ } else if (numMonths > shortNumMonths) {
842
+ amount = Math.round(
843
+ (amountToInteger(
844
+ template.amount,
845
+ templateContext.currency.decimalPlaces,
846
+ ) /
847
+ (numMonths + 1)) *
848
+ (shortNumMonths + 1),
849
+ );
850
+ } else {
851
+ amount = amountToInteger(
852
+ template.amount,
853
+ templateContext.currency.decimalPlaces,
854
+ );
855
+ }
856
+ totalNeeded += amount;
857
+ }
858
+ return Math.round(
859
+ (totalNeeded - templateContext.fromLastMonth) / (shortNumMonths + 1),
860
+ );
861
+ }
862
+ }