@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,560 @@
1
+ // @ts-strict-ignore
2
+ import type { IRuleOptions } from '@rschedule/core';
3
+ import * as d from 'date-fns';
4
+ import type { Locale } from 'date-fns';
5
+ import { t } from 'i18next';
6
+
7
+ import type { PayeeEntity, RecurConfig, ScheduleEntity } from '#types/models';
8
+ import { Condition } from '../server/rules';
9
+
10
+ import * as monthUtils from './months';
11
+ import { q } from './query';
12
+
13
+ export function getStatus(
14
+ nextDate: string,
15
+ completed: boolean,
16
+ hasTrans: boolean,
17
+ upcomingLength: string = '7',
18
+ ) {
19
+ const upcomingDays = getUpcomingDays(upcomingLength);
20
+ const today = monthUtils.currentDay();
21
+ if (completed) {
22
+ return 'completed';
23
+ } else if (hasTrans) {
24
+ return 'paid';
25
+ } else if (nextDate === today) {
26
+ return 'due';
27
+ } else if (
28
+ nextDate > today &&
29
+ nextDate <= monthUtils.addDays(today, upcomingDays)
30
+ ) {
31
+ return 'upcoming';
32
+ } else if (nextDate < today) {
33
+ return 'missed';
34
+ } else {
35
+ return 'scheduled';
36
+ }
37
+ }
38
+
39
+ export function getStatusLabel(status: string) {
40
+ switch (status) {
41
+ case 'completed':
42
+ return t('completed');
43
+ case 'paid':
44
+ return t('paid');
45
+ case 'due':
46
+ return t('due');
47
+ case 'upcoming':
48
+ return t('upcoming');
49
+ case 'missed':
50
+ return t('missed');
51
+ case 'scheduled':
52
+ return t('scheduled');
53
+ default:
54
+ return t('unknown');
55
+ }
56
+ }
57
+
58
+ export function getHasTransactionsQuery(schedules) {
59
+ const filters = schedules.map(schedule => {
60
+ const dateCond = schedule._conditions?.find(c => c.field === 'date');
61
+ return {
62
+ $and: {
63
+ schedule: schedule.id,
64
+ date: {
65
+ $gte:
66
+ dateCond && dateCond.op === 'is'
67
+ ? schedule.next_date
68
+ : monthUtils.subDays(schedule.next_date, 2),
69
+ },
70
+ },
71
+ };
72
+ });
73
+
74
+ return q('transactions')
75
+ .options({ splits: 'all' })
76
+ .filter({ $or: filters })
77
+ .orderBy({ date: 'desc' })
78
+ .select(['schedule', 'date']);
79
+ }
80
+
81
+ function makeNumberSuffix(num: number, locale: Locale) {
82
+ // Slight abuse of date-fns to turn a number like "1" into the full
83
+ // form "1st" but formatting a date with that number
84
+ return monthUtils.format(new Date(2020, 0, num, 12), 'do', locale);
85
+ }
86
+
87
+ function prettyDayName(day) {
88
+ const days = {
89
+ SU: t('Sunday'),
90
+ MO: t('Monday'),
91
+ TU: t('Tuesday'),
92
+ WE: t('Wednesday'),
93
+ TH: t('Thursday'),
94
+ FR: t('Friday'),
95
+ SA: t('Saturday'),
96
+ };
97
+ return days[day];
98
+ }
99
+
100
+ export function getRecurringDescription(
101
+ config: RecurConfig,
102
+ dateFormat: string,
103
+ locale: Locale,
104
+ ) {
105
+ const interval = config.interval || 1;
106
+
107
+ let endModeSuffix = '';
108
+ switch (config.endMode) {
109
+ case 'after_n_occurrences':
110
+ if (config.endOccurrences === 1) {
111
+ endModeSuffix = t('once');
112
+ } else {
113
+ endModeSuffix = t('{{endOccurrences}} times', {
114
+ endOccurrences: config.endOccurrences,
115
+ });
116
+ }
117
+ break;
118
+ case 'on_date':
119
+ endModeSuffix = t('until {{dateFormatted}}', {
120
+ dateFormatted: monthUtils.format(config.endDate, dateFormat),
121
+ });
122
+ break;
123
+ default:
124
+ break;
125
+ }
126
+
127
+ const weekendSolveModeString = config.weekendSolveMode
128
+ ? config.weekendSolveMode === 'after'
129
+ ? t('(after weekend)')
130
+ : t('(before weekend)')
131
+ : '';
132
+
133
+ const weekendSolveSuffix = config.skipWeekend ? weekendSolveModeString : '';
134
+ const suffix = endModeSuffix
135
+ ? `, ${endModeSuffix} ${weekendSolveSuffix}`
136
+ : `${weekendSolveSuffix}`;
137
+
138
+ let desc = null;
139
+
140
+ switch (config.frequency) {
141
+ case 'daily':
142
+ desc =
143
+ interval !== 1
144
+ ? t(`Every {{interval}} days`, { interval })
145
+ : t('Every day');
146
+ break;
147
+ case 'weekly':
148
+ desc =
149
+ interval !== 1
150
+ ? t(`Every {{interval}} weeks on {{dateFormatted}}`, {
151
+ interval,
152
+ dateFormatted: monthUtils.format(config.start, 'EEEE', locale),
153
+ })
154
+ : t('Every week on {{dateFormatted}}', {
155
+ dateFormatted: monthUtils.format(config.start, 'EEEE', locale),
156
+ });
157
+ break;
158
+ case 'monthly':
159
+ if (config.patterns && config.patterns.length > 0) {
160
+ // Sort the days ascending. We filter out -1 because that
161
+ // represents "last days" and should always be last, but this
162
+ // sort would put them first
163
+ let patterns = [...config.patterns]
164
+ .sort((p1, p2) => {
165
+ const typeOrder =
166
+ (p1.type === 'day' ? 1 : 0) - (p2.type === 'day' ? 1 : 0);
167
+ const valOrder = p1.value - p2.value;
168
+
169
+ if (typeOrder === 0) {
170
+ return valOrder;
171
+ }
172
+ return typeOrder;
173
+ })
174
+ .filter(p => p.value !== -1);
175
+
176
+ // Add on all -1 values to the end
177
+ patterns = patterns.concat(config.patterns.filter(p => p.value === -1));
178
+
179
+ const strs: string[] = [];
180
+
181
+ const uniqueDays = new Set(patterns.map(p => p.type));
182
+ const isSameDay = uniqueDays.size === 1 && !uniqueDays.has('day');
183
+ for (const pattern of patterns) {
184
+ if (pattern.type === 'day') {
185
+ if (pattern.value === -1) {
186
+ strs.push(t('last day'));
187
+ } else {
188
+ // Example: 15th day
189
+ strs.push(makeNumberSuffix(pattern.value, locale));
190
+ }
191
+ } else {
192
+ const dayName = isSameDay ? '' : ' ' + prettyDayName(pattern.type);
193
+
194
+ if (pattern.value === -1) {
195
+ // Example: last Monday
196
+ strs.push(t('last') + dayName);
197
+ } else {
198
+ // Example: 3rd Monday
199
+ strs.push(makeNumberSuffix(pattern.value, locale) + dayName);
200
+ }
201
+ }
202
+ }
203
+
204
+ let range = '';
205
+ if (strs.length > 2) {
206
+ range += strs.slice(0, strs.length - 1).join(', ');
207
+ range += `, ${t('and')} `;
208
+ range += strs[strs.length - 1];
209
+ } else {
210
+ range += strs.join(` ${t('and')} `);
211
+ }
212
+
213
+ if (isSameDay) {
214
+ range += ' ' + prettyDayName(patterns[0].type);
215
+ }
216
+
217
+ desc =
218
+ interval !== 1
219
+ ? t(`Every {{interval}} months on the {{range}}`, {
220
+ interval,
221
+ range,
222
+ })
223
+ : t('Every month on the {{range}}', { range });
224
+ } else {
225
+ desc =
226
+ interval !== 1
227
+ ? t(`Every {{interval}} months on the {{dateFormatted}}`, {
228
+ interval,
229
+ dateFormatted: monthUtils.format(config.start, 'do', locale),
230
+ })
231
+ : t('Every month on the {{dateFormatted}}', {
232
+ dateFormatted: monthUtils.format(config.start, 'do', locale),
233
+ });
234
+ }
235
+ break;
236
+
237
+ case 'yearly':
238
+ desc =
239
+ interval !== 1
240
+ ? t(`Every {{interval}} years on {{dateFormatted}}`, {
241
+ interval,
242
+ dateFormatted: monthUtils.format(config.start, 'LLL do', locale),
243
+ })
244
+ : t('Every year on {{dateFormatted}}', {
245
+ dateFormatted: monthUtils.format(config.start, 'LLL do', locale),
246
+ });
247
+ break;
248
+
249
+ default:
250
+ return t('Recurring error');
251
+ }
252
+
253
+ return `${desc}${suffix}`.trim();
254
+ }
255
+
256
+ type ScheduleRuleOptions = IRuleOptions & {
257
+ frequency: string;
258
+ interval?: number;
259
+ byHourOfDay?: number[];
260
+ };
261
+
262
+ export function recurConfigToRSchedule(config) {
263
+ const base: ScheduleRuleOptions = {
264
+ start: monthUtils.parseDate(config.start),
265
+ frequency: config.frequency.toUpperCase(),
266
+ byHourOfDay: [12],
267
+ };
268
+
269
+ if (config.interval) {
270
+ base.interval = config.interval;
271
+ }
272
+
273
+ switch (config.endMode) {
274
+ case 'after_n_occurrences':
275
+ base.count = config.endOccurrences;
276
+ break;
277
+ case 'on_date':
278
+ base.end = monthUtils.parseDate(config.endDate);
279
+ break;
280
+ default:
281
+ break;
282
+ }
283
+
284
+ const abbrevDay = name => name.slice(0, 2).toUpperCase();
285
+
286
+ switch (config.frequency) {
287
+ case 'daily':
288
+ // Nothing to do
289
+ return [base];
290
+ case 'weekly':
291
+ // Nothing to do
292
+ return [base];
293
+ case 'monthly':
294
+ if (config.patterns && config.patterns.length > 0) {
295
+ const days = config.patterns.filter(p => p.type === 'day');
296
+ const dayNames = config.patterns.filter(p => p.type !== 'day');
297
+
298
+ return [
299
+ days.length > 0 && { ...base, byDayOfMonth: days.map(p => p.value) },
300
+ dayNames.length > 0 && {
301
+ ...base,
302
+ byDayOfWeek: dayNames.map(p => [abbrevDay(p.type), p.value]),
303
+ },
304
+ ].filter(Boolean);
305
+ } else {
306
+ // Nothing to do
307
+ return [base];
308
+ }
309
+ case 'yearly':
310
+ return [base];
311
+ default:
312
+ throw new Error('Invalid recurring date config');
313
+ }
314
+ }
315
+
316
+ export function extractScheduleConds(conditions) {
317
+ return {
318
+ payee:
319
+ conditions.find(cond => cond.op === 'is' && cond.field === 'payee') ||
320
+ conditions.find(
321
+ cond => cond.op === 'is' && cond.field === 'description',
322
+ ) ||
323
+ null,
324
+ account:
325
+ conditions.find(cond => cond.op === 'is' && cond.field === 'account') ||
326
+ conditions.find(cond => cond.op === 'is' && cond.field === 'acct') ||
327
+ null,
328
+ amount:
329
+ conditions.find(
330
+ cond =>
331
+ (cond.op === 'is' ||
332
+ cond.op === 'isapprox' ||
333
+ cond.op === 'isbetween') &&
334
+ cond.field === 'amount',
335
+ ) || null,
336
+ date:
337
+ conditions.find(
338
+ cond =>
339
+ (cond.op === 'is' || cond.op === 'isapprox') && cond.field === 'date',
340
+ ) || null,
341
+ };
342
+ }
343
+
344
+ export function getNextDate(
345
+ dateCond,
346
+ start = new Date(monthUtils.currentDay()),
347
+ noSkipWeekend = false,
348
+ ) {
349
+ start = d.startOfDay(start);
350
+
351
+ const cond = new Condition(dateCond.op, 'date', dateCond.value, null);
352
+ const value = cond.getValue();
353
+
354
+ if (value.type === 'date') {
355
+ return value.date;
356
+ } else if (value.type === 'recur') {
357
+ let dates = value.schedule.occurrences({ start, take: 1 }).toArray();
358
+
359
+ if (dates.length === 0) {
360
+ // Could be a schedule with limited occurrences, so we try to
361
+ // find the last occurrence
362
+ dates = value.schedule.occurrences({ reverse: true, take: 1 }).toArray();
363
+ }
364
+
365
+ if (dates.length > 0) {
366
+ let date = dates[0].date;
367
+ if (value.schedule.data.skipWeekend && !noSkipWeekend) {
368
+ date = getDateWithSkippedWeekend(
369
+ date,
370
+ value.schedule.data.weekendSolve,
371
+ );
372
+ }
373
+ return monthUtils.dayFromDate(date);
374
+ }
375
+ }
376
+ return null;
377
+ }
378
+
379
+ export function getDateWithSkippedWeekend(
380
+ date: Date,
381
+ solveMode: 'after' | 'before',
382
+ ) {
383
+ if (d.isWeekend(date)) {
384
+ if (solveMode === 'after') {
385
+ return d.nextMonday(date);
386
+ } else if (solveMode === 'before') {
387
+ return d.previousFriday(date);
388
+ } else {
389
+ throw new Error('Unknown weekend solve mode, this should not happen!');
390
+ }
391
+ }
392
+ return date;
393
+ }
394
+
395
+ export function getScheduledAmount(
396
+ amount: number | { num1: number; num2: number },
397
+ inverse: boolean = false,
398
+ ): number {
399
+ // this check is temporary, and required at the moment as a schedule rule
400
+ // allows the amount condition to be deleted which causes a crash
401
+ if (amount == null) return 0;
402
+
403
+ if (typeof amount === 'number') {
404
+ return inverse ? -amount : amount;
405
+ }
406
+ const avg = (amount.num1 + amount.num2) / 2;
407
+ return inverse ? -Math.round(avg) : Math.round(avg);
408
+ }
409
+
410
+ export function describeSchedule(
411
+ schedule: ScheduleEntity,
412
+ payee?: PayeeEntity,
413
+ ) {
414
+ if (payee) {
415
+ return `${payee.name} (${schedule.next_date})`;
416
+ } else {
417
+ return `${t('Next:')} ${schedule.next_date}`;
418
+ }
419
+ }
420
+
421
+ export function getUpcomingDays(
422
+ upcomingLength = '7',
423
+ today = monthUtils.currentDay(), // for testability
424
+ ): number {
425
+ const month = monthUtils.getMonth(today);
426
+
427
+ switch (upcomingLength) {
428
+ case 'currentMonth': {
429
+ const day = monthUtils.getDay(today);
430
+ const end = monthUtils.getDay(monthUtils.getMonthEnd(today));
431
+ return end - day;
432
+ }
433
+ case 'oneMonth': {
434
+ return monthUtils.differenceInCalendarDays(
435
+ monthUtils.nextMonth(month),
436
+ month,
437
+ );
438
+ }
439
+ default:
440
+ if (upcomingLength.includes('-')) {
441
+ const [num, unit] = upcomingLength.split('-');
442
+ const value = Math.max(1, parseInt(num, 10));
443
+ switch (unit) {
444
+ case 'day':
445
+ return value;
446
+ case 'week':
447
+ return value * 7;
448
+ case 'month':
449
+ const future = monthUtils.addMonths(today, value);
450
+ return monthUtils.differenceInCalendarDays(future, month) + 1;
451
+ case 'year':
452
+ const futureYear = monthUtils.addMonths(today, value * 12);
453
+ return monthUtils.differenceInCalendarDays(futureYear, month) + 1;
454
+ default:
455
+ return 7;
456
+ }
457
+ }
458
+ return parseInt(upcomingLength, 10);
459
+ }
460
+ }
461
+
462
+ export function scheduleIsRecurring(dateCond: Condition | null) {
463
+ if (!dateCond) {
464
+ return false;
465
+ }
466
+ const cond = new Condition(dateCond.op, 'date', dateCond.value, null);
467
+ const value = cond.getValue();
468
+
469
+ return value.type === 'recur';
470
+ }
471
+
472
+ export type ScheduleStatusType = ReturnType<typeof getStatus>;
473
+ export type ScheduleStatuses = Map<ScheduleEntity['id'], ScheduleStatusType>;
474
+
475
+ export function isForPreview(
476
+ schedule: ScheduleEntity,
477
+ statuses: ScheduleStatuses,
478
+ ) {
479
+ const status = statuses.get(schedule.id);
480
+ return (
481
+ !schedule.completed &&
482
+ ['due', 'upcoming', 'missed', 'paid'].includes(status!)
483
+ );
484
+ }
485
+
486
+ export function computeSchedulePreviewTransactions(
487
+ schedules: readonly ScheduleEntity[],
488
+ statuses: ScheduleStatuses,
489
+ upcomingLength?: string,
490
+ filter?: (schedule: ScheduleEntity) => boolean,
491
+ ) {
492
+ const schedulesForPreview = schedules
493
+ .filter(s => isForPreview(s, statuses))
494
+ .filter(filter ? filter : () => true);
495
+
496
+ const today = d.startOfDay(monthUtils.parseDate(monthUtils.currentDay()));
497
+
498
+ const upcomingPeriodEnd = d.startOfDay(
499
+ monthUtils.parseDate(
500
+ monthUtils.addDays(today, getUpcomingDays(upcomingLength)),
501
+ ),
502
+ );
503
+
504
+ return schedulesForPreview
505
+ .flatMap(schedule => {
506
+ const { date: dateConditions } = extractScheduleConds(
507
+ schedule._conditions,
508
+ );
509
+
510
+ const status = statuses.get(schedule.id);
511
+ const isRecurring = scheduleIsRecurring(dateConditions);
512
+
513
+ const dates = [schedule.next_date];
514
+ let day = d.startOfDay(monthUtils.parseDate(schedule.next_date));
515
+ if (isRecurring) {
516
+ while (day <= upcomingPeriodEnd) {
517
+ const nextDate = getNextDate(dateConditions, day);
518
+
519
+ if (
520
+ d.startOfDay(monthUtils.parseDate(nextDate)) > upcomingPeriodEnd
521
+ ) {
522
+ break;
523
+ }
524
+
525
+ if (dates.includes(nextDate)) {
526
+ day = d.startOfDay(
527
+ monthUtils.parseDate(monthUtils.addDays(day, 1)),
528
+ );
529
+ continue;
530
+ }
531
+
532
+ dates.push(nextDate);
533
+ day = d.startOfDay(
534
+ monthUtils.parseDate(monthUtils.addDays(nextDate, 1)),
535
+ );
536
+ }
537
+ }
538
+
539
+ if (status === 'paid') {
540
+ dates.shift();
541
+ }
542
+
543
+ return dates.map(date => ({
544
+ id: 'preview/' + schedule.id + `/${date}`,
545
+ payee: schedule._payee,
546
+ account: schedule._account,
547
+ amount: getScheduledAmount(schedule._amount),
548
+ date,
549
+ schedule: schedule.id,
550
+ forceUpcoming:
551
+ (date !== schedule.next_date || status === 'paid') &&
552
+ date >= monthUtils.currentDay(),
553
+ }));
554
+ })
555
+ .sort(
556
+ (a, b) =>
557
+ monthUtils.parseDate(b.date).getTime() -
558
+ monthUtils.parseDate(a.date).getTime() || a.amount - b.amount,
559
+ );
560
+ }
@@ -0,0 +1,156 @@
1
+ import { logger } from '../platform/server/log';
2
+
3
+ export let tracer: null | ReturnType<typeof execTracer> = null;
4
+
5
+ function timeout<T extends Promise<unknown>>(promise: T, n: number) {
6
+ let resolve: (response: string) => void;
7
+ const timeoutPromise = new Promise<string>(_ => (resolve = _));
8
+ const timer = setTimeout(() => resolve(`timeout(${n})`), n);
9
+
10
+ return Promise.race([
11
+ promise.then(res => {
12
+ clearTimeout(timer);
13
+ return res;
14
+ }),
15
+ timeoutPromise,
16
+ ]);
17
+ }
18
+
19
+ export function resetTracer() {
20
+ tracer = execTracer();
21
+ }
22
+
23
+ export function execTracer<T>() {
24
+ const queue: Array<{ name: string; data?: T | undefined }> = [];
25
+ let hasStarted = false;
26
+ let waitingFor: null | {
27
+ name: string;
28
+ reject: (error: Error) => void;
29
+ resolve: (data?: T) => void;
30
+ } = null;
31
+ let ended = false;
32
+
33
+ const log = false;
34
+
35
+ return {
36
+ event(name: string, data?: T) {
37
+ if (!hasStarted) {
38
+ return;
39
+ } else if (log) {
40
+ logger.log(`--- event(${name}, ${JSON.stringify(data)}) ---`);
41
+ }
42
+
43
+ if (ended) {
44
+ throw new Error(
45
+ `Tracer received event but didn't expect it: ${name} with data: ${JSON.stringify(
46
+ data,
47
+ )}`,
48
+ );
49
+ } else if (waitingFor) {
50
+ if (waitingFor.name !== name) {
51
+ waitingFor.reject(
52
+ new Error(
53
+ `Event traced "${name}" but expected "${waitingFor.name}"`,
54
+ ),
55
+ );
56
+ } else {
57
+ waitingFor.resolve(data);
58
+ }
59
+ waitingFor = null;
60
+ } else {
61
+ queue.push({ name, data });
62
+ }
63
+ },
64
+
65
+ wait(name: string) {
66
+ if (waitingFor) {
67
+ throw new Error(
68
+ `Already waiting for ${waitingFor.name}, cannot wait for multiple events`,
69
+ );
70
+ }
71
+
72
+ return new Promise((resolve, reject) => {
73
+ waitingFor = { resolve, reject, name };
74
+ });
75
+ },
76
+
77
+ expectWait(name: string, data?: T) {
78
+ if (!hasStarted) {
79
+ throw new Error(`Expected "${name}" but tracer hasn't started yet`);
80
+ } else if (log) {
81
+ logger.log(`--- expectWait(${name}) ---`);
82
+ }
83
+
84
+ const promise = this.wait(name);
85
+ if (data === undefined) {
86
+ // We want to ignore the result
87
+ return expect(
88
+ timeout(
89
+ promise.then(() => true),
90
+ 1000,
91
+ ),
92
+ ).resolves.toEqual(true);
93
+ }
94
+
95
+ if (typeof data === 'function') {
96
+ return expect(timeout(promise, 1000))
97
+ .resolves.toBeTruthy()
98
+ .then(() => promise)
99
+ .then(res => data(res));
100
+ } else {
101
+ // We use this form because it tracks the right location in the
102
+ // test when it fails. It's annoying to always write this in the
103
+ // test though, so this provides a clean API. The right line
104
+ // number in the test will show up in the stack.
105
+ return expect(timeout(promise, 1000)).resolves.toEqual(data);
106
+ }
107
+ },
108
+
109
+ expectNow(name: string, data?: T) {
110
+ if (!hasStarted) {
111
+ throw new Error(`Expected "${name}" but tracer hasn't started yet`);
112
+ } else if (log) {
113
+ logger.log(`--- expectNow(${name}) ---`);
114
+ }
115
+
116
+ const entry = queue.shift();
117
+
118
+ if (!entry) {
119
+ throw new Error(
120
+ `Expected event "${name}" but none found - has it happened yet?`,
121
+ );
122
+ } else if (entry.name === name) {
123
+ if (typeof data === 'function') {
124
+ data(entry.data);
125
+ } else {
126
+ expect(entry.data).toEqual(data);
127
+ }
128
+ } else {
129
+ throw new Error(
130
+ `Event traced "${queue[0].name}" but expected "${name}"`,
131
+ );
132
+ }
133
+ },
134
+
135
+ expect(name: string, data?: T) {
136
+ if (queue.length === 0) {
137
+ return this.expectWait(name, data);
138
+ }
139
+ return this.expectNow(name, data);
140
+ },
141
+
142
+ start() {
143
+ hasStarted = true;
144
+ },
145
+
146
+ end() {
147
+ if (hasStarted && queue.length !== 0) {
148
+ const str = queue.map(x => JSON.stringify(x));
149
+ throw new Error(
150
+ 'Event tracer ended with existing events: ' + str.join('\n\n'),
151
+ );
152
+ }
153
+ ended = true;
154
+ },
155
+ };
156
+ }