@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,184 @@
1
+ import type { Currency } from '#shared/currencies';
2
+ import type { CategoryEntity } from '../../types/models';
3
+ import * as db from '../db';
4
+ import { Rule } from '../rules';
5
+ import { getRuleForSchedule } from '../schedules/app';
6
+
7
+ import { isReflectBudget } from './actions';
8
+ import { runSchedule } from './schedule-template';
9
+
10
+ vi.mock('../db');
11
+ vi.mock('./actions');
12
+ vi.mock('../schedules/app', async () => {
13
+ const actualModule = await vi.importActual('../schedules/app');
14
+ return {
15
+ ...actualModule,
16
+ getRuleForSchedule: vi.fn(),
17
+ };
18
+ });
19
+
20
+ describe('runSchedule', () => {
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ });
24
+
25
+ it('should return correct budget when recurring schedule set', async () => {
26
+ // Given
27
+ const template_lines = [
28
+ {
29
+ type: 'schedule',
30
+ name: 'Test Schedule',
31
+ priority: 0,
32
+ directive: 'template',
33
+ } as const,
34
+ ];
35
+ const current_month = '2024-08-01';
36
+ const balance = 0;
37
+ const remainder = 0;
38
+ const last_month_balance = 0;
39
+ const to_budget = 0;
40
+ const errors: string[] = [];
41
+ const category = { id: '1', name: 'Test Category' } as CategoryEntity;
42
+ const currency: Currency = {
43
+ code: '',
44
+ symbol: '',
45
+ name: '',
46
+ decimalPlaces: 2,
47
+ numberFormat: 'comma-dot',
48
+ symbolFirst: false,
49
+ };
50
+
51
+ vi.mocked(db.first).mockResolvedValue({ id: 1, completed: 0 });
52
+ vi.mocked(getRuleForSchedule).mockResolvedValue(
53
+ new Rule({
54
+ id: 'test',
55
+ stage: 'pre',
56
+ conditionsOp: 'and',
57
+ conditions: [
58
+ {
59
+ op: 'is',
60
+ field: 'date',
61
+ value: {
62
+ start: '2024-08-01',
63
+ interval: 1,
64
+ frequency: 'monthly',
65
+ patterns: [],
66
+ skipWeekend: false,
67
+ weekendSolveMode: 'before',
68
+ endMode: 'never',
69
+ endOccurrences: 1,
70
+ endDate: '2024-08-04',
71
+ },
72
+ type: 'date',
73
+ },
74
+ {
75
+ op: 'is',
76
+ field: 'amount',
77
+ value: -10000,
78
+ type: 'number',
79
+ },
80
+ ],
81
+ actions: [],
82
+ }),
83
+ );
84
+ vi.mocked(isReflectBudget).mockReturnValue(false);
85
+
86
+ // When
87
+ const result = await runSchedule(
88
+ template_lines,
89
+ current_month,
90
+ balance,
91
+ remainder,
92
+ last_month_balance,
93
+ to_budget,
94
+ errors,
95
+ category,
96
+ currency,
97
+ );
98
+
99
+ // Then
100
+ expect(result.to_budget).toBe(10000);
101
+ expect(result.errors).toHaveLength(0);
102
+ expect(result.remainder).toBe(0);
103
+ });
104
+
105
+ it('should return correct budget when yearly recurring schedule set and balance is greater than target', async () => {
106
+ // Given
107
+ const template_lines = [
108
+ {
109
+ type: 'schedule',
110
+ name: 'Test Schedule',
111
+ directive: 'template',
112
+ priority: 0,
113
+ } as const,
114
+ ];
115
+ const current_month = '2024-09-01';
116
+ const balance = 12000;
117
+ const remainder = 0;
118
+ const last_month_balance = 12000;
119
+ const to_budget = 0;
120
+ const errors: string[] = [];
121
+ const category = { id: '1', name: 'Test Category' } as CategoryEntity;
122
+ const currency: Currency = {
123
+ code: '',
124
+ symbol: '',
125
+ name: '',
126
+ decimalPlaces: 2,
127
+ numberFormat: 'comma-dot',
128
+ symbolFirst: false,
129
+ };
130
+
131
+ vi.mocked(db.first).mockResolvedValue({ id: 1, completed: 0 });
132
+ vi.mocked(getRuleForSchedule).mockResolvedValue(
133
+ new Rule({
134
+ id: 'test',
135
+ stage: 'pre',
136
+ conditionsOp: 'and',
137
+ conditions: [
138
+ {
139
+ op: 'is',
140
+ field: 'date',
141
+ value: {
142
+ start: '2024-08-01',
143
+ interval: 1,
144
+ frequency: 'yearly',
145
+ patterns: [],
146
+ skipWeekend: false,
147
+ weekendSolveMode: 'before',
148
+ endMode: 'never',
149
+ endOccurrences: 1,
150
+ endDate: '2024-08-04',
151
+ },
152
+ type: 'date',
153
+ },
154
+ {
155
+ op: 'is',
156
+ field: 'amount',
157
+ value: -12000,
158
+ type: 'number',
159
+ },
160
+ ],
161
+ actions: [],
162
+ }),
163
+ );
164
+ vi.mocked(isReflectBudget).mockReturnValue(false);
165
+
166
+ // When
167
+ const result = await runSchedule(
168
+ template_lines,
169
+ current_month,
170
+ balance,
171
+ remainder,
172
+ last_month_balance,
173
+ to_budget,
174
+ errors,
175
+ category,
176
+ currency,
177
+ );
178
+
179
+ // Then
180
+ expect(result.to_budget).toBe(1000);
181
+ expect(result.errors).toHaveLength(0);
182
+ expect(result.remainder).toBe(0);
183
+ });
184
+ });
@@ -0,0 +1,351 @@
1
+ // @ts-strict-ignore
2
+
3
+ import type { Currency } from '#shared/currencies';
4
+ import { amountToInteger } from '#shared/util';
5
+ import * as monthUtils from '../../shared/months';
6
+ import {
7
+ extractScheduleConds,
8
+ getDateWithSkippedWeekend,
9
+ getNextDate,
10
+ } from '../../shared/schedules';
11
+ import type { CategoryEntity } from '../../types/models';
12
+ import type { ScheduleTemplate, Template } from '../../types/models/templates';
13
+ import * as db from '../db';
14
+ import { getRuleForSchedule } from '../schedules/app';
15
+
16
+ import { getSheetValue, isReflectBudget } from './actions';
17
+
18
+ type ScheduleTemplateTarget = {
19
+ name: string;
20
+ target: number;
21
+ next_date_string: string;
22
+ target_interval: number;
23
+ target_frequency: string | undefined;
24
+ num_months: number;
25
+ completed: number;
26
+ full: boolean;
27
+ repeat: boolean;
28
+ };
29
+
30
+ async function createScheduleList(
31
+ templates: ScheduleTemplate[],
32
+ current_month: string,
33
+ category: CategoryEntity,
34
+ currency: Currency,
35
+ ) {
36
+ const t: Array<ScheduleTemplateTarget> = [];
37
+ const errors: string[] = [];
38
+
39
+ for (const template of templates) {
40
+ const { id: sid, completed } = await db.first<
41
+ Pick<db.DbSchedule, 'id' | 'completed'>
42
+ >(
43
+ 'SELECT id, completed FROM schedules WHERE TRIM(name) = ? AND tombstone = 0',
44
+ [template.name],
45
+ );
46
+ const rule = await getRuleForSchedule(sid);
47
+ const conditions = rule.serialize().conditions;
48
+ const { date: dateConditions, amount: amountCondition } =
49
+ extractScheduleConds(conditions);
50
+ let scheduleAmount =
51
+ amountCondition.op === 'isbetween'
52
+ ? Math.round(amountCondition.value.num1 + amountCondition.value.num2) /
53
+ 2
54
+ : amountCondition.value;
55
+ // Apply adjustment percentage if specified
56
+ if (template.adjustment !== undefined && template.adjustmentType) {
57
+ switch (template.adjustmentType) {
58
+ case 'percent': {
59
+ const adjustmentFactor = 1 + template.adjustment / 100;
60
+ scheduleAmount = scheduleAmount * adjustmentFactor;
61
+ break;
62
+ }
63
+ case 'fixed': {
64
+ const sign = scheduleAmount < 0 ? -1 : 1;
65
+ scheduleAmount +=
66
+ sign * amountToInteger(template.adjustment, currency.decimalPlaces);
67
+ break;
68
+ }
69
+
70
+ default:
71
+ //no valid adjustment was found
72
+ }
73
+ }
74
+
75
+ scheduleAmount = Math.round(scheduleAmount);
76
+
77
+ const { amount: postRuleAmount, subtransactions } = rule.execActions({
78
+ amount: scheduleAmount,
79
+ category: category.id,
80
+ subtransactions: [],
81
+ });
82
+ const categorySubtransactions = subtransactions?.filter(
83
+ t => t.category === category.id,
84
+ );
85
+
86
+ // Unless the current category is relevant to the schedule, target the post-rule amount.
87
+ const sign = category.is_income ? 1 : -1;
88
+ const target =
89
+ sign *
90
+ (categorySubtransactions?.length
91
+ ? categorySubtransactions.reduce((acc, t) => acc + t.amount, 0)
92
+ : (postRuleAmount ?? scheduleAmount));
93
+
94
+ const next_date_string = getNextDate(
95
+ dateConditions,
96
+ monthUtils._parse(current_month),
97
+ );
98
+ const target_interval = dateConditions.value.interval
99
+ ? dateConditions.value.interval
100
+ : 1;
101
+ const target_frequency = dateConditions.value.frequency;
102
+ const isRepeating =
103
+ Object(dateConditions.value) === dateConditions.value &&
104
+ 'frequency' in dateConditions.value;
105
+ const num_months = monthUtils.differenceInCalendarMonths(
106
+ next_date_string,
107
+ current_month,
108
+ );
109
+ if (num_months < 0) {
110
+ //non-repeating schedules could be negative
111
+ errors.push(`Schedule ${template.name} is in the Past.`);
112
+ } else {
113
+ t.push({
114
+ target,
115
+ next_date_string,
116
+ target_interval,
117
+ target_frequency,
118
+ num_months,
119
+ completed,
120
+ //started,
121
+ full: template.full === null ? false : template.full,
122
+ repeat: isRepeating,
123
+ name: template.name,
124
+ });
125
+ if (!completed) {
126
+ if (isRepeating) {
127
+ let monthlyTarget = 0;
128
+ const nextMonth = monthUtils.addMonths(
129
+ current_month,
130
+ t[t.length - 1].num_months + 1,
131
+ );
132
+ let nextBaseDate = getNextDate(
133
+ dateConditions,
134
+ monthUtils._parse(current_month),
135
+ true,
136
+ );
137
+ let nextDate = dateConditions.value.skipWeekend
138
+ ? monthUtils.dayFromDate(
139
+ getDateWithSkippedWeekend(
140
+ monthUtils._parse(nextBaseDate),
141
+ dateConditions.value.weekendSolveMode,
142
+ ),
143
+ )
144
+ : nextBaseDate;
145
+ while (nextDate < nextMonth) {
146
+ monthlyTarget += -target;
147
+ const currentDate = nextBaseDate;
148
+ const oneDayLater = monthUtils.addDays(nextBaseDate, 1);
149
+ nextBaseDate = getNextDate(
150
+ dateConditions,
151
+ monthUtils._parse(oneDayLater),
152
+ true,
153
+ );
154
+ nextDate = dateConditions.value.skipWeekend
155
+ ? monthUtils.dayFromDate(
156
+ getDateWithSkippedWeekend(
157
+ monthUtils._parse(nextBaseDate),
158
+ dateConditions.value.weekendSolveMode,
159
+ ),
160
+ )
161
+ : nextBaseDate;
162
+ const diffDays = monthUtils.differenceInCalendarDays(
163
+ nextBaseDate,
164
+ currentDate,
165
+ );
166
+ if (!diffDays) {
167
+ // This can happen if the schedule has an end condition.
168
+ break;
169
+ }
170
+ }
171
+ t[t.length - 1].target = -monthlyTarget;
172
+ }
173
+ } else {
174
+ errors.push(
175
+ `Schedule ${template.name} is not active during the month in question.`,
176
+ );
177
+ }
178
+ }
179
+ }
180
+ return { t: t.filter(c => c.completed === 0), errors };
181
+ }
182
+
183
+ function getPayMonthOfTotal(t: ScheduleTemplateTarget[]) {
184
+ //return the contribution amounts of full or every month type schedules
185
+ let total = 0;
186
+ const schedules = t.filter(c => c.num_months === 0);
187
+ for (const schedule of schedules) {
188
+ total += schedule.target;
189
+ }
190
+ return total;
191
+ }
192
+
193
+ async function getSinkingContributionTotal(
194
+ t: ScheduleTemplateTarget[],
195
+ remainder: number,
196
+ last_month_balance: number,
197
+ ) {
198
+ //return the contribution amount if there is a balance carried in the category
199
+ let total = 0;
200
+ for (const [index, schedule] of t.entries()) {
201
+ remainder =
202
+ index === 0
203
+ ? schedule.target - last_month_balance
204
+ : schedule.target - remainder;
205
+ let tg = 0;
206
+ if (remainder >= 0) {
207
+ tg = remainder;
208
+ remainder = 0;
209
+ } else {
210
+ tg = 0;
211
+ remainder = Math.abs(remainder);
212
+ }
213
+ total += tg / (schedule.num_months + 1);
214
+ }
215
+ return total;
216
+ }
217
+
218
+ function getSinkingBaseContributionTotal(t: ScheduleTemplateTarget[]) {
219
+ //return only the base contribution of each schedule
220
+ let total = 0;
221
+ for (const schedule of t) {
222
+ let monthlyAmount = 0;
223
+ let prevDate;
224
+ let intervalMonths;
225
+ switch (schedule.target_frequency) {
226
+ case 'yearly':
227
+ monthlyAmount = schedule.target / schedule.target_interval / 12;
228
+ break;
229
+ case 'monthly':
230
+ monthlyAmount = schedule.target / schedule.target_interval;
231
+ break;
232
+ case 'weekly':
233
+ prevDate = monthUtils.subWeeks(
234
+ schedule.next_date_string,
235
+ schedule.target_interval,
236
+ );
237
+ intervalMonths = monthUtils.differenceInCalendarMonths(
238
+ schedule.next_date_string,
239
+ prevDate,
240
+ );
241
+ // shouldn't be possible, but better check
242
+ if (intervalMonths === 0) intervalMonths = 1;
243
+ monthlyAmount = schedule.target / intervalMonths;
244
+ break;
245
+ case 'daily':
246
+ prevDate = monthUtils.subDays(
247
+ schedule.next_date_string,
248
+ schedule.target_interval,
249
+ );
250
+ intervalMonths = monthUtils.differenceInCalendarMonths(
251
+ schedule.next_date_string,
252
+ prevDate,
253
+ );
254
+ // shouldn't be possible, but better check
255
+ if (intervalMonths === 0) intervalMonths = 1;
256
+ monthlyAmount = schedule.target / intervalMonths;
257
+ break;
258
+ default:
259
+ // default to same math as monthly for now for non-reoccuring
260
+ monthlyAmount = schedule.target / schedule.target_interval;
261
+ break;
262
+ }
263
+ total += monthlyAmount;
264
+ }
265
+ return total;
266
+ }
267
+
268
+ function getSinkingTotal(t: ScheduleTemplateTarget[]) {
269
+ //sum the total of all upcoming schedules
270
+ let total = 0;
271
+ for (const schedule of t) {
272
+ total += schedule.target;
273
+ }
274
+ return total;
275
+ }
276
+
277
+ export async function runSchedule(
278
+ template_lines: Template[],
279
+ current_month: string,
280
+ balance: number,
281
+ remainder: number,
282
+ last_month_balance: number,
283
+ to_budget: number,
284
+ errors: string[],
285
+ category: CategoryEntity,
286
+ currency: Currency,
287
+ ) {
288
+ const scheduleTemplates = template_lines.filter(t => t.type === 'schedule');
289
+
290
+ const t = await createScheduleList(
291
+ scheduleTemplates,
292
+ current_month,
293
+ category,
294
+ currency,
295
+ );
296
+ errors = errors.concat(t.errors);
297
+
298
+ const isPayMonthOf = c =>
299
+ c.full ||
300
+ ((c.target_frequency === 'monthly' || !c.target_frequency) &&
301
+ c.target_interval === 1 &&
302
+ c.num_months === 0) ||
303
+ (c.target_frequency === 'weekly' && c.target_interval <= 4) ||
304
+ (c.target_frequency === 'daily' && c.target_interval <= 31) ||
305
+ isReflectBudget();
306
+
307
+ const isSubMonthly = c =>
308
+ c.target_frequency === 'weekly' || c.target_frequency === 'daily';
309
+
310
+ const t_payMonthOf = t.t.filter(isPayMonthOf);
311
+ const t_sinking = t.t
312
+ .filter(c => !isPayMonthOf(c))
313
+ .sort((a, b) => a.next_date_string.localeCompare(b.next_date_string));
314
+ const numSubMonthly = t.t.filter(isSubMonthly).length;
315
+ const totalPayMonthOf = getPayMonthOfTotal(t_payMonthOf);
316
+ const totalSinking = getSinkingTotal(t_sinking);
317
+ const totalSinkingBaseContribution =
318
+ getSinkingBaseContributionTotal(t_sinking);
319
+ const lastMonthGoal = await getSheetValue(
320
+ monthUtils.sheetForMonth(monthUtils.subMonths(current_month, 1)),
321
+ `goal-${category.id}`,
322
+ );
323
+
324
+ // check and see if we should budget the full amount becaue the previous schedules
325
+ // haven't been paid yet, or if we can use the leftover balance for this month
326
+ // First option: check if the previous month doesn't have its monthly schedules paid yet
327
+ // Second option: check if the previous month needed less than this month and hasn't paid yet
328
+ if (
329
+ balance >= totalSinking + totalPayMonthOf ||
330
+ (lastMonthGoal < totalSinking + totalPayMonthOf &&
331
+ lastMonthGoal !== 0 &&
332
+ balance >= lastMonthGoal &&
333
+ numSubMonthly > 0)
334
+ ) {
335
+ to_budget += Math.round(totalPayMonthOf + totalSinkingBaseContribution);
336
+ } else {
337
+ const totalSinkingContribution = await getSinkingContributionTotal(
338
+ t_sinking,
339
+ remainder,
340
+ last_month_balance,
341
+ );
342
+ if (t_sinking.length === 0) {
343
+ to_budget +=
344
+ Math.round(totalPayMonthOf + totalSinkingContribution) -
345
+ last_month_balance;
346
+ } else {
347
+ to_budget += Math.round(totalPayMonthOf + totalSinkingContribution);
348
+ }
349
+ }
350
+ return { to_budget, errors, remainder };
351
+ }
@@ -0,0 +1,60 @@
1
+ import * as db from '../db';
2
+ import type { DbSchedule } from '../db';
3
+
4
+ import { GOAL_PREFIX, TEMPLATE_PREFIX } from './template-notes';
5
+
6
+ export async function resetCategoryGoalDefsWithNoTemplates(): Promise<void> {
7
+ await db.run(
8
+ `
9
+ UPDATE categories
10
+ SET goal_def = NULL
11
+ WHERE id NOT IN (SELECT n.id
12
+ FROM notes n
13
+ WHERE lower(note) LIKE '%${TEMPLATE_PREFIX}%'
14
+ OR lower(note) LIKE '%${GOAL_PREFIX}%')
15
+ AND COALESCE(JSON_EXTRACT(template_settings, '$.source'), 'notes') <> 'ui'
16
+ `,
17
+ );
18
+ }
19
+
20
+ export type CategoryWithTemplateNote = {
21
+ id: string;
22
+ name: string;
23
+ note: string;
24
+ };
25
+
26
+ export async function getCategoriesWithTemplateNotes(): Promise<
27
+ CategoryWithTemplateNote[]
28
+ > {
29
+ return await db.all<
30
+ Pick<db.DbCategory, 'id' | 'name'> & Pick<db.DbNote, 'note'>
31
+ >(
32
+ `
33
+ SELECT c.id AS id, c.name as name, n.note AS note
34
+ FROM notes n
35
+ JOIN categories c ON n.id = c.id
36
+ WHERE c.id = n.id
37
+ AND c.tombstone = 0
38
+ AND COALESCE(JSON_EXTRACT(c.template_settings, '$.source'), 'notes') <> 'ui'
39
+ AND (lower(note) LIKE '%${TEMPLATE_PREFIX}%'
40
+ OR lower(note) LIKE '%${GOAL_PREFIX}%')
41
+ `,
42
+ );
43
+ }
44
+
45
+ export async function getActiveSchedules() {
46
+ return await db.all<
47
+ Pick<
48
+ DbSchedule,
49
+ | 'id'
50
+ | 'rule'
51
+ | 'active'
52
+ | 'completed'
53
+ | 'posts_transaction'
54
+ | 'tombstone'
55
+ | 'name'
56
+ >
57
+ >(
58
+ 'SELECT id, rule, active, completed, posts_transaction, tombstone, name from schedules WHERE name NOT NULL AND tombstone = 0',
59
+ );
60
+ }