@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,22 @@
1
+ import { Action } from './action';
2
+ import { Condition } from './condition';
3
+ import { registerHandlebarsHelpers } from './handlebars-helpers';
4
+ import { execActions, Rule } from './rule';
5
+ import { RuleIndexer } from './rule-indexer';
6
+ import {
7
+ iterateIds,
8
+ migrateIds,
9
+ parseDateString,
10
+ rankRules,
11
+ } from './rule-utils';
12
+
13
+ // Ensure helpers are registered
14
+ registerHandlebarsHelpers();
15
+
16
+ // Re-export all the main classes and functions
17
+ export { parseDateString };
18
+ export { Condition };
19
+ export { Action };
20
+ export { Rule, execActions };
21
+ export { RuleIndexer };
22
+ export { rankRules, migrateIds, iterateIds };
@@ -0,0 +1,89 @@
1
+ // @ts-strict-ignore
2
+ import { fastSetMerge } from '../../shared/util';
3
+
4
+ import type { Rule } from './rule';
5
+
6
+ export class RuleIndexer {
7
+ field: string;
8
+ method?: string;
9
+ rules: Map<string, Set<Rule>>;
10
+
11
+ constructor({ field, method }: { field: string; method?: string }) {
12
+ this.field = field;
13
+ this.method = method;
14
+ this.rules = new Map();
15
+ }
16
+
17
+ getIndex(key: string | null): Set<Rule> {
18
+ if (!this.rules.has(key)) {
19
+ this.rules.set(key, new Set());
20
+ }
21
+ return this.rules.get(key);
22
+ }
23
+
24
+ getIndexForValue(value: unknown): Set<Rule> {
25
+ return this.getIndex(this.getKey(value) || '*');
26
+ }
27
+
28
+ getKey(value: unknown): string | null {
29
+ if (typeof value === 'string' && value !== '') {
30
+ if (this.method === 'firstchar') {
31
+ return value[0].toLowerCase();
32
+ }
33
+ return value.toLowerCase();
34
+ }
35
+ return null;
36
+ }
37
+
38
+ getIndexes(rule: Rule): Set<Rule>[] {
39
+ const cond = rule.conditions.find(cond => cond.field === this.field);
40
+ const indexes = [];
41
+
42
+ if (
43
+ cond &&
44
+ (cond.op === 'oneOf' ||
45
+ cond.op === 'is' ||
46
+ cond.op === 'isNot' ||
47
+ cond.op === 'notOneOf')
48
+ ) {
49
+ if (cond.op === 'oneOf' || cond.op === 'notOneOf') {
50
+ cond.value.forEach(val => indexes.push(this.getIndexForValue(val)));
51
+ } else {
52
+ indexes.push(this.getIndexForValue(cond.value));
53
+ }
54
+ } else {
55
+ indexes.push(this.getIndex('*'));
56
+ }
57
+
58
+ return indexes;
59
+ }
60
+
61
+ index(rule: Rule): void {
62
+ const indexes = this.getIndexes(rule);
63
+ indexes.forEach(index => {
64
+ index.add(rule);
65
+ });
66
+ }
67
+
68
+ remove(rule: Rule): void {
69
+ const indexes = this.getIndexes(rule);
70
+ indexes.forEach(index => {
71
+ index.delete(rule);
72
+ });
73
+ }
74
+
75
+ getApplicableRules(object): Set<Rule> {
76
+ let indexedRules;
77
+ if (this.field in object) {
78
+ const key = this.getKey(object[this.field]);
79
+ if (key) {
80
+ indexedRules = this.rules.get(key);
81
+ }
82
+ }
83
+
84
+ return fastSetMerge(
85
+ indexedRules || new Set(),
86
+ this.rules.get('*') || new Set(),
87
+ );
88
+ }
89
+ }
@@ -0,0 +1,274 @@
1
+ // @ts-strict-ignore
2
+ import * as dateFns from 'date-fns';
3
+
4
+ import { logger } from '../../platform/server/log';
5
+ import { recurConfigToRSchedule } from '../../shared/schedules';
6
+ import type { RuleConditionEntity } from '../../types/models';
7
+ import { RuleError } from '../errors';
8
+ import { RSchedule } from '../util/rschedule';
9
+
10
+ import type { Rule } from './rule';
11
+
12
+ export function assert(test: unknown, type: string, msg: string): asserts test {
13
+ if (!test) {
14
+ throw new RuleError(type, msg);
15
+ }
16
+ }
17
+
18
+ const OP_SCORES: Record<RuleConditionEntity['op'], number> = {
19
+ is: 10,
20
+ isNot: 10,
21
+ oneOf: 9,
22
+ notOneOf: 9,
23
+ isapprox: 5,
24
+ isbetween: 5,
25
+ gt: 1,
26
+ gte: 1,
27
+ lt: 1,
28
+ lte: 1,
29
+ contains: 0,
30
+ doesNotContain: 0,
31
+ matches: 0,
32
+ hasTags: 0,
33
+ onBudget: 0,
34
+ offBudget: 0,
35
+ };
36
+
37
+ function computeScore(rule: Rule): number {
38
+ const initialScore = rule.conditions.reduce((score, condition) => {
39
+ if (OP_SCORES[condition.op] == null) {
40
+ logger.log(`Found invalid operation while ranking: ${condition.op}`);
41
+ return 0;
42
+ }
43
+
44
+ return score + OP_SCORES[condition.op];
45
+ }, 0);
46
+
47
+ if (
48
+ rule.conditions.every(
49
+ cond =>
50
+ cond.op === 'is' ||
51
+ cond.op === 'isNot' ||
52
+ cond.op === 'isapprox' ||
53
+ cond.op === 'oneOf' ||
54
+ cond.op === 'notOneOf',
55
+ )
56
+ ) {
57
+ return initialScore * 2;
58
+ }
59
+ return initialScore;
60
+ }
61
+
62
+ function _rankRules(rules: Rule[]): Rule[] {
63
+ const scores = new Map();
64
+ rules.forEach(rule => {
65
+ scores.set(rule, computeScore(rule));
66
+ });
67
+
68
+ // No matter the order of rules, this must always return exactly the same
69
+ // order. That's why rules have ids: if two rules have the same score, it
70
+ // sorts by id
71
+ return [...rules].sort((r1, r2) => {
72
+ const score1 = scores.get(r1);
73
+ const score2 = scores.get(r2);
74
+ if (score1 < score2) {
75
+ return -1;
76
+ } else if (score1 > score2) {
77
+ return 1;
78
+ } else {
79
+ const id1 = r1.getId();
80
+ const id2 = r2.getId();
81
+ return id1 < id2 ? -1 : id1 > id2 ? 1 : 0;
82
+ }
83
+ });
84
+ }
85
+
86
+ export function rankRules(rules: Iterable<Rule>): Rule[] {
87
+ let pre = [];
88
+ let normal = [];
89
+ let post = [];
90
+
91
+ for (const rule of rules) {
92
+ switch (rule.stage) {
93
+ case 'pre':
94
+ pre.push(rule);
95
+ break;
96
+ case 'post':
97
+ post.push(rule);
98
+ break;
99
+ default:
100
+ normal.push(rule);
101
+ }
102
+ }
103
+
104
+ pre = _rankRules(pre);
105
+ normal = _rankRules(normal);
106
+ post = _rankRules(post);
107
+
108
+ return pre.concat(normal).concat(post);
109
+ }
110
+
111
+ export function migrateIds(rule: Rule, mappings: Map<string, string>): void {
112
+ // Go through the in-memory rules and patch up ids that have been
113
+ // "migrated" to other ids. This is a little tricky, but a lot
114
+ // easier than trying to keep an up-to-date mapping in the db. This
115
+ // is necessary because ids can be transparently mapped as items are
116
+ // merged/deleted in the system.
117
+ //
118
+ // It's very important here that we look at `rawValue` specifically,
119
+ // and only apply the patches to the other `value` fields. We always
120
+ // need to keep the original id around because undo can walk
121
+ // backwards, and we need to be able to consistently apply a
122
+ // "projection" of these mapped values. For example: if we have ids
123
+ // [1, 2] and applying mappings transforms it to [2, 2], if `1` gets
124
+ // mapped to something else there's no way to no to map *only* the
125
+ // first id back to make [1, 2]. Keeping the original value around
126
+ // solves this.
127
+ for (let ci = 0; ci < rule.conditions.length; ci++) {
128
+ const cond = rule.conditions[ci];
129
+ if (cond.type === 'id') {
130
+ switch (cond.op) {
131
+ case 'is':
132
+ cond.value = mappings.get(cond.rawValue) || cond.rawValue;
133
+ cond.unparsedValue = cond.value;
134
+ break;
135
+ case 'isNot':
136
+ cond.value = mappings.get(cond.rawValue) || cond.rawValue;
137
+ cond.unparsedValue = cond.value;
138
+ break;
139
+ case 'oneOf':
140
+ cond.value = cond.rawValue.map(v => mappings.get(v) || v);
141
+ cond.unparsedValue = [...cond.value];
142
+ break;
143
+ case 'notOneOf':
144
+ cond.value = cond.rawValue.map(v => mappings.get(v) || v);
145
+ cond.unparsedValue = [...cond.value];
146
+ break;
147
+ default:
148
+ }
149
+ }
150
+ }
151
+
152
+ for (let ai = 0; ai < rule.actions.length; ai++) {
153
+ const action = rule.actions[ai];
154
+ if (action.type === 'id') {
155
+ if (action.op === 'set') {
156
+ action.value = mappings.get(action.rawValue) || action.rawValue;
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ // This finds all the rules that reference the `id`
163
+ export function iterateIds(
164
+ rules: Rule[],
165
+ fieldName: string,
166
+ func: (rule: Rule, id: string) => void | boolean,
167
+ ): void {
168
+ let i;
169
+
170
+ ruleiter: for (i = 0; i < rules.length; i++) {
171
+ const rule = rules[i];
172
+ for (let ci = 0; ci < rule.conditions.length; ci++) {
173
+ const cond = rule.conditions[ci];
174
+ if (cond.type === 'id' && cond.field === fieldName) {
175
+ switch (cond.op) {
176
+ case 'is':
177
+ if (func(rule, cond.value)) {
178
+ continue ruleiter;
179
+ }
180
+ break;
181
+ case 'isNot':
182
+ if (func(rule, cond.value)) {
183
+ continue ruleiter;
184
+ }
185
+ break;
186
+ case 'oneOf':
187
+ for (let vi = 0; vi < cond.value.length; vi++) {
188
+ if (func(rule, cond.value[vi])) {
189
+ continue ruleiter;
190
+ }
191
+ }
192
+ break;
193
+ case 'notOneOf':
194
+ for (let vi = 0; vi < cond.value.length; vi++) {
195
+ if (func(rule, cond.value[vi])) {
196
+ continue ruleiter;
197
+ }
198
+ }
199
+ break;
200
+ default:
201
+ }
202
+ }
203
+ }
204
+
205
+ for (let ai = 0; ai < rule.actions.length; ai++) {
206
+ const action = rule.actions[ai];
207
+ if (action.type === 'id' && action.field === fieldName) {
208
+ // Currently `set` is the only op, but if we add more this
209
+ // will need to be extended
210
+ if (action.op === 'set') {
211
+ if (func(rule, action.value)) {
212
+ break;
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+ }
219
+
220
+ export function parseRecurDate(desc) {
221
+ try {
222
+ const rules = recurConfigToRSchedule(desc);
223
+
224
+ return {
225
+ type: 'recur',
226
+ schedule: new RSchedule({
227
+ rrules: rules,
228
+ data: {
229
+ skipWeekend: desc.skipWeekend,
230
+ weekendSolve: desc.weekendSolveMode,
231
+ },
232
+ }),
233
+ };
234
+ } catch (e) {
235
+ throw new RuleError('parse-recur-date', e.message);
236
+ }
237
+ }
238
+
239
+ export function parseDateString(str) {
240
+ if (typeof str !== 'string') {
241
+ return null;
242
+ } else if (str.length === 10) {
243
+ // YYYY-MM-DD
244
+ if (!dateFns.isValid(dateFns.parseISO(str))) {
245
+ return null;
246
+ }
247
+
248
+ return { type: 'date', date: str };
249
+ } else if (str.length === 7) {
250
+ // YYYY-MM
251
+ if (!dateFns.isValid(dateFns.parseISO(str + '-01'))) {
252
+ return null;
253
+ }
254
+
255
+ return { type: 'month', date: str };
256
+ } else if (str.length === 4) {
257
+ // YYYY
258
+ if (!dateFns.isValid(dateFns.parseISO(str + '-01-01'))) {
259
+ return null;
260
+ }
261
+
262
+ return { type: 'year', date: str };
263
+ }
264
+
265
+ return null;
266
+ }
267
+
268
+ export function parseBetweenAmount(between) {
269
+ const { num1, num2 } = between;
270
+ if (typeof num1 !== 'number' || typeof num2 !== 'number') {
271
+ return null;
272
+ }
273
+ return { type: 'between', num1, num2 };
274
+ }
@@ -0,0 +1,193 @@
1
+ // @ts-strict-ignore
2
+ import {
3
+ addSplitTransaction,
4
+ groupTransaction,
5
+ recalculateSplit,
6
+ splitTransaction,
7
+ ungroupTransaction,
8
+ } from '../../shared/transactions';
9
+ import type { RuleEntity } from '../../types/models';
10
+
11
+ import { Action } from './action';
12
+ import { Condition } from './condition';
13
+
14
+ function execNonSplitActions(actions: Action[], transaction) {
15
+ const update = transaction;
16
+ actions.forEach(action => action.exec(update));
17
+ return update;
18
+ }
19
+
20
+ function getSplitRemainder(transactions) {
21
+ const { error } = recalculateSplit(groupTransaction(transactions));
22
+ return error ? error.difference : 0;
23
+ }
24
+
25
+ function execSplitActions(actions: Action[], transaction) {
26
+ const splitAmountActions = actions.filter(
27
+ action => action.op === 'set-split-amount',
28
+ );
29
+
30
+ // Convert the transaction to a split transaction.
31
+ const { data } = splitTransaction(
32
+ ungroupTransaction(transaction),
33
+ transaction.id,
34
+ );
35
+ let newTransactions = data;
36
+
37
+ // Add empty splits, and apply non-set-amount actions.
38
+ // This also populates any fixed-amount splits.
39
+ actions.forEach(action => {
40
+ const splitTransactionIndex = (action.options?.splitIndex ?? 0) + 1;
41
+ if (splitTransactionIndex >= newTransactions.length) {
42
+ const { data } = addSplitTransaction(newTransactions, transaction.id);
43
+ newTransactions = data;
44
+ }
45
+ action.exec(newTransactions[splitTransactionIndex]);
46
+ });
47
+
48
+ // Distribute to fixed-percent splits.
49
+ const remainingAfterFixedAmounts = getSplitRemainder(newTransactions);
50
+ splitAmountActions
51
+ .filter(action => action.options.method === 'fixed-percent')
52
+ .forEach(action => {
53
+ const splitTransactionIndex = (action.options?.splitIndex ?? 0) + 1;
54
+ const percent = action.value / 100;
55
+ const amount = Math.round(remainingAfterFixedAmounts * percent);
56
+ newTransactions[splitTransactionIndex].amount = amount;
57
+ });
58
+
59
+ // Distribute to remainder splits.
60
+ const remainderActions = splitAmountActions.filter(
61
+ action => action.options.method === 'remainder',
62
+ );
63
+ const remainingAfterFixedPercents = getSplitRemainder(newTransactions);
64
+ if (remainderActions.length !== 0) {
65
+ const amountPerRemainderSplit = Math.round(
66
+ remainingAfterFixedPercents / remainderActions.length,
67
+ );
68
+ let lastNonFixedTransactionIndex = -1;
69
+ remainderActions.forEach(action => {
70
+ const splitTransactionIndex = (action.options?.splitIndex ?? 0) + 1;
71
+ newTransactions[splitTransactionIndex].amount = amountPerRemainderSplit;
72
+ lastNonFixedTransactionIndex = Math.max(
73
+ lastNonFixedTransactionIndex,
74
+ splitTransactionIndex,
75
+ );
76
+ });
77
+
78
+ // The last remainder split will be adjusted for any leftovers from rounding.
79
+ newTransactions[lastNonFixedTransactionIndex].amount +=
80
+ getSplitRemainder(newTransactions);
81
+ }
82
+
83
+ // The split index 0 (transaction index 1) is reserved for "Apply to all" actions.
84
+ // Remove that entry from the transaction list.
85
+ newTransactions.splice(1, 1);
86
+ return recalculateSplit(groupTransaction(newTransactions));
87
+ }
88
+
89
+ export function execActions(actions: Action[], transaction) {
90
+ const parentActions = actions.filter(action => !action.options?.splitIndex);
91
+ const childActions = actions.filter(action => action.options?.splitIndex);
92
+ const totalSplitCount =
93
+ actions.reduce(
94
+ (prev, cur) => Math.max(prev, cur.options?.splitIndex ?? 0),
95
+ 0,
96
+ ) + 1;
97
+
98
+ const nonSplitResult = execNonSplitActions(parentActions, transaction);
99
+ if (totalSplitCount === 1) {
100
+ // No splits, no need to do anything else.
101
+ return nonSplitResult;
102
+ }
103
+
104
+ if (nonSplitResult.is_child) {
105
+ // Rules with splits can't be applied to child transactions.
106
+ return nonSplitResult;
107
+ }
108
+
109
+ return execSplitActions(childActions, nonSplitResult);
110
+ }
111
+
112
+ export class Rule {
113
+ actions: Action[];
114
+ conditions: Condition[];
115
+ conditionsOp;
116
+ id?: string;
117
+ stage: 'pre' | null | 'post';
118
+
119
+ constructor({
120
+ id,
121
+ stage,
122
+ conditionsOp,
123
+ conditions,
124
+ actions,
125
+ }: {
126
+ id?: string;
127
+ stage?: 'pre' | null | 'post';
128
+ conditionsOp;
129
+ conditions;
130
+ actions;
131
+ }) {
132
+ this.id = id;
133
+ this.stage = stage ?? null;
134
+ this.conditionsOp = conditionsOp;
135
+ this.conditions = conditions.map(
136
+ c => new Condition(c.op, c.field, c.value, c.options),
137
+ );
138
+ this.actions = actions.map(
139
+ a => new Action(a.op, a.field, a.value, a.options),
140
+ );
141
+ }
142
+
143
+ evalConditions(object): boolean {
144
+ if (this.conditions.length === 0) {
145
+ return false;
146
+ }
147
+
148
+ const method = this.conditionsOp === 'or' ? 'some' : 'every';
149
+ return this.conditions[method](condition => {
150
+ return condition.eval(object);
151
+ });
152
+ }
153
+
154
+ execActions<T>(object: T): Partial<T> {
155
+ const result = execActions(this.actions, {
156
+ ...object,
157
+ });
158
+ const changes = Object.keys(result).reduce((prev, cur) => {
159
+ if (result[cur] !== object[cur]) {
160
+ prev[cur] = result[cur];
161
+ }
162
+ return prev;
163
+ }, {} as T);
164
+ return changes;
165
+ }
166
+
167
+ exec(object) {
168
+ if (this.evalConditions(object)) {
169
+ return this.execActions(object);
170
+ }
171
+ return null;
172
+ }
173
+
174
+ // Apply is similar to exec but applies the changes for you
175
+ apply(object) {
176
+ const changes = this.exec(object);
177
+ return Object.assign({}, object, changes);
178
+ }
179
+
180
+ getId(): string | undefined {
181
+ return this.id;
182
+ }
183
+
184
+ serialize(): RuleEntity {
185
+ return {
186
+ id: this.id,
187
+ stage: this.stage,
188
+ conditionsOp: this.conditionsOp,
189
+ conditions: this.conditions.map(c => c.serialize()),
190
+ actions: this.actions.map(a => a.serialize()),
191
+ };
192
+ }
193
+ }