@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,1658 @@
1
+ import { vi } from 'vitest';
2
+
3
+ import { amountToInteger } from '../../shared/util';
4
+ import type { CategoryEntity } from '../../types/models';
5
+ import type { Template } from '../../types/models/templates';
6
+ import * as aql from '../aql';
7
+ import * as db from '../db';
8
+
9
+ import * as actions from './actions';
10
+ import { CategoryTemplateContext } from './category-template-context';
11
+
12
+ // Mock getSheetValue and getCategories
13
+ vi.mock('./actions', () => ({
14
+ getSheetValue: vi.fn(),
15
+ getSheetBoolean: vi.fn(),
16
+ isReflectBudget: vi.fn(),
17
+ }));
18
+
19
+ vi.mock('../db', () => ({
20
+ getCategories: vi.fn(),
21
+ }));
22
+
23
+ vi.mock('../aql', () => ({
24
+ aqlQuery: vi.fn(),
25
+ }));
26
+
27
+ // Helper function to mock preferences (hideFraction and defaultCurrencyCode)
28
+ function mockPreferences(
29
+ hideFraction: boolean = false,
30
+ currencyCode: string = 'USD',
31
+ ) {
32
+ vi.mocked(aql.aqlQuery).mockImplementation(async (query: unknown) => {
33
+ const queryStr = JSON.stringify(query);
34
+ if (queryStr.includes('hideFraction')) {
35
+ return {
36
+ data: [{ value: hideFraction ? 'true' : 'false' }],
37
+ dependencies: [],
38
+ };
39
+ }
40
+ if (queryStr.includes('defaultCurrencyCode')) {
41
+ return {
42
+ data: currencyCode ? [{ value: currencyCode }] : [],
43
+ dependencies: [],
44
+ };
45
+ }
46
+ return { data: [], dependencies: [] };
47
+ });
48
+ }
49
+
50
+ // Test helper class to access constructor and methods
51
+ class TestCategoryTemplateContext extends CategoryTemplateContext {
52
+ public constructor(
53
+ templates: Template[],
54
+ category: CategoryEntity,
55
+ month: string,
56
+ fromLastMonth: number,
57
+ budgeted: number,
58
+ currencyCode: string = 'USD',
59
+ ) {
60
+ super(templates, category, month, fromLastMonth, budgeted, currencyCode);
61
+ }
62
+ }
63
+
64
+ describe('CategoryTemplateContext', () => {
65
+ describe('runSimple', () => {
66
+ it('should return monthly amount when provided', () => {
67
+ const category: CategoryEntity = {
68
+ id: 'test',
69
+ name: 'Test Category',
70
+ group: 'test-group',
71
+ is_income: false,
72
+ };
73
+ const template: Template = {
74
+ type: 'simple',
75
+ monthly: 100,
76
+ directive: 'template',
77
+ priority: 1,
78
+ };
79
+
80
+ const instance = new TestCategoryTemplateContext(
81
+ [],
82
+ category,
83
+ '2024-01',
84
+ 0,
85
+ 0,
86
+ );
87
+
88
+ const result = CategoryTemplateContext.runSimple(template, instance);
89
+ expect(result).toBe(amountToInteger(100));
90
+ });
91
+
92
+ it('should return limit when monthly is not provided', () => {
93
+ const category: CategoryEntity = {
94
+ id: 'test',
95
+ name: 'Test Category',
96
+ group: 'test-group',
97
+ is_income: false,
98
+ };
99
+ const template: Template = {
100
+ type: 'simple',
101
+ limit: { amount: 500, hold: false, period: 'monthly' },
102
+ directive: 'template',
103
+ priority: 1,
104
+ };
105
+
106
+ const instance = new TestCategoryTemplateContext(
107
+ [template],
108
+ category,
109
+ '2024-01',
110
+ 0,
111
+ 0,
112
+ );
113
+
114
+ const result = CategoryTemplateContext.runSimple(template, instance);
115
+ expect(result).toBe(amountToInteger(500));
116
+ });
117
+
118
+ it('should handle weekly limit', async () => {
119
+ const category: CategoryEntity = {
120
+ id: 'test',
121
+ name: 'Test Category',
122
+ group: 'test-group',
123
+ is_income: false,
124
+ };
125
+ const template: Template = {
126
+ type: 'simple',
127
+ limit: {
128
+ amount: 100,
129
+ hold: false,
130
+ period: 'weekly',
131
+ start: '2024-01-01',
132
+ },
133
+ directive: 'template',
134
+ priority: 1,
135
+ };
136
+ const instance = new TestCategoryTemplateContext(
137
+ [template],
138
+ category,
139
+ '2024-01',
140
+ 0,
141
+ 0,
142
+ );
143
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
144
+ expect(result).toBe(50000); // 5 Mondays * 100
145
+ });
146
+
147
+ it('should handle daily limit', async () => {
148
+ const category: CategoryEntity = {
149
+ id: 'test',
150
+ name: 'Test Category',
151
+ group: 'test-group',
152
+ is_income: false,
153
+ };
154
+ const template: Template = {
155
+ type: 'simple',
156
+ limit: { amount: 10, hold: false, period: 'daily' },
157
+ directive: 'template',
158
+ priority: 1,
159
+ };
160
+ const instance = new TestCategoryTemplateContext(
161
+ [template],
162
+ category,
163
+ '2024-01',
164
+ 0,
165
+ 0,
166
+ );
167
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
168
+ expect(result).toBe(31000); // 31 days * 10
169
+ });
170
+ });
171
+
172
+ describe('runRefill', () => {
173
+ it('should refill up to the monthly limit', async () => {
174
+ const category: CategoryEntity = {
175
+ id: 'test',
176
+ name: 'Test Category',
177
+ group: 'test-group',
178
+ is_income: false,
179
+ };
180
+ const limitTemplate: Template = {
181
+ type: 'limit',
182
+ amount: 150,
183
+ hold: false,
184
+ period: 'monthly',
185
+ directive: 'template',
186
+ priority: null,
187
+ };
188
+ const refillTemplate: Template = {
189
+ type: 'refill',
190
+ directive: 'template',
191
+ priority: 1,
192
+ };
193
+
194
+ const instance = new TestCategoryTemplateContext(
195
+ [limitTemplate, refillTemplate],
196
+ category,
197
+ '2024-01',
198
+ 9000,
199
+ 0,
200
+ );
201
+
202
+ const result = await instance.runTemplatesForPriority(1, 10000, 10000);
203
+ expect(result).toBe(6000); // 150 - 90
204
+ });
205
+
206
+ it('should handle weekly limit refill', async () => {
207
+ const category: CategoryEntity = {
208
+ id: 'test',
209
+ name: 'Test Category',
210
+ group: 'test-group',
211
+ is_income: false,
212
+ };
213
+ const limitTemplate: Template = {
214
+ type: 'limit',
215
+ amount: 100,
216
+ hold: false,
217
+ period: 'weekly',
218
+ start: '2024-01-01',
219
+ directive: 'template',
220
+ priority: null,
221
+ };
222
+ const refillTemplate: Template = {
223
+ type: 'refill',
224
+ directive: 'template',
225
+ priority: 1,
226
+ };
227
+
228
+ const instance = new TestCategoryTemplateContext(
229
+ [limitTemplate, refillTemplate],
230
+ category,
231
+ '2024-01',
232
+ 0,
233
+ 0,
234
+ );
235
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
236
+ expect(result).toBe(50000); // 5 Mondays * 100
237
+ });
238
+
239
+ it('should handle daily limit refill', async () => {
240
+ const category: CategoryEntity = {
241
+ id: 'test',
242
+ name: 'Test Category',
243
+ group: 'test-group',
244
+ is_income: false,
245
+ };
246
+ const limitTemplate: Template = {
247
+ type: 'limit',
248
+ amount: 10,
249
+ hold: false,
250
+ period: 'daily',
251
+ directive: 'template',
252
+ priority: null,
253
+ };
254
+ const refillTemplate: Template = {
255
+ type: 'refill',
256
+ directive: 'template',
257
+ priority: 1,
258
+ };
259
+ const instance = new TestCategoryTemplateContext(
260
+ [limitTemplate, refillTemplate],
261
+ category,
262
+ '2024-01',
263
+ 0,
264
+ 0,
265
+ );
266
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
267
+ expect(result).toBe(31000); // 31 days * 10
268
+ });
269
+ });
270
+
271
+ describe('runCopy', () => {
272
+ let instance: TestCategoryTemplateContext;
273
+
274
+ beforeEach(() => {
275
+ const category: CategoryEntity = {
276
+ id: 'test',
277
+ name: 'Test Category',
278
+ group: 'test-group',
279
+ is_income: false,
280
+ };
281
+ instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
282
+ vi.clearAllMocks();
283
+ });
284
+
285
+ it('should copy budget from previous month', async () => {
286
+ const template: Template = {
287
+ type: 'copy',
288
+ lookBack: 1,
289
+ directive: 'template',
290
+ priority: 1,
291
+ };
292
+
293
+ vi.mocked(actions.getSheetValue).mockResolvedValue(100);
294
+
295
+ const result = await CategoryTemplateContext.runCopy(template, instance);
296
+ expect(result).toBe(100);
297
+ });
298
+
299
+ it('should copy budget from multiple months back', async () => {
300
+ const template: Template = {
301
+ type: 'copy',
302
+ lookBack: 3,
303
+ directive: 'template',
304
+ priority: 1,
305
+ };
306
+
307
+ vi.mocked(actions.getSheetValue).mockResolvedValue(200);
308
+
309
+ const result = await CategoryTemplateContext.runCopy(template, instance);
310
+ expect(result).toBe(200);
311
+ });
312
+
313
+ it('should handle zero budget amount', async () => {
314
+ const template: Template = {
315
+ type: 'copy',
316
+ lookBack: 1,
317
+ directive: 'template',
318
+ priority: 1,
319
+ };
320
+
321
+ vi.mocked(actions.getSheetValue).mockResolvedValue(0);
322
+
323
+ const result = await CategoryTemplateContext.runCopy(template, instance);
324
+ expect(result).toBe(0);
325
+ });
326
+ });
327
+
328
+ describe('runPeriodic', () => {
329
+ let instance: TestCategoryTemplateContext;
330
+
331
+ beforeEach(() => {
332
+ const category: CategoryEntity = {
333
+ id: 'test',
334
+ name: 'Test Category',
335
+ group: 'test-group',
336
+ is_income: false,
337
+ };
338
+ instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
339
+ });
340
+
341
+ //5 mondays in January 2024
342
+ it('should calculate weekly amount for single week', () => {
343
+ const template: Template = {
344
+ type: 'periodic',
345
+ amount: 100,
346
+ period: {
347
+ period: 'week',
348
+ amount: 1,
349
+ },
350
+ starting: '2024-01-01',
351
+ directive: 'template',
352
+ priority: 1,
353
+ };
354
+
355
+ const result = CategoryTemplateContext.runPeriodic(template, instance);
356
+ expect(result).toBe(amountToInteger(500));
357
+ });
358
+
359
+ it('should calculate weekly amount for multiple weeks', () => {
360
+ const template: Template = {
361
+ type: 'periodic',
362
+ amount: 100,
363
+ period: {
364
+ period: 'week',
365
+ amount: 2,
366
+ },
367
+ starting: '2024-01-01',
368
+ directive: 'template',
369
+ priority: 1,
370
+ };
371
+
372
+ const result = CategoryTemplateContext.runPeriodic(template, instance);
373
+ expect(result).toBe(amountToInteger(300));
374
+ });
375
+
376
+ it('should handle weeks spanning multiple months', () => {
377
+ const template: Template = {
378
+ type: 'periodic',
379
+ amount: 100,
380
+ period: {
381
+ period: 'week',
382
+ amount: 7,
383
+ },
384
+ starting: '2023-12-04',
385
+ directive: 'template',
386
+ priority: 1,
387
+ };
388
+
389
+ const result = CategoryTemplateContext.runPeriodic(template, instance);
390
+ expect(result).toBe(amountToInteger(100));
391
+ });
392
+
393
+ it('should handle periodic days', () => {
394
+ const template: Template = {
395
+ type: 'periodic',
396
+ amount: 100,
397
+ period: {
398
+ period: 'day',
399
+ amount: 10,
400
+ },
401
+ starting: '2024-01-01',
402
+ directive: 'template',
403
+ priority: 1,
404
+ };
405
+
406
+ const result = CategoryTemplateContext.runPeriodic(template, instance);
407
+ expect(result).toBe(amountToInteger(400)); // for the 1st, 11th, 21st, 31st
408
+ });
409
+
410
+ it('should handle periodic years', () => {
411
+ const template: Template = {
412
+ type: 'periodic',
413
+ amount: 100,
414
+ period: {
415
+ period: 'year',
416
+ amount: 1,
417
+ },
418
+ starting: '2023-01-01',
419
+ directive: 'template',
420
+ priority: 1,
421
+ };
422
+
423
+ const result = CategoryTemplateContext.runPeriodic(template, instance);
424
+ expect(result).toBe(amountToInteger(100));
425
+ });
426
+
427
+ it('should handle periodic months', () => {
428
+ const template: Template = {
429
+ type: 'periodic',
430
+ amount: 100,
431
+ period: {
432
+ period: 'month',
433
+ amount: 2,
434
+ },
435
+ starting: '2023-11-01',
436
+ directive: 'template',
437
+ priority: 1,
438
+ };
439
+
440
+ const result = CategoryTemplateContext.runPeriodic(template, instance);
441
+ expect(result).toBe(amountToInteger(100));
442
+ });
443
+ });
444
+
445
+ describe('runSpend', () => {
446
+ let instance: TestCategoryTemplateContext;
447
+
448
+ beforeEach(() => {
449
+ const category: CategoryEntity = {
450
+ id: 'test',
451
+ name: 'Test Category',
452
+ group: 'test-group',
453
+ is_income: false,
454
+ };
455
+ instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
456
+ vi.clearAllMocks();
457
+ });
458
+
459
+ it('should calculate monthly amount needed to reach target', async () => {
460
+ const template: Template = {
461
+ type: 'spend',
462
+ amount: 1000,
463
+ from: '2023-11',
464
+ month: '2024-01',
465
+ directive: 'template',
466
+ priority: 1,
467
+ };
468
+
469
+ vi.mocked(actions.getSheetValue)
470
+ .mockResolvedValueOnce(-10000) // spent in Nov
471
+ .mockResolvedValueOnce(20000) // leftover in Nov
472
+ .mockResolvedValueOnce(10000); // budgeted in Dec
473
+
474
+ const result = await CategoryTemplateContext.runSpend(template, instance);
475
+ expect(result).toBe(60000);
476
+ });
477
+
478
+ it('should handle repeating spend template', async () => {
479
+ const template: Template = {
480
+ type: 'spend',
481
+ amount: 1000,
482
+ from: '2023-11',
483
+ month: '2023-12',
484
+ repeat: 3,
485
+ directive: 'template',
486
+ priority: 1,
487
+ };
488
+
489
+ vi.mocked(actions.getSheetValue)
490
+ .mockResolvedValueOnce(-10000)
491
+ .mockResolvedValueOnce(20000)
492
+ .mockResolvedValueOnce(10000);
493
+
494
+ const result = await CategoryTemplateContext.runSpend(template, instance);
495
+ expect(result).toBe(33333);
496
+ });
497
+
498
+ it('should return zero for past target date', async () => {
499
+ const template: Template = {
500
+ type: 'spend',
501
+ amount: 1000,
502
+ from: '2023-12',
503
+ month: '2023-12',
504
+ directive: 'template',
505
+ priority: 1,
506
+ };
507
+
508
+ const result = await CategoryTemplateContext.runSpend(template, instance);
509
+ expect(result).toBe(0);
510
+ });
511
+ });
512
+
513
+ describe('runPercentage', () => {
514
+ let instance: TestCategoryTemplateContext;
515
+
516
+ beforeEach(() => {
517
+ const category: CategoryEntity = {
518
+ id: 'test',
519
+ name: 'Test Category',
520
+ group: 'test-group',
521
+ is_income: false,
522
+ };
523
+ instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
524
+ vi.clearAllMocks();
525
+ });
526
+
527
+ it('should calculate percentage of all income', async () => {
528
+ const template: Template = {
529
+ type: 'percentage',
530
+ percent: 10,
531
+ category: 'all income',
532
+ previous: false,
533
+ directive: 'template',
534
+ priority: 1,
535
+ };
536
+
537
+ vi.mocked(actions.getSheetValue).mockResolvedValue(10000);
538
+
539
+ const result = await CategoryTemplateContext.runPercentage(
540
+ template,
541
+ 0,
542
+ instance,
543
+ );
544
+ expect(result).toBe(1000); // 10% of 10000
545
+ });
546
+
547
+ it('should calculate percentage of available funds', async () => {
548
+ const template: Template = {
549
+ type: 'percentage',
550
+ percent: 20,
551
+ category: 'available funds',
552
+ previous: false,
553
+ directive: 'template',
554
+ priority: 1,
555
+ };
556
+
557
+ const result = await CategoryTemplateContext.runPercentage(
558
+ template,
559
+ 500,
560
+ instance,
561
+ );
562
+ expect(result).toBe(100); // 20% of 500
563
+ });
564
+
565
+ it('should calculate percentage of specific income category', async () => {
566
+ const template: Template = {
567
+ type: 'percentage',
568
+ percent: 15,
569
+ category: 'Salary',
570
+ previous: false,
571
+ directive: 'template',
572
+ priority: 1,
573
+ };
574
+
575
+ vi.mocked(db.getCategories).mockResolvedValue([
576
+ {
577
+ id: 'income1',
578
+ name: 'Salary',
579
+ is_income: 1,
580
+ cat_group: 'income',
581
+ sort_order: 1,
582
+ hidden: 0,
583
+ tombstone: 0,
584
+ },
585
+ ]);
586
+ vi.mocked(actions.getSheetValue).mockResolvedValue(2000);
587
+
588
+ const result = await CategoryTemplateContext.runPercentage(
589
+ template,
590
+ 0,
591
+ instance,
592
+ );
593
+ expect(result).toBe(300); // 15% of 2000
594
+ });
595
+
596
+ it('should calculate percentage of previous month income', async () => {
597
+ const template: Template = {
598
+ type: 'percentage',
599
+ percent: 10,
600
+ category: 'all income',
601
+ previous: true,
602
+ directive: 'template',
603
+ priority: 1,
604
+ };
605
+
606
+ vi.mocked(actions.getSheetValue).mockResolvedValue(10000);
607
+
608
+ const result = await CategoryTemplateContext.runPercentage(
609
+ template,
610
+ 0,
611
+ instance,
612
+ );
613
+ expect(result).toBe(1000); // 10% of 10000
614
+ expect(actions.getSheetValue).toHaveBeenCalledWith(
615
+ 'budget202312',
616
+ 'total-income',
617
+ );
618
+ });
619
+ });
620
+
621
+ describe('runAverage', () => {
622
+ let instance: TestCategoryTemplateContext;
623
+
624
+ beforeEach(() => {
625
+ const category: CategoryEntity = {
626
+ id: 'test',
627
+ name: 'Test Category',
628
+ group: 'test-group',
629
+ is_income: false,
630
+ };
631
+ instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
632
+ vi.clearAllMocks();
633
+ });
634
+
635
+ it('should calculate average of 3 months', async () => {
636
+ const template: Template = {
637
+ type: 'average',
638
+ numMonths: 3,
639
+ directive: 'template',
640
+ priority: 1,
641
+ };
642
+
643
+ vi.mocked(actions.getSheetValue)
644
+ .mockResolvedValueOnce(-100) // Dec 2023
645
+ .mockResolvedValueOnce(-200) // Nov 2023
646
+ .mockResolvedValueOnce(-300); // Oct 2023
647
+
648
+ const result = await CategoryTemplateContext.runAverage(
649
+ template,
650
+ instance,
651
+ );
652
+ expect(result).toBe(200); // Average of -100, -200, -300
653
+ });
654
+
655
+ it('should handle zero amounts', async () => {
656
+ const template: Template = {
657
+ type: 'average',
658
+ numMonths: 3,
659
+ directive: 'template',
660
+ priority: 1,
661
+ };
662
+
663
+ vi.mocked(actions.getSheetValue)
664
+ .mockResolvedValueOnce(0)
665
+ .mockResolvedValueOnce(0)
666
+ .mockResolvedValueOnce(-300);
667
+
668
+ const result = await CategoryTemplateContext.runAverage(
669
+ template,
670
+ instance,
671
+ );
672
+ expect(result).toBe(100);
673
+ });
674
+
675
+ it('should handle mixed positive and negative amounts', async () => {
676
+ const template: Template = {
677
+ type: 'average',
678
+ numMonths: 3,
679
+ directive: 'template',
680
+ priority: 1,
681
+ };
682
+
683
+ vi.mocked(actions.getSheetValue)
684
+ .mockResolvedValueOnce(-100)
685
+ .mockResolvedValueOnce(200)
686
+ .mockResolvedValueOnce(-300);
687
+
688
+ const result = await CategoryTemplateContext.runAverage(
689
+ template,
690
+ instance,
691
+ );
692
+ expect(result).toBe(67); // Average of -100, 200, -300
693
+ });
694
+
695
+ it('should handle positive percent adjustments', async () => {
696
+ const template: Template = {
697
+ type: 'average',
698
+ numMonths: 3,
699
+ directive: 'template',
700
+ priority: 1,
701
+ adjustment: 10,
702
+ adjustmentType: 'percent',
703
+ };
704
+
705
+ vi.mocked(actions.getSheetValue)
706
+ .mockResolvedValueOnce(-100)
707
+ .mockResolvedValueOnce(-100)
708
+ .mockResolvedValueOnce(-100);
709
+
710
+ const result = await CategoryTemplateContext.runAverage(
711
+ template,
712
+ instance,
713
+ );
714
+ expect(result).toBe(110);
715
+ });
716
+
717
+ it('should handle negative percent adjustments', async () => {
718
+ const template: Template = {
719
+ type: 'average',
720
+ numMonths: 3,
721
+ directive: 'template',
722
+ priority: 1,
723
+ adjustment: -10,
724
+ adjustmentType: 'percent',
725
+ };
726
+
727
+ vi.mocked(actions.getSheetValue)
728
+ .mockResolvedValueOnce(-100)
729
+ .mockResolvedValueOnce(-100)
730
+ .mockResolvedValueOnce(-100);
731
+
732
+ const result = await CategoryTemplateContext.runAverage(
733
+ template,
734
+ instance,
735
+ );
736
+ expect(result).toBe(90);
737
+ });
738
+ it('should handle zero percent adjustments', async () => {
739
+ const template: Template = {
740
+ type: 'average',
741
+ numMonths: 3,
742
+ directive: 'template',
743
+ priority: 1,
744
+ adjustment: 0,
745
+ adjustmentType: 'percent',
746
+ };
747
+
748
+ vi.mocked(actions.getSheetValue)
749
+ .mockResolvedValueOnce(-100)
750
+ .mockResolvedValueOnce(-100)
751
+ .mockResolvedValueOnce(-100);
752
+
753
+ const result = await CategoryTemplateContext.runAverage(
754
+ template,
755
+ instance,
756
+ );
757
+ expect(result).toBe(100);
758
+ });
759
+
760
+ it('should handle zero amount adjustments', async () => {
761
+ const template: Template = {
762
+ type: 'average',
763
+ numMonths: 3,
764
+ directive: 'template',
765
+ priority: 1,
766
+ adjustment: 0,
767
+ adjustmentType: 'fixed',
768
+ };
769
+
770
+ vi.mocked(actions.getSheetValue)
771
+ .mockResolvedValueOnce(-100)
772
+ .mockResolvedValueOnce(-100)
773
+ .mockResolvedValueOnce(-100);
774
+
775
+ const result = await CategoryTemplateContext.runAverage(
776
+ template,
777
+ instance,
778
+ );
779
+ expect(result).toBe(100);
780
+ });
781
+
782
+ it('should handle positive amount adjustments', async () => {
783
+ const template: Template = {
784
+ type: 'average',
785
+ numMonths: 3,
786
+ directive: 'template',
787
+ priority: 1,
788
+ adjustment: 11,
789
+ adjustmentType: 'fixed',
790
+ };
791
+
792
+ vi.mocked(actions.getSheetValue)
793
+ .mockResolvedValueOnce(-10000)
794
+ .mockResolvedValueOnce(-10000)
795
+ .mockResolvedValueOnce(-10000);
796
+
797
+ const result = await CategoryTemplateContext.runAverage(
798
+ template,
799
+ instance,
800
+ );
801
+ expect(result).toBe(11100);
802
+ });
803
+
804
+ it('should handle negative amount adjustments', async () => {
805
+ const template: Template = {
806
+ type: 'average',
807
+ numMonths: 3,
808
+ directive: 'template',
809
+ priority: 1,
810
+ adjustment: -1,
811
+ adjustmentType: 'fixed',
812
+ };
813
+
814
+ vi.mocked(actions.getSheetValue)
815
+ .mockResolvedValueOnce(-10000)
816
+ .mockResolvedValueOnce(-10000)
817
+ .mockResolvedValueOnce(-10000);
818
+
819
+ const result = await CategoryTemplateContext.runAverage(
820
+ template,
821
+ instance,
822
+ );
823
+ expect(result).toBe(9900);
824
+ });
825
+ });
826
+
827
+ describe('runBy', () => {
828
+ let instance: TestCategoryTemplateContext;
829
+
830
+ beforeEach(() => {
831
+ const category: CategoryEntity = {
832
+ id: 'test',
833
+ name: 'Test Category',
834
+ group: 'test-group',
835
+ is_income: false,
836
+ };
837
+ instance = new TestCategoryTemplateContext(
838
+ [
839
+ {
840
+ type: 'by',
841
+ amount: 1000,
842
+ month: '2024-03',
843
+ directive: 'template',
844
+ priority: 1,
845
+ },
846
+ {
847
+ type: 'by',
848
+ amount: 2000,
849
+ month: '2024-06',
850
+ directive: 'template',
851
+ priority: 1,
852
+ },
853
+ ],
854
+ category,
855
+ '2024-01',
856
+ 0,
857
+ 0,
858
+ );
859
+ });
860
+
861
+ it('should calculate monthly amount needed for multiple targets', () => {
862
+ const result = CategoryTemplateContext.runBy(instance);
863
+ expect(result).toBe(66667);
864
+ });
865
+
866
+ it('should handle repeating targets', () => {
867
+ instance = new TestCategoryTemplateContext(
868
+ [
869
+ {
870
+ type: 'by',
871
+ amount: 1000,
872
+ month: '2023-03',
873
+ repeat: 12,
874
+ directive: 'template',
875
+ priority: 1,
876
+ },
877
+ {
878
+ type: 'by',
879
+ amount: 2000,
880
+ month: '2023-06',
881
+ repeat: 12,
882
+ directive: 'template',
883
+ priority: 1,
884
+ },
885
+ ],
886
+ instance.category,
887
+ '2024-01',
888
+ 0,
889
+ 0,
890
+ );
891
+
892
+ const result = CategoryTemplateContext.runBy(instance);
893
+ expect(result).toBe(83333);
894
+ });
895
+
896
+ it('should handle existing balance', () => {
897
+ instance = new TestCategoryTemplateContext(
898
+ [
899
+ {
900
+ type: 'by',
901
+ amount: 1000,
902
+ month: '2024-03',
903
+ directive: 'template',
904
+ priority: 1,
905
+ },
906
+ {
907
+ type: 'by',
908
+ amount: 2000,
909
+ month: '2024-06',
910
+ directive: 'template',
911
+ priority: 1,
912
+ },
913
+ ],
914
+ instance.category,
915
+ '2024-01',
916
+ 500,
917
+ 0,
918
+ );
919
+
920
+ const result = CategoryTemplateContext.runBy(instance);
921
+ expect(result).toBe(66500); // (1000 + 2000 - 5) / 3
922
+ });
923
+ });
924
+
925
+ describe('template priorities', () => {
926
+ it('should handle multiple templates with priorities and insufficient funds', async () => {
927
+ const category: CategoryEntity = {
928
+ id: 'test',
929
+ name: 'Test Category',
930
+ group: 'test-group',
931
+ is_income: false,
932
+ };
933
+ const templates: Template[] = [
934
+ {
935
+ type: 'simple',
936
+ monthly: 100,
937
+ directive: 'template',
938
+ priority: 1,
939
+ },
940
+ {
941
+ type: 'simple',
942
+ monthly: 200,
943
+ directive: 'template',
944
+ priority: 1,
945
+ },
946
+ ];
947
+ const instance = new TestCategoryTemplateContext(
948
+ templates,
949
+ category,
950
+ '2024-01',
951
+ 0,
952
+ 0,
953
+ );
954
+ const result = await instance.runTemplatesForPriority(1, 150, 150);
955
+ expect(result).toBe(150); // Max out at available funds
956
+ });
957
+ });
958
+
959
+ describe('category limits', () => {
960
+ it('should not budget over monthly limit', async () => {
961
+ const category: CategoryEntity = {
962
+ id: 'test',
963
+ name: 'Test Category',
964
+ group: 'test-group',
965
+ is_income: false,
966
+ };
967
+ const templates: Template[] = [
968
+ {
969
+ type: 'simple',
970
+ monthly: 100,
971
+ limit: { amount: 150, hold: false, period: 'monthly' },
972
+ directive: 'template',
973
+ priority: 1,
974
+ },
975
+ ];
976
+ const instance = new TestCategoryTemplateContext(
977
+ templates,
978
+ category,
979
+ '2024-01',
980
+ 9000,
981
+ 0,
982
+ );
983
+ const result = await instance.runTemplatesForPriority(1, 10000, 10000);
984
+ expect(result).toBe(6000); //150 - 90
985
+ });
986
+
987
+ it('should handle hold flag when limit is reached', async () => {
988
+ const category: CategoryEntity = {
989
+ id: 'test',
990
+ name: 'Test Category',
991
+ group: 'test-group',
992
+ is_income: false,
993
+ };
994
+ const templates: Template[] = [
995
+ {
996
+ type: 'simple',
997
+ monthly: 100,
998
+ limit: { amount: 200, hold: true, period: 'monthly' },
999
+ directive: 'template',
1000
+ priority: 1,
1001
+ },
1002
+ ];
1003
+ const instance = new TestCategoryTemplateContext(
1004
+ templates,
1005
+ category,
1006
+ '2024-01',
1007
+ 300,
1008
+ 0,
1009
+ );
1010
+ const result = instance.getLimitExcess();
1011
+ expect(result).toBe(0);
1012
+ });
1013
+
1014
+ it('should remove funds if over limit', async () => {
1015
+ const category: CategoryEntity = {
1016
+ id: 'test',
1017
+ name: 'Test Category',
1018
+ group: 'test-group',
1019
+ is_income: false,
1020
+ };
1021
+ const templates: Template[] = [
1022
+ {
1023
+ type: 'simple',
1024
+ monthly: 100,
1025
+ limit: { amount: 200, hold: false, period: 'monthly' },
1026
+ directive: 'template',
1027
+ priority: 1,
1028
+ },
1029
+ ];
1030
+ const instance = new TestCategoryTemplateContext(
1031
+ templates,
1032
+ category,
1033
+ '2024-01',
1034
+ 30000,
1035
+ 0,
1036
+ );
1037
+ const result = instance.getLimitExcess();
1038
+ expect(result).toBe(10000);
1039
+ });
1040
+ });
1041
+
1042
+ describe('remainder templates', () => {
1043
+ it('should distribute available funds based on weight', async () => {
1044
+ const category: CategoryEntity = {
1045
+ id: 'test',
1046
+ name: 'Test Category',
1047
+ group: 'test-group',
1048
+ is_income: false,
1049
+ };
1050
+ const templates: Template[] = [
1051
+ {
1052
+ type: 'remainder',
1053
+ weight: 2,
1054
+ directive: 'template',
1055
+ priority: null,
1056
+ },
1057
+ ];
1058
+ const instance = new TestCategoryTemplateContext(
1059
+ templates,
1060
+ category,
1061
+ '2024-01',
1062
+ 0,
1063
+ 0,
1064
+ );
1065
+ const result = instance.runRemainder(100, 50);
1066
+ expect(result).toBe(100); // 2 * 50 = 100
1067
+ });
1068
+
1069
+ it('remainder should handle last cent', async () => {
1070
+ const category: CategoryEntity = {
1071
+ id: 'test',
1072
+ name: 'Test Category',
1073
+ group: 'test-group',
1074
+ is_income: false,
1075
+ };
1076
+ const templates: Template[] = [
1077
+ {
1078
+ type: 'remainder',
1079
+ weight: 1,
1080
+ directive: 'template',
1081
+ priority: null,
1082
+ },
1083
+ ];
1084
+ const instance = new TestCategoryTemplateContext(
1085
+ templates,
1086
+ category,
1087
+ '2024-01',
1088
+ 0,
1089
+ 0,
1090
+ );
1091
+ const result = instance.runRemainder(101, 100);
1092
+ expect(result).toBe(101);
1093
+ });
1094
+
1095
+ it('remainder wont over budget', async () => {
1096
+ const category: CategoryEntity = {
1097
+ id: 'test',
1098
+ name: 'Test Category',
1099
+ group: 'test-group',
1100
+ is_income: false,
1101
+ };
1102
+ const templates: Template[] = [
1103
+ {
1104
+ type: 'remainder',
1105
+ weight: 1,
1106
+ directive: 'template',
1107
+ priority: null,
1108
+ },
1109
+ ];
1110
+ const instance = new TestCategoryTemplateContext(
1111
+ templates,
1112
+ category,
1113
+ '2024-01',
1114
+ 0,
1115
+ 0,
1116
+ );
1117
+ const result = instance.runRemainder(99, 100);
1118
+ expect(result).toBe(99);
1119
+ });
1120
+ });
1121
+
1122
+ describe('full process', () => {
1123
+ it('should handle priority limits through the entire process', async () => {
1124
+ const category: CategoryEntity = {
1125
+ id: 'test',
1126
+ name: 'Test Category',
1127
+ group: 'test-group',
1128
+ is_income: false,
1129
+ };
1130
+ const templates: Template[] = [
1131
+ {
1132
+ type: 'simple',
1133
+ monthly: 100,
1134
+ directive: 'template',
1135
+ priority: 1,
1136
+ },
1137
+ {
1138
+ type: 'simple',
1139
+ monthly: 200,
1140
+ directive: 'template',
1141
+ priority: 2,
1142
+ },
1143
+ {
1144
+ type: 'remainder',
1145
+ weight: 1,
1146
+ directive: 'template',
1147
+ priority: null,
1148
+ },
1149
+ ];
1150
+
1151
+ // Mock the sheet values needed for init
1152
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
1153
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
1154
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1155
+ mockPreferences(false, 'USD');
1156
+
1157
+ // Initialize the template
1158
+ const instance = await CategoryTemplateContext.init(
1159
+ templates,
1160
+ category,
1161
+ '2024-01',
1162
+ 0,
1163
+ );
1164
+
1165
+ // Run each priority level separately
1166
+ const priority1Result = await instance.runTemplatesForPriority(
1167
+ 1,
1168
+ 15000,
1169
+ 15000,
1170
+ );
1171
+ const priority2Result = await instance.runTemplatesForPriority(
1172
+ 2,
1173
+ 15000 - priority1Result,
1174
+ 15000,
1175
+ );
1176
+
1177
+ // Get the final values
1178
+ const values = instance.getValues();
1179
+
1180
+ // Verify the results
1181
+ expect(priority1Result).toBe(10000); // Should get full amount for priority 1
1182
+ expect(priority2Result).toBe(5000); // Should get remaining funds for priority 2
1183
+ expect(values.budgeted).toBe(15000); // Should match the total of both priorities
1184
+ expect(values.goal).toBe(30000); // Should be the sum of all template amounts
1185
+ expect(values.longGoal).toBe(null); // No goal template
1186
+ });
1187
+
1188
+ it('should handle category limits through the entire process', async () => {
1189
+ const category: CategoryEntity = {
1190
+ id: 'test',
1191
+ name: 'Test Category',
1192
+ group: 'test-group',
1193
+ is_income: false,
1194
+ };
1195
+ const templates: Template[] = [
1196
+ {
1197
+ type: 'simple',
1198
+ monthly: 100,
1199
+ directive: 'template',
1200
+ priority: 1,
1201
+ },
1202
+ {
1203
+ type: 'simple',
1204
+ monthly: 200,
1205
+ directive: 'template',
1206
+ priority: 1,
1207
+ },
1208
+ {
1209
+ type: 'simple',
1210
+ limit: { amount: 150, hold: false, period: 'monthly' },
1211
+ directive: 'template',
1212
+ priority: 1,
1213
+ },
1214
+ ];
1215
+
1216
+ // Mock the sheet values needed for init
1217
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
1218
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
1219
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1220
+ mockPreferences(false, 'USD');
1221
+
1222
+ // Initialize the template
1223
+ const instance = await CategoryTemplateContext.init(
1224
+ templates,
1225
+ category,
1226
+ '2024-01',
1227
+ 0,
1228
+ );
1229
+
1230
+ // Run the templates with more than enough funds
1231
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
1232
+
1233
+ // Get the final values
1234
+ const values = instance.getValues();
1235
+
1236
+ // Verify the results
1237
+ expect(result).toBe(15000); // Should be limited by the category limit
1238
+ expect(values.budgeted).toBe(15000); // Should match the limit
1239
+ expect(values.goal).toBe(15000); // Should be the limit amount
1240
+ expect(values.longGoal).toBe(null); // No goal template
1241
+ });
1242
+
1243
+ it('should handle remainder template at the end of the process', async () => {
1244
+ const category: CategoryEntity = {
1245
+ id: 'test',
1246
+ name: 'Test Category',
1247
+ group: 'test-group',
1248
+ is_income: false,
1249
+ };
1250
+ const templates: Template[] = [
1251
+ {
1252
+ type: 'simple',
1253
+ monthly: 100,
1254
+ directive: 'template',
1255
+ priority: 1,
1256
+ },
1257
+ {
1258
+ type: 'simple',
1259
+ monthly: 200,
1260
+ directive: 'template',
1261
+ priority: 1,
1262
+ },
1263
+ {
1264
+ type: 'remainder',
1265
+ weight: 1,
1266
+ directive: 'template',
1267
+ priority: null,
1268
+ },
1269
+ ];
1270
+
1271
+ // Mock the sheet values needed for init
1272
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
1273
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
1274
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1275
+ mockPreferences(false, 'USD');
1276
+
1277
+ // Initialize the template
1278
+ const instance = await CategoryTemplateContext.init(
1279
+ templates,
1280
+ category,
1281
+ '2024-01',
1282
+ 0,
1283
+ );
1284
+ const weight = instance.getRemainderWeight();
1285
+
1286
+ // Run the templates with more than enough funds
1287
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
1288
+
1289
+ // Run the remainder template
1290
+ const perWeight = (100000 - result) / weight;
1291
+ const remainderResult = instance.runRemainder(perWeight, perWeight);
1292
+
1293
+ // Get the final values
1294
+ const values = instance.getValues();
1295
+
1296
+ // Verify the results
1297
+ expect(result).toBe(30000); // Should get full amount for both simple templates
1298
+ expect(remainderResult).toBe(70000); // Should get remaining funds
1299
+ expect(values.budgeted).toBe(100000); // Should match the total of all templates
1300
+ expect(values.goal).toBe(30000); // Should be the sum of the simple templates
1301
+ expect(values.longGoal).toBe(null); // No goal template
1302
+ });
1303
+
1304
+ it('should handle goal template through the entire process', async () => {
1305
+ const category: CategoryEntity = {
1306
+ id: 'test',
1307
+ name: 'Test Category',
1308
+ group: 'test-group',
1309
+ is_income: false,
1310
+ };
1311
+ const templates: Template[] = [
1312
+ {
1313
+ type: 'simple',
1314
+ monthly: 100,
1315
+ directive: 'template',
1316
+ priority: 1,
1317
+ },
1318
+ {
1319
+ type: 'simple',
1320
+ monthly: 200,
1321
+ directive: 'template',
1322
+ priority: 1,
1323
+ },
1324
+ {
1325
+ type: 'goal',
1326
+ amount: 1000,
1327
+ directive: 'goal',
1328
+ },
1329
+ ];
1330
+
1331
+ // Mock the sheet values needed for init
1332
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
1333
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
1334
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1335
+ mockPreferences(false, 'USD');
1336
+
1337
+ // Initialize the template
1338
+ const instance = await CategoryTemplateContext.init(
1339
+ templates,
1340
+ category,
1341
+ '2024-01',
1342
+ 0,
1343
+ );
1344
+
1345
+ // Run the templates with more than enough funds
1346
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
1347
+
1348
+ // Get the final values
1349
+ const values = instance.getValues();
1350
+
1351
+ // Verify the results
1352
+ expect(result).toBe(30000); // Should get full amount for both simple templates
1353
+ expect(values.budgeted).toBe(30000); // Should match the result
1354
+ expect(values.goal).toBe(100000); // Should be the goal amount
1355
+ expect(values.longGoal).toBe(true); // Should have a long goal
1356
+ expect(instance.isGoalOnly()).toBe(false); // Should not be goal only
1357
+ });
1358
+
1359
+ it('should handle goal-only template through the entire process', async () => {
1360
+ const category: CategoryEntity = {
1361
+ id: 'test',
1362
+ name: 'Test Category',
1363
+ group: 'test-group',
1364
+ is_income: false,
1365
+ };
1366
+ const templates: Template[] = [
1367
+ {
1368
+ type: 'goal',
1369
+ amount: 1000,
1370
+ directive: 'goal',
1371
+ },
1372
+ ];
1373
+
1374
+ // Mock the sheet values needed for init
1375
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(10000); // lastMonthBalance
1376
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
1377
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1378
+ mockPreferences(false, 'USD');
1379
+
1380
+ // Initialize the template
1381
+ const instance = await CategoryTemplateContext.init(
1382
+ templates,
1383
+ category,
1384
+ '2024-01',
1385
+ 10000,
1386
+ );
1387
+
1388
+ expect(instance.isGoalOnly()).toBe(true); // Should be goal only
1389
+ // Get the final values
1390
+ const values = instance.getValues();
1391
+
1392
+ // Verify the results
1393
+ expect(values.budgeted).toBe(10000);
1394
+ expect(values.goal).toBe(100000); // Should be the goal amount
1395
+ expect(values.longGoal).toBe(true); // Should have a long goal
1396
+ });
1397
+
1398
+ it('should handle hide fraction', async () => {
1399
+ const category: CategoryEntity = {
1400
+ id: 'test',
1401
+ name: 'Test Category',
1402
+ group: 'test-group',
1403
+ is_income: false,
1404
+ };
1405
+ const templates: Template[] = [
1406
+ {
1407
+ type: 'simple',
1408
+ monthly: 100.89,
1409
+ directive: 'template',
1410
+ priority: 1,
1411
+ },
1412
+ {
1413
+ type: 'goal',
1414
+ amount: 1000,
1415
+ directive: 'goal',
1416
+ },
1417
+ ];
1418
+
1419
+ // Mock the sheet values needed for init
1420
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
1421
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
1422
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1423
+ mockPreferences(true, 'USD');
1424
+
1425
+ // Initialize the template
1426
+ const instance = await CategoryTemplateContext.init(
1427
+ templates,
1428
+ category,
1429
+ '2024-01',
1430
+ 0,
1431
+ );
1432
+
1433
+ // Run the templates with more than enough funds
1434
+ const result = await instance.runTemplatesForPriority(1, 100000, 100000);
1435
+
1436
+ // Get the final values
1437
+ const values = instance.getValues();
1438
+
1439
+ // Verify the results
1440
+ expect(result).toBe(10100); // Should get full amount rounded up
1441
+ expect(values.budgeted).toBe(10100); // Should match the result
1442
+ expect(values.goal).toBe(100000); // Should be the goal amount
1443
+ expect(values.longGoal).toBe(true); // Should have a long goal
1444
+ expect(instance.isGoalOnly()).toBe(false); // Should not be goal only
1445
+ });
1446
+ });
1447
+
1448
+ describe('JPY currency', () => {
1449
+ it('should handle simple template with JPY correctly', async () => {
1450
+ const category: CategoryEntity = {
1451
+ id: 'test',
1452
+ name: 'Test Category',
1453
+ group: 'test-group',
1454
+ is_income: false,
1455
+ };
1456
+ const template: Template = {
1457
+ type: 'simple',
1458
+ monthly: 50,
1459
+ directive: 'template',
1460
+ priority: 1,
1461
+ };
1462
+
1463
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
1464
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
1465
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1466
+ mockPreferences(true, 'JPY');
1467
+
1468
+ const instance = await CategoryTemplateContext.init(
1469
+ [template],
1470
+ category,
1471
+ '2024-01',
1472
+ 0,
1473
+ );
1474
+
1475
+ await instance.runTemplatesForPriority(1, 100000, 100000);
1476
+ const values = instance.getValues();
1477
+
1478
+ expect(values.budgeted).toBe(50);
1479
+ });
1480
+
1481
+ it('should handle small amounts with JPY correctly', async () => {
1482
+ const category: CategoryEntity = {
1483
+ id: 'test',
1484
+ name: 'Test Category',
1485
+ group: 'test-group',
1486
+ is_income: false,
1487
+ };
1488
+ const template: Template = {
1489
+ type: 'simple',
1490
+ monthly: 5,
1491
+ directive: 'template',
1492
+ priority: 1,
1493
+ };
1494
+
1495
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
1496
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
1497
+ vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
1498
+ mockPreferences(true, 'JPY');
1499
+
1500
+ const instance = await CategoryTemplateContext.init(
1501
+ [template],
1502
+ category,
1503
+ '2024-01',
1504
+ 0,
1505
+ );
1506
+
1507
+ await instance.runTemplatesForPriority(1, 100000, 100000);
1508
+ const values = instance.getValues();
1509
+
1510
+ expect(values.budgeted).toBe(5);
1511
+ });
1512
+
1513
+ it('should handle larger amounts with JPY correctly', async () => {
1514
+ const category: CategoryEntity = {
1515
+ id: 'test',
1516
+ name: 'Test Category',
1517
+ group: 'test-group',
1518
+ is_income: false,
1519
+ };
1520
+ const template: Template = {
1521
+ type: 'simple',
1522
+ monthly: 250,
1523
+ directive: 'template',
1524
+ priority: 1,
1525
+ };
1526
+
1527
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
1528
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
1529
+ mockPreferences(true, 'JPY');
1530
+
1531
+ const instance = await CategoryTemplateContext.init(
1532
+ [template],
1533
+ category,
1534
+ '2024-01',
1535
+ 0,
1536
+ );
1537
+
1538
+ await instance.runTemplatesForPriority(1, 100000, 100000);
1539
+ const values = instance.getValues();
1540
+
1541
+ expect(values.budgeted).toBe(250);
1542
+ });
1543
+
1544
+ it('should handle weekly limit with JPY correctly', async () => {
1545
+ const category: CategoryEntity = {
1546
+ id: 'test',
1547
+ name: 'Test Category',
1548
+ group: 'test-group',
1549
+ is_income: false,
1550
+ };
1551
+ const template: Template = {
1552
+ type: 'simple',
1553
+ limit: {
1554
+ amount: 100,
1555
+ hold: false,
1556
+ period: 'weekly',
1557
+ start: '2024-01-01',
1558
+ },
1559
+ directive: 'template',
1560
+ priority: 1,
1561
+ };
1562
+
1563
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
1564
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
1565
+ mockPreferences(true, 'JPY');
1566
+
1567
+ const instance = await CategoryTemplateContext.init(
1568
+ [template],
1569
+ category,
1570
+ '2024-01',
1571
+ 0,
1572
+ );
1573
+
1574
+ const result = CategoryTemplateContext.runSimple(template, instance);
1575
+
1576
+ expect(result).toBeGreaterThanOrEqual(400);
1577
+ expect(result).toBeLessThanOrEqual(500);
1578
+ });
1579
+
1580
+ it('should handle periodic template with JPY correctly', async () => {
1581
+ const category: CategoryEntity = {
1582
+ id: 'test',
1583
+ name: 'Test Category',
1584
+ group: 'test-group',
1585
+ is_income: false,
1586
+ };
1587
+ const template: Template = {
1588
+ type: 'periodic',
1589
+ amount: 1000,
1590
+ period: { period: 'week', amount: 1 },
1591
+ starting: '2024-01-01',
1592
+ directive: 'template',
1593
+ priority: 1,
1594
+ };
1595
+
1596
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
1597
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
1598
+ mockPreferences(true, 'JPY');
1599
+
1600
+ const instance = await CategoryTemplateContext.init(
1601
+ [template],
1602
+ category,
1603
+ '2024-01',
1604
+ 0,
1605
+ );
1606
+
1607
+ await instance.runTemplatesForPriority(1, 100000, 100000);
1608
+ const values = instance.getValues();
1609
+
1610
+ expect(values.budgeted).toBeGreaterThan(3500);
1611
+ expect(values.budgeted).toBeLessThan(5500);
1612
+ });
1613
+
1614
+ it('should compare JPY vs USD for same template', async () => {
1615
+ const category: CategoryEntity = {
1616
+ id: 'test',
1617
+ name: 'Test Category',
1618
+ group: 'test-group',
1619
+ is_income: false,
1620
+ };
1621
+ const template: Template = {
1622
+ type: 'simple',
1623
+ monthly: 100,
1624
+ directive: 'template',
1625
+ priority: 1,
1626
+ };
1627
+
1628
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
1629
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
1630
+ mockPreferences(true, 'JPY');
1631
+
1632
+ const instanceJPY = await CategoryTemplateContext.init(
1633
+ [template],
1634
+ category,
1635
+ '2024-01',
1636
+ 0,
1637
+ );
1638
+ await instanceJPY.runTemplatesForPriority(1, 100000, 100000);
1639
+ const valuesJPY = instanceJPY.getValues();
1640
+
1641
+ vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
1642
+ vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
1643
+ mockPreferences(false, 'USD');
1644
+
1645
+ const instanceUSD = await CategoryTemplateContext.init(
1646
+ [template],
1647
+ category,
1648
+ '2024-01',
1649
+ 0,
1650
+ );
1651
+ await instanceUSD.runTemplatesForPriority(1, 100000, 100000);
1652
+ const valuesUSD = instanceUSD.getValues();
1653
+
1654
+ expect(valuesJPY.budgeted).toBe(100);
1655
+ expect(valuesUSD.budgeted).toBe(10000);
1656
+ });
1657
+ });
1658
+ });