@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,391 @@
1
+ // @ts-strict-ignore
2
+ import * as d from 'date-fns';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+
5
+ import { dayFromDate, parseDate } from '../../shared/months';
6
+ import { q } from '../../shared/query';
7
+ import { getApproxNumberThreshold } from '../../shared/rules';
8
+ import { recurConfigToRSchedule } from '../../shared/schedules';
9
+ import { groupBy } from '../../shared/util';
10
+ import { aqlQuery } from '../aql';
11
+ import * as db from '../db';
12
+ import { fromDateRepr } from '../models';
13
+ import { conditionsToAQL } from '../transactions/transaction-rules';
14
+ import { RSchedule } from '../util/rschedule';
15
+
16
+ function takeDates(config) {
17
+ const schedule = new RSchedule({ rrules: recurConfigToRSchedule(config) });
18
+ return schedule
19
+ .occurrences({ take: 3 })
20
+ .toArray()
21
+ .map(d => d.date);
22
+ }
23
+
24
+ async function getTransactions(date, account) {
25
+ const { data } = await aqlQuery(
26
+ q('transactions')
27
+ .filter({
28
+ account,
29
+ schedule: null,
30
+ // Don't match transfers
31
+ 'payee.transfer_acct': null,
32
+ $and: [
33
+ { date: { $gte: d.subDays(date, 2) } },
34
+ { date: { $lte: d.addDays(date, 2) } },
35
+ ],
36
+ })
37
+ .select('*')
38
+ .options({ splits: 'none' }),
39
+ );
40
+ return data;
41
+ }
42
+
43
+ function getRank(day1, day2) {
44
+ const dayDiff = Math.abs(
45
+ d.differenceInDays(parseDate(day1), parseDate(day2)),
46
+ );
47
+
48
+ // The amount of days off determines the rank: exact same day
49
+ // is highest rank 1, 1 day off is .5, etc. This will find the
50
+ // best start date that matches all the dates the closest
51
+ return 1 / (dayDiff + 1);
52
+ }
53
+
54
+ function matchSchedules(allOccurs, config) {
55
+ allOccurs = [...allOccurs].reverse();
56
+ const baseOccur = allOccurs[0];
57
+ const occurs = allOccurs.slice(1);
58
+ const schedules = [];
59
+
60
+ for (const trans of baseOccur.transactions) {
61
+ const threshold = getApproxNumberThreshold(trans.amount);
62
+ const payee = trans.payee;
63
+
64
+ const found = occurs.map(occur => {
65
+ let matched = occur.transactions.find(
66
+ t =>
67
+ t.amount >= trans.amount - threshold &&
68
+ t.amount <= trans.amount + threshold,
69
+ );
70
+ matched = matched && matched.payee === payee ? matched : null;
71
+
72
+ if (matched) {
73
+ return { trans: matched, rank: getRank(occur.date, matched.date) };
74
+ }
75
+ return null;
76
+ });
77
+
78
+ if (found.indexOf(null) !== -1) {
79
+ continue;
80
+ }
81
+
82
+ const rank = found.reduce(
83
+ (total, match) => total + match.rank,
84
+ getRank(baseOccur.date, trans.date),
85
+ );
86
+
87
+ const exactAmount = found.reduce(
88
+ (exact, match) => exact && match.trans.amount === trans.amount,
89
+ true,
90
+ );
91
+
92
+ schedules.push({
93
+ rank,
94
+ amount: trans.amount,
95
+ account: trans.account,
96
+ payee: trans.payee,
97
+ date: config,
98
+ // Exact dates rank as 1, so all of them matches exactly it
99
+ // would equal the number of `allOccurs`
100
+ exactDate: rank === allOccurs.length,
101
+ exactAmount,
102
+ });
103
+ }
104
+
105
+ return schedules;
106
+ }
107
+
108
+ async function schedulesForPattern(baseStart, numDays, baseConfig, accountId) {
109
+ let schedules = [];
110
+
111
+ for (let i = 0; i < numDays; i++) {
112
+ const start = d.addDays(baseStart, i);
113
+ let config;
114
+ if (typeof baseConfig === 'function') {
115
+ config = baseConfig(start);
116
+
117
+ if (config === false) {
118
+ // Skip this one
119
+ continue;
120
+ }
121
+ } else {
122
+ config = { ...baseConfig, start };
123
+ }
124
+
125
+ // Our recur config expects a day string, not a native date format
126
+ config.start = dayFromDate(config.start);
127
+
128
+ const data = [];
129
+ const dates = takeDates(config);
130
+ for (const date of dates) {
131
+ data.push({
132
+ date: dayFromDate(date),
133
+ transactions: await getTransactions(date, accountId),
134
+ });
135
+ }
136
+
137
+ schedules = schedules.concat(matchSchedules(data, config));
138
+ }
139
+ return schedules;
140
+ }
141
+
142
+ async function weekly(startDate, accountId) {
143
+ return schedulesForPattern(
144
+ d.subWeeks(parseDate(startDate), 4),
145
+ 7 * 2,
146
+ { frequency: 'weekly' },
147
+ accountId,
148
+ );
149
+ }
150
+
151
+ async function every2weeks(startDate, accountId) {
152
+ return schedulesForPattern(
153
+ // 6 weeks would cover 3 instances, but we also scan an addition
154
+ // week back
155
+ d.subWeeks(parseDate(startDate), 7),
156
+ 7 * 2,
157
+ { frequency: 'weekly', interval: 2 },
158
+ accountId,
159
+ );
160
+ }
161
+
162
+ async function monthly(startDate, accountId) {
163
+ return schedulesForPattern(
164
+ d.subMonths(parseDate(startDate), 4),
165
+ 31 * 2,
166
+ start => {
167
+ // 28 is the max number of days that all months are guaranteed
168
+ // to have. We don't want to go any higher than that because
169
+ // we'll end up skipping months that don't have that day.
170
+ // The use cases of end of month days will be covered with the
171
+ // `monthlyLastDay` pattern;
172
+ if (d.getDate(start) > 28) {
173
+ return false;
174
+ }
175
+ return { start, frequency: 'monthly' };
176
+ },
177
+ accountId,
178
+ );
179
+ }
180
+
181
+ async function monthlyLastDay(startDate, accountId) {
182
+ // We do two separate calls because this pattern doesn't fit into
183
+ // how `schedulesForPattern` works
184
+ const s1 = await schedulesForPattern(
185
+ d.subMonths(parseDate(startDate), 3),
186
+ 1,
187
+ { frequency: 'monthly', patterns: [{ type: 'day', value: -1 }] },
188
+ accountId,
189
+ );
190
+
191
+ const s2 = await schedulesForPattern(
192
+ d.subMonths(parseDate(startDate), 4),
193
+ 1,
194
+ { frequency: 'monthly', patterns: [{ type: 'day', value: -1 }] },
195
+ accountId,
196
+ );
197
+
198
+ return s1.concat(s2);
199
+ }
200
+
201
+ async function monthly1stor3rd(startDate, accountId) {
202
+ return schedulesForPattern(
203
+ d.subWeeks(parseDate(startDate), 8),
204
+ 14,
205
+ start => {
206
+ const day = d.format(new Date(), 'iiii');
207
+ const dayValue = day.slice(0, 2).toUpperCase();
208
+
209
+ return {
210
+ start,
211
+ frequency: 'monthly',
212
+ patterns: [
213
+ { type: dayValue, value: 1 },
214
+ { type: dayValue, value: 3 },
215
+ ],
216
+ };
217
+ },
218
+ accountId,
219
+ );
220
+ }
221
+
222
+ async function monthly2ndor4th(startDate, accountId) {
223
+ return schedulesForPattern(
224
+ d.subMonths(parseDate(startDate), 8),
225
+ 14,
226
+ start => {
227
+ const day = d.format(new Date(), 'iiii');
228
+ const dayValue = day.slice(0, 2).toUpperCase();
229
+
230
+ return {
231
+ start,
232
+ frequency: 'monthly',
233
+ patterns: [
234
+ { type: dayValue, value: 2 },
235
+ { type: dayValue, value: 4 },
236
+ ],
237
+ };
238
+ },
239
+ accountId,
240
+ );
241
+ }
242
+
243
+ async function findStartDate(schedule) {
244
+ const conditions = schedule._conditions;
245
+ const dateCond = conditions.find(c => c.field === 'date');
246
+ let currentConfig = dateCond.value;
247
+
248
+ while (true) {
249
+ const prevConfig = currentConfig;
250
+ currentConfig = { ...prevConfig };
251
+
252
+ switch (currentConfig.frequency) {
253
+ case 'weekly':
254
+ currentConfig.start = dayFromDate(
255
+ d.subWeeks(
256
+ parseDate(currentConfig.start),
257
+ currentConfig.interval || 1,
258
+ ),
259
+ );
260
+
261
+ break;
262
+ case 'monthly':
263
+ currentConfig.start = dayFromDate(
264
+ d.subMonths(
265
+ parseDate(currentConfig.start),
266
+ currentConfig.interval || 1,
267
+ ),
268
+ );
269
+ break;
270
+ case 'yearly':
271
+ currentConfig.start = dayFromDate(
272
+ d.subYears(
273
+ parseDate(currentConfig.start),
274
+ currentConfig.interval || 1,
275
+ ),
276
+ );
277
+ break;
278
+ default:
279
+ throw new Error('findStartDate: invalid frequency');
280
+ }
281
+
282
+ const newConditions = conditions.map(c =>
283
+ c.field === 'date' ? { ...c, value: currentConfig } : c,
284
+ );
285
+
286
+ const { filters, errors } = conditionsToAQL(newConditions, {
287
+ recurDateBounds: 1,
288
+ });
289
+ if (errors.length > 0) {
290
+ // Somehow we generated an invalid config. Abort the whole
291
+ // process and don't change the date at all
292
+ currentConfig = null;
293
+ break;
294
+ }
295
+
296
+ const { data } = await aqlQuery(
297
+ q('transactions').filter({ $and: filters }).select('*'),
298
+ );
299
+
300
+ if (data.length === 0) {
301
+ // No data, revert back to the last valid value and stop
302
+ currentConfig = prevConfig;
303
+ break;
304
+ }
305
+ }
306
+
307
+ if (currentConfig) {
308
+ return {
309
+ ...schedule,
310
+ date: currentConfig,
311
+ _conditions: conditions.map(c =>
312
+ c.field === 'date' ? { ...c, value: currentConfig } : c,
313
+ ),
314
+ };
315
+ }
316
+ return schedule;
317
+ }
318
+
319
+ export async function findSchedules() {
320
+ // Patterns to look for:
321
+ // * Weekly
322
+ // * Every two weeks
323
+ // * Monthly on day X
324
+ // * Monthly on every 1st or 3rd day
325
+ // * Monthly on every 2nd or 4th day
326
+ //
327
+ // Search for them approx (+- 2 days) but track which transactions
328
+ // and find the best one...
329
+
330
+ const { data: accounts } = await aqlQuery(
331
+ q('accounts').filter({ closed: false }).select('*'),
332
+ );
333
+
334
+ let allSchedules = [];
335
+
336
+ for (const account of accounts) {
337
+ // Find latest transaction-ish to start with
338
+ const latestTrans = await db.first<Pick<db.DbViewTransaction, 'date'>>(
339
+ 'SELECT date FROM v_transactions WHERE account = ? AND parent_id IS NULL ORDER BY date DESC LIMIT 1',
340
+ [account.id],
341
+ );
342
+
343
+ if (latestTrans) {
344
+ const latestDate = fromDateRepr(latestTrans.date);
345
+ allSchedules = allSchedules.concat(
346
+ await weekly(latestDate, account.id),
347
+ await every2weeks(latestDate, account.id),
348
+ await monthly(latestDate, account.id),
349
+ await monthlyLastDay(latestDate, account.id),
350
+ await monthly1stor3rd(latestDate, account.id),
351
+ await monthly2ndor4th(latestDate, account.id),
352
+ );
353
+ }
354
+ }
355
+
356
+ const schedules = [...groupBy(allSchedules, 'payee').entries()].map(
357
+ ([, schedules]) => {
358
+ schedules.sort((s1, s2) => s2.rank - s1.rank);
359
+ const winner = schedules[0];
360
+
361
+ // Convert to schedule and return it
362
+ return {
363
+ id: uuidv4(),
364
+ account: winner.account,
365
+ payee: winner.payee,
366
+ date: winner.date,
367
+ amount: winner.amount,
368
+ _conditions: [
369
+ { op: 'is', field: 'account', value: winner.account },
370
+ { op: 'is', field: 'payee', value: winner.payee },
371
+ {
372
+ op: winner.exactDate ? 'is' : 'isapprox',
373
+ field: 'date',
374
+ value: winner.date,
375
+ },
376
+ {
377
+ op: winner.exactAmount ? 'is' : 'isapprox',
378
+ field: 'amount',
379
+ value: winner.amount,
380
+ },
381
+ ],
382
+ };
383
+ },
384
+ );
385
+
386
+ const finalized: Awaited<ReturnType<typeof findStartDate>> = [];
387
+ for (const schedule of schedules) {
388
+ finalized.push(await findStartDate(schedule));
389
+ }
390
+ return finalized;
391
+ }
@@ -0,0 +1,59 @@
1
+ import * as fs from '../platform/server/fs';
2
+ import { logger } from '../platform/server/log';
3
+
4
+ type ServerConfig = {
5
+ BASE_SERVER: string;
6
+ SYNC_SERVER: string;
7
+ SIGNUP_SERVER: string;
8
+ GOCARDLESS_SERVER: string;
9
+ SIMPLEFIN_SERVER: string;
10
+ PLUGGYAI_SERVER: string;
11
+ };
12
+
13
+ let config: ServerConfig | null = null;
14
+
15
+ function joinURL(base: string | URL, ...paths: string[]): string {
16
+ const url = new URL(base);
17
+ url.pathname = fs.join(url.pathname, ...paths);
18
+ return url.toString();
19
+ }
20
+
21
+ export function isValidBaseURL(base: string): boolean {
22
+ try {
23
+ return Boolean(new URL(base));
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+
29
+ export function setServer(url: string): void {
30
+ if (url == null) {
31
+ config = null;
32
+ } else {
33
+ config = getServer(url);
34
+ }
35
+ }
36
+
37
+ // `url` is optional; if not given it will provide the global config
38
+ export function getServer(url?: string): ServerConfig | null {
39
+ if (url) {
40
+ try {
41
+ return {
42
+ BASE_SERVER: url,
43
+ SYNC_SERVER: joinURL(url, '/sync'),
44
+ SIGNUP_SERVER: joinURL(url, '/account'),
45
+ GOCARDLESS_SERVER: joinURL(url, '/gocardless'),
46
+ SIMPLEFIN_SERVER: joinURL(url, '/simplefin'),
47
+ PLUGGYAI_SERVER: joinURL(url, '/pluggyai'),
48
+ };
49
+ } catch (error) {
50
+ logger.warn(
51
+ 'Unable to parse server URL - using the global config.',
52
+ { config },
53
+ error,
54
+ );
55
+ return config;
56
+ }
57
+ }
58
+ return config;
59
+ }
@@ -0,0 +1,101 @@
1
+ // @ts-strict-ignore
2
+ import { generateTransaction } from '../mocks';
3
+
4
+ import * as db from './db';
5
+ import * as sheet from './sheet';
6
+
7
+ beforeEach(global.emptyDatabase());
8
+
9
+ async function insertTransactions() {
10
+ await db.insertCategoryGroup({ id: 'group1', name: 'group1' });
11
+ await db.insertCategory({ id: 'cat1', name: 'cat1', cat_group: 'group1' });
12
+ await db.insertCategory({ id: 'cat2', name: 'cat2', cat_group: 'group1' });
13
+
14
+ await db.insertTransaction(
15
+ generateTransaction({
16
+ id: 'trans1',
17
+ amount: -3200,
18
+ account: '1',
19
+ category: 'cat1',
20
+ date: '2017-01-08',
21
+ })[0],
22
+ );
23
+ await db.insertTransaction(
24
+ generateTransaction({
25
+ id: 'trans2',
26
+ amount: -2800,
27
+ account: '1',
28
+ category: 'cat2',
29
+ date: '2017-01-10',
30
+ })[0],
31
+ );
32
+ await db.insertTransaction(
33
+ generateTransaction({
34
+ id: 'trans3',
35
+ amount: -9832,
36
+ account: '1',
37
+ category: 'cat2',
38
+ date: '2017-01-15',
39
+ })[0],
40
+ );
41
+ }
42
+
43
+ describe('Spreadsheet', () => {
44
+ test('transferring a category triggers an update', async () => {
45
+ const spreadsheet = await sheet.loadSpreadsheet(db);
46
+ await insertTransactions();
47
+
48
+ spreadsheet.startTransaction();
49
+ spreadsheet.set(
50
+ 'g!foo',
51
+ `=from transactions where category = "cat2" calculate { sum(amount) }`,
52
+ );
53
+ spreadsheet.endTransaction();
54
+
55
+ await new Promise(resolve => {
56
+ spreadsheet.onFinish(() => {
57
+ expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
58
+ resolve(undefined);
59
+ });
60
+ });
61
+
62
+ await db.deleteCategory({ id: 'cat1' }, 'cat2');
63
+
64
+ return new Promise(resolve => {
65
+ spreadsheet.onFinish(() => {
66
+ expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
67
+ resolve(undefined);
68
+ });
69
+ });
70
+ });
71
+
72
+ test('updating still works after transferring categories', async () => {
73
+ const spreadsheet = await sheet.loadSpreadsheet(db);
74
+ await insertTransactions();
75
+
76
+ await db.deleteCategory({ id: 'cat1' }, 'cat2');
77
+
78
+ spreadsheet.startTransaction();
79
+ spreadsheet.set(
80
+ 'g!foo',
81
+ `=from transactions where category = "cat2" calculate { sum(amount) }`,
82
+ );
83
+ spreadsheet.endTransaction();
84
+
85
+ await new Promise(resolve => {
86
+ spreadsheet.onFinish(() => {
87
+ expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
88
+ resolve(undefined);
89
+ });
90
+ });
91
+
92
+ await db.updateTransaction({ id: 'trans1', amount: 50000 });
93
+
94
+ await new Promise(resolve => {
95
+ spreadsheet.onFinish(() => {
96
+ expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
97
+ resolve(undefined);
98
+ });
99
+ });
100
+ });
101
+ });