@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,644 @@
1
+ // @ts-strict-ignore
2
+ import * as d from 'date-fns';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ import { captureBreadcrumb } from '../../platform/exceptions';
6
+ import * as connection from '../../platform/server/connection';
7
+ import { logger } from '../../platform/server/log';
8
+ import { currentDay, dayFromDate, parseDate } from '../../shared/months';
9
+ import { q } from '../../shared/query';
10
+ import {
11
+ extractScheduleConds,
12
+ getDateWithSkippedWeekend,
13
+ getHasTransactionsQuery,
14
+ getNextDate,
15
+ getScheduledAmount,
16
+ getStatus,
17
+ recurConfigToRSchedule,
18
+ } from '../../shared/schedules';
19
+ import type { RuleConditionEntity, ScheduleEntity } from '../../types/models';
20
+ import { addTransactions } from '../accounts/sync';
21
+ import { createApp } from '../app';
22
+ import { aqlQuery } from '../aql';
23
+ import * as db from '../db';
24
+ import { toDateRepr } from '../models';
25
+ import { mutator, runMutator } from '../mutators';
26
+ import * as prefs from '../prefs';
27
+ import { Rule } from '../rules';
28
+ import { addSyncListener, batchMessages } from '../sync';
29
+ import {
30
+ getRules,
31
+ insertRule,
32
+ ruleModel,
33
+ updateRule,
34
+ } from '../transactions/transaction-rules';
35
+ import { undoable } from '../undo';
36
+ import { RSchedule } from '../util/rschedule';
37
+
38
+ import { findSchedules } from './find-schedules';
39
+
40
+ // Utilities
41
+
42
+ function zip(arr1, arr2) {
43
+ const result = [];
44
+ for (let i = 0; i < arr1.length; i++) {
45
+ result.push([arr1[i], arr2[i]]);
46
+ }
47
+ return result;
48
+ }
49
+
50
+ export function areConditionValuesEqual(left, right) {
51
+ if (left === right) {
52
+ return true;
53
+ }
54
+
55
+ if (left == null || right == null) {
56
+ return left === right;
57
+ }
58
+
59
+ if (Array.isArray(left) || Array.isArray(right)) {
60
+ return (
61
+ Array.isArray(left) &&
62
+ Array.isArray(right) &&
63
+ left.length === right.length &&
64
+ left.every((value, index) => areConditionValuesEqual(value, right[index]))
65
+ );
66
+ }
67
+
68
+ if (typeof left === 'object' && typeof right === 'object') {
69
+ const leftKeys = Object.keys(left).sort();
70
+ const rightKeys = Object.keys(right).sort();
71
+
72
+ return (
73
+ leftKeys.length === rightKeys.length &&
74
+ leftKeys.every((key, index) => {
75
+ const rightKey = rightKeys[index];
76
+ return (
77
+ key === rightKey &&
78
+ areConditionValuesEqual(left[key], right[rightKey])
79
+ );
80
+ })
81
+ );
82
+ }
83
+
84
+ return false;
85
+ }
86
+
87
+ function areScheduleConditionsEqual(
88
+ left?: RuleConditionEntity,
89
+ right?: RuleConditionEntity,
90
+ ) {
91
+ if (left == null || right == null) {
92
+ return left === right;
93
+ }
94
+
95
+ const { type: _leftType, ...leftCondition } = left;
96
+ const { type: _rightType, ...rightCondition } = right;
97
+
98
+ return areConditionValuesEqual(leftCondition, rightCondition);
99
+ }
100
+
101
+ export function updateConditions(conditions, newConditions) {
102
+ const scheduleConds = extractScheduleConds(conditions);
103
+ const newScheduleConds = extractScheduleConds(newConditions);
104
+
105
+ const replacements = zip(
106
+ Object.values(scheduleConds),
107
+ Object.values(newScheduleConds),
108
+ );
109
+
110
+ const updated = conditions.map(cond => {
111
+ const r = replacements.find(r => cond === r[0]);
112
+ return r && r[1] ? r[1] : cond;
113
+ });
114
+
115
+ const added = replacements
116
+ .filter(x => x[0] == null && x[1] != null)
117
+ .map(x => x[1]);
118
+
119
+ return updated.concat(added);
120
+ }
121
+
122
+ export async function getRuleForSchedule(id: string | null): Promise<Rule> {
123
+ if (id == null) {
124
+ throw new Error('Schedule not attached to a rule');
125
+ }
126
+
127
+ const { data: ruleId } = await aqlQuery(
128
+ q('schedules').filter({ id }).calculate('rule'),
129
+ );
130
+ return getRules().find(rule => rule.id === ruleId);
131
+ }
132
+
133
+ async function fixRuleForSchedule(id) {
134
+ const { data: ruleId } = await aqlQuery(
135
+ q('schedules').filter({ id }).calculate('rule'),
136
+ );
137
+
138
+ if (ruleId) {
139
+ // Take the bad rule out of the system so it never causes problems
140
+ // in the future
141
+ await db.delete_('rules', ruleId);
142
+ }
143
+
144
+ const newId = await insertRule({
145
+ stage: null,
146
+ conditionsOp: 'and',
147
+ conditions: [
148
+ { op: 'isapprox', field: 'date', value: currentDay() },
149
+ { op: 'isapprox', field: 'amount', value: 0 },
150
+ ],
151
+ actions: [{ op: 'link-schedule', value: id }],
152
+ });
153
+
154
+ await db.updateWithSchema('schedules', { id, rule: newId });
155
+
156
+ return getRules().find(rule => rule.id === newId);
157
+ }
158
+
159
+ export async function setNextDate({
160
+ id,
161
+ start,
162
+ conditions,
163
+ reset,
164
+ skipRequested,
165
+ }: {
166
+ id: string;
167
+ start?;
168
+ conditions?;
169
+ reset?: boolean;
170
+ skipRequested?: boolean;
171
+ }) {
172
+ if (conditions == null) {
173
+ const rule = await getRuleForSchedule(id);
174
+ if (rule == null) {
175
+ throw new Error('No rule found for schedule');
176
+ }
177
+ conditions = rule.serialize().conditions;
178
+ }
179
+
180
+ const { date: dateCond } = extractScheduleConds(conditions);
181
+
182
+ let { data: nextDate } = await aqlQuery(
183
+ q('schedules').filter({ id }).calculate('next_date'),
184
+ );
185
+
186
+ if (skipRequested === true) {
187
+ const skipWeekend: boolean = dateCond.value?.skipWeekend;
188
+ const weekendSolveMode: string = dateCond.value?.weekendSolveMode;
189
+
190
+ if (weekendSolveMode === 'before' && skipWeekend === true) {
191
+ const parsedNextDate = parseDate(nextDate);
192
+ if (d.isFriday(parsedNextDate) || d.isWeekend(parsedNextDate)) {
193
+ // nextDate is on weekend or friday, moving to monday
194
+ // so getNextDate and getDateWithSkippedWeekend
195
+ // don't push the date back to Friday, thus causing
196
+ // `(newNextDate !== nextDate) ` to be false and not updating the next date
197
+ nextDate = dayFromDate(d.nextMonday(parsedNextDate));
198
+ }
199
+ }
200
+ }
201
+
202
+ // Only do this if a date condition exists
203
+ if (dateCond) {
204
+ const newNextDate = getNextDate(
205
+ dateCond,
206
+ start ? start(nextDate) : new Date(),
207
+ );
208
+
209
+ if (newNextDate !== nextDate) {
210
+ // Our `update` functon requires the id of the item and we don't
211
+ // have it, so we need to query it
212
+ const nd = await db.first<
213
+ Pick<db.DbScheduleNextDate, 'id' | 'base_next_date_ts'>
214
+ >(
215
+ 'SELECT id, base_next_date_ts FROM schedules_next_date WHERE schedule_id = ?',
216
+ [id],
217
+ );
218
+
219
+ await db.update(
220
+ 'schedules_next_date',
221
+ reset
222
+ ? {
223
+ id: nd.id,
224
+ base_next_date: toDateRepr(newNextDate),
225
+ base_next_date_ts: Date.now(),
226
+ }
227
+ : {
228
+ id: nd.id,
229
+ local_next_date: toDateRepr(newNextDate),
230
+ local_next_date_ts: nd.base_next_date_ts,
231
+ },
232
+ );
233
+ }
234
+ }
235
+ }
236
+
237
+ // Methods
238
+
239
+ async function checkIfScheduleExists(name, scheduleId) {
240
+ const idForName = await db.first<Pick<db.DbSchedule, 'id'>>(
241
+ 'SELECT id from schedules WHERE tombstone = 0 AND name = ?',
242
+ [name],
243
+ );
244
+
245
+ if (idForName == null) {
246
+ return false;
247
+ }
248
+ if (scheduleId) {
249
+ return idForName['id'] !== scheduleId;
250
+ }
251
+ return true;
252
+ }
253
+
254
+ export async function createSchedule({
255
+ schedule = null,
256
+ conditions = [],
257
+ } = {}): Promise<ScheduleEntity['id']> {
258
+ const scheduleId = schedule?.id || uuidv4();
259
+
260
+ const { date: dateCond } = extractScheduleConds(conditions);
261
+ if (dateCond == null) {
262
+ throw new Error('A date condition is required to create a schedule');
263
+ }
264
+ if (dateCond.value == null) {
265
+ throw new Error('Date is required');
266
+ }
267
+
268
+ const nextDate = getNextDate(dateCond);
269
+ const nextDateRepr = nextDate ? toDateRepr(nextDate) : null;
270
+ if (schedule) {
271
+ if (schedule.name) {
272
+ if (await checkIfScheduleExists(schedule.name, scheduleId)) {
273
+ throw new Error('Cannot create schedules with the same name');
274
+ }
275
+ } else {
276
+ schedule.name = null;
277
+ }
278
+ }
279
+
280
+ // Create the rule here based on the info
281
+ const ruleId = await insertRule({
282
+ stage: null,
283
+ conditionsOp: 'and',
284
+ conditions,
285
+ actions: [{ op: 'link-schedule', value: scheduleId }],
286
+ });
287
+
288
+ const now = Date.now();
289
+ await db.insertWithUUID('schedules_next_date', {
290
+ schedule_id: scheduleId,
291
+ local_next_date: nextDateRepr,
292
+ local_next_date_ts: now,
293
+ base_next_date: nextDateRepr,
294
+ base_next_date_ts: now,
295
+ });
296
+
297
+ await db.insertWithSchema('schedules', {
298
+ ...schedule,
299
+ id: scheduleId,
300
+ rule: ruleId,
301
+ });
302
+
303
+ return scheduleId;
304
+ }
305
+
306
+ // TODO: don't allow deleting rules that link schedules
307
+
308
+ export async function updateSchedule({
309
+ schedule,
310
+ conditions,
311
+ resetNextDate,
312
+ }: {
313
+ schedule: Partial<ScheduleEntity> & Pick<ScheduleEntity, 'id'>;
314
+ conditions?: RuleConditionEntity[];
315
+ resetNextDate?: boolean;
316
+ }) {
317
+ if (schedule.rule) {
318
+ throw new Error('You cannot change the rule of a schedule');
319
+ }
320
+ let rule;
321
+
322
+ // This must be outside the `batchMessages` call because we change
323
+ // and then read data
324
+ if (conditions) {
325
+ const { date: dateCond } = extractScheduleConds(conditions);
326
+ if (dateCond && dateCond.value == null) {
327
+ throw new Error('Date is required');
328
+ }
329
+
330
+ // We need to get the full rule to merge in the updated
331
+ // conditions
332
+ rule = await getRuleForSchedule(schedule.id);
333
+
334
+ if (rule == null) {
335
+ // In the edge case that a rule gets corrupted (either by a bug in
336
+ // the system or user messing with their data), don't crash. We
337
+ // generate a new rule because schedules have to have a rule
338
+ // attached to them.
339
+ rule = await fixRuleForSchedule(schedule.id);
340
+ }
341
+ }
342
+
343
+ await batchMessages(async () => {
344
+ if (conditions) {
345
+ const oldConditions = rule.serialize().conditions;
346
+ const newConditions = updateConditions(oldConditions, conditions);
347
+
348
+ await updateRule({ id: rule.id, conditions: newConditions });
349
+
350
+ // Annoyingly, sometimes it has `type` and sometimes it doesn't
351
+ const stripType = ({ type: _type, ...fields }) => fields;
352
+
353
+ // Update `next_date` if the user forced it, or if the account
354
+ // or date changed. We check account because we don't update
355
+ // schedules automatically for closed account, and the user
356
+ // might switch accounts from a closed one
357
+ if (
358
+ resetNextDate ||
359
+ !areScheduleConditionsEqual(
360
+ oldConditions.find(c => c.field === 'account'),
361
+ newConditions.find(c => c.field === 'account'),
362
+ ) ||
363
+ !areConditionValuesEqual(
364
+ stripType(oldConditions.find(c => c.field === 'date') || {}),
365
+ stripType(newConditions.find(c => c.field === 'date') || {}),
366
+ )
367
+ ) {
368
+ await setNextDate({
369
+ id: schedule.id,
370
+ conditions: newConditions,
371
+ reset: true,
372
+ });
373
+ }
374
+ } else if (resetNextDate) {
375
+ await setNextDate({ id: schedule.id, reset: true });
376
+ }
377
+
378
+ await db.updateWithSchema('schedules', schedule);
379
+ });
380
+
381
+ return schedule.id;
382
+ }
383
+
384
+ export async function deleteSchedule({ id }) {
385
+ const { data: ruleId } = await aqlQuery(
386
+ q('schedules').filter({ id }).calculate('rule'),
387
+ );
388
+
389
+ await batchMessages(async () => {
390
+ await db.delete_('rules', ruleId);
391
+ await db.delete_('schedules', id);
392
+ });
393
+ }
394
+
395
+ export async function skipNextDate({ id }) {
396
+ return setNextDate({
397
+ id,
398
+ start: nextDate => {
399
+ return d.addDays(parseDate(nextDate), 1);
400
+ },
401
+ skipRequested: true,
402
+ });
403
+ }
404
+
405
+ function discoverSchedules() {
406
+ return findSchedules();
407
+ }
408
+
409
+ async function getUpcomingDates({ config, count }) {
410
+ const rules = recurConfigToRSchedule(config);
411
+
412
+ try {
413
+ const schedule = new RSchedule({ rrules: rules });
414
+
415
+ return schedule
416
+ .occurrences({ start: d.startOfDay(new Date()), take: count })
417
+ .toArray()
418
+ .map(date =>
419
+ config.skipWeekend
420
+ ? getDateWithSkippedWeekend(date.date, config.weekendSolveMode)
421
+ : date.date,
422
+ )
423
+ .map(date => dayFromDate(date));
424
+ } catch (err) {
425
+ captureBreadcrumb(config);
426
+ throw err;
427
+ }
428
+ }
429
+
430
+ // Services
431
+
432
+ function onRuleUpdate(rule) {
433
+ const { actions, conditions } =
434
+ rule instanceof Rule ? rule.serialize() : ruleModel.toJS(rule);
435
+
436
+ if (actions && actions.find(a => a.op === 'link-schedule')) {
437
+ const scheduleId = actions.find(a => a.op === 'link-schedule').value;
438
+
439
+ if (scheduleId) {
440
+ const conds = extractScheduleConds(conditions);
441
+
442
+ const payeeIdx = conditions.findIndex(c => c === conds.payee);
443
+ const accountIdx = conditions.findIndex(c => c === conds.account);
444
+ const amountIdx = conditions.findIndex(c => c === conds.amount);
445
+ const dateIdx = conditions.findIndex(c => c === conds.date);
446
+
447
+ db.runQuery(
448
+ 'INSERT OR REPLACE INTO schedules_json_paths (schedule_id, payee, account, amount, date) VALUES (?, ?, ?, ?, ?)',
449
+ [
450
+ scheduleId,
451
+ payeeIdx === -1 ? null : `$[${payeeIdx}]`,
452
+ accountIdx === -1 ? null : `$[${accountIdx}]`,
453
+ amountIdx === -1 ? null : `$[${amountIdx}]`,
454
+ dateIdx === -1 ? null : `$[${dateIdx}]`,
455
+ ],
456
+ );
457
+ }
458
+ }
459
+ }
460
+
461
+ function trackJSONPaths() {
462
+ // Populate the table
463
+ db.transaction(() => {
464
+ getRules().forEach(rule => {
465
+ onRuleUpdate(rule);
466
+ });
467
+ });
468
+
469
+ return addSyncListener(onApplySync);
470
+ }
471
+
472
+ function onApplySync(oldValues, newValues) {
473
+ newValues.forEach((items, table) => {
474
+ if (table === 'rules') {
475
+ items.forEach(newValue => {
476
+ onRuleUpdate(newValue);
477
+ });
478
+ }
479
+ });
480
+ }
481
+
482
+ // This is the service that move schedules forward automatically and
483
+ // posts transactions
484
+
485
+ async function postTransactionForSchedule({
486
+ id,
487
+ today,
488
+ }: {
489
+ id: string;
490
+ today?: boolean;
491
+ }) {
492
+ const { data } = await aqlQuery(q('schedules').filter({ id }).select('*'));
493
+ const schedule = data[0];
494
+ if (schedule == null || schedule._account == null) {
495
+ return;
496
+ }
497
+
498
+ const transaction = {
499
+ payee: schedule._payee,
500
+ account: schedule._account,
501
+ amount: getScheduledAmount(schedule._amount),
502
+ date: today ? currentDay() : schedule.next_date,
503
+ schedule: schedule.id,
504
+ cleared: false,
505
+ };
506
+
507
+ if (transaction.account) {
508
+ await addTransactions(transaction.account, [transaction]);
509
+ }
510
+ }
511
+
512
+ // TODO: make this sequential
513
+
514
+ async function advanceSchedulesService(syncSuccess) {
515
+ // Move all paid schedules
516
+ const { data: schedules } = await aqlQuery(
517
+ q('schedules')
518
+ .filter({ completed: false, '_account.closed': false })
519
+ .select('*'),
520
+ );
521
+
522
+ const { data: hasTransData } = await aqlQuery(
523
+ getHasTransactionsQuery(schedules),
524
+ );
525
+ const hasTrans = new Set(
526
+ hasTransData.filter(Boolean).map(row => row.schedule),
527
+ );
528
+
529
+ const failedToPost = [];
530
+ let didPost = false;
531
+
532
+ const { data: upcomingLength } = await aqlQuery(
533
+ q('preferences')
534
+ .filter({ id: 'upcomingScheduledTransactionLength' })
535
+ .select('value'),
536
+ );
537
+
538
+ for (const schedule of schedules) {
539
+ const status = getStatus(
540
+ schedule.next_date,
541
+ schedule.completed,
542
+ hasTrans.has(schedule.id),
543
+ upcomingLength[0]?.value ?? '7',
544
+ );
545
+
546
+ if (status === 'paid') {
547
+ if (schedule._date) {
548
+ // Move forward recurring schedules
549
+ if (schedule._date.frequency) {
550
+ try {
551
+ await setNextDate({ id: schedule.id });
552
+ } catch {
553
+ // This might error if the rule is corrupted and it can't
554
+ // find the rule
555
+ }
556
+ } else {
557
+ if (schedule._date < currentDay()) {
558
+ // Complete any single schedules
559
+ await updateSchedule({
560
+ schedule: { id: schedule.id, completed: true },
561
+ });
562
+ }
563
+ }
564
+ }
565
+ } else if (
566
+ (status === 'due' || status === 'missed') &&
567
+ schedule.posts_transaction &&
568
+ schedule._account
569
+ ) {
570
+ // Automatically create a transaction for due schedules
571
+ if (syncSuccess) {
572
+ await postTransactionForSchedule({ id: schedule.id });
573
+
574
+ didPost = true;
575
+ } else {
576
+ failedToPost.push(schedule._payee);
577
+ }
578
+ }
579
+ }
580
+
581
+ if (failedToPost.length > 0) {
582
+ connection.send('schedules-offline');
583
+ } else if (didPost) {
584
+ // This forces a full refresh of transactions because it
585
+ // simulates them coming in from a full sync. This not a
586
+ // great API right now, but I think generally the approach
587
+ // is sane to treat them as external sync events.
588
+ connection.send('sync-event', {
589
+ type: 'success',
590
+ tables: ['transactions'],
591
+ syncDisabled: false,
592
+ });
593
+ }
594
+ }
595
+
596
+ export type SchedulesHandlers = {
597
+ 'schedule/create': typeof createSchedule;
598
+ 'schedule/update': typeof updateSchedule;
599
+ 'schedule/delete': typeof deleteSchedule;
600
+ 'schedule/skip-next-date': typeof skipNextDate;
601
+ 'schedule/post-transaction': typeof postTransactionForSchedule;
602
+ 'schedule/force-run-service': typeof advanceSchedulesService;
603
+ 'schedule/discover': typeof discoverSchedules;
604
+ 'schedule/get-upcoming-dates': typeof getUpcomingDates;
605
+ };
606
+
607
+ // Expose functions to the client
608
+ export const app = createApp<SchedulesHandlers>();
609
+
610
+ app.method('schedule/create', mutator(undoable(createSchedule)));
611
+ app.method('schedule/update', mutator(undoable(updateSchedule)));
612
+ app.method('schedule/delete', mutator(undoable(deleteSchedule)));
613
+ app.method('schedule/skip-next-date', mutator(undoable(skipNextDate)));
614
+ app.method(
615
+ 'schedule/post-transaction',
616
+ mutator(undoable(postTransactionForSchedule)),
617
+ );
618
+ app.method(
619
+ 'schedule/force-run-service',
620
+ mutator(() => advanceSchedulesService(true)),
621
+ );
622
+ app.method('schedule/discover', discoverSchedules);
623
+ app.method('schedule/get-upcoming-dates', getUpcomingDates);
624
+
625
+ app.service(trackJSONPaths);
626
+
627
+ app.events.on('sync', ({ type }) => {
628
+ const completeEvent =
629
+ type === 'success' || type === 'error' || type === 'unauthorized';
630
+
631
+ if (completeEvent && prefs.getPrefs()) {
632
+ if (!db.getDatabase()) {
633
+ logger.info('database is not available, skipping schedule service');
634
+ return;
635
+ }
636
+
637
+ const { lastScheduleRun } = prefs.getPrefs();
638
+ if (lastScheduleRun !== currentDay()) {
639
+ void runMutator(() => advanceSchedulesService(type === 'success'));
640
+
641
+ void prefs.savePrefs({ lastScheduleRun: currentDay() });
642
+ }
643
+ }
644
+ });