@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,61 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+
3
+ import * as fs from '../../platform/server/fs';
4
+ import { handlers } from '../main';
5
+
6
+ export async function uniqueBudgetName(
7
+ initialName: string = 'My Finances',
8
+ ): Promise<string> {
9
+ const budgets = await handlers['get-budgets']();
10
+ let idx = 1;
11
+
12
+ // If there is a conflict, keep appending an index until there is no
13
+ // conflict and we have a unique name
14
+ let newName = initialName;
15
+ while (budgets.find(file => file.name === newName)) {
16
+ newName = `${initialName} ${idx}`;
17
+ idx++;
18
+ }
19
+
20
+ return newName;
21
+ }
22
+
23
+ export async function validateBudgetName(
24
+ name: string,
25
+ ): Promise<{ valid: boolean; message?: string }> {
26
+ const trimmedName = name.trim();
27
+ const uniqueName = await uniqueBudgetName(trimmedName);
28
+ let message: string | null = null;
29
+
30
+ if (trimmedName === '') message = 'Budget name cannot be blank';
31
+ if (trimmedName.length > 100) {
32
+ message = 'Budget name is too long (max length 100)';
33
+ }
34
+ if (uniqueName !== trimmedName) {
35
+ message = `"${name}" already exists, try "${uniqueName}" instead`;
36
+ }
37
+
38
+ return message ? { valid: false, message } : { valid: true };
39
+ }
40
+
41
+ export async function idFromBudgetName(name: string): Promise<string> {
42
+ let id = name.replace(/( |[^A-Za-z0-9])/g, '-') + '-' + uuidv4().slice(0, 7);
43
+
44
+ // Make sure the id is unique. There's a chance one could already
45
+ // exist (although very unlikely now that we append unique
46
+ // characters onto the id)
47
+ let index = 0;
48
+
49
+ let budgetDir = fs.getBudgetDir(id);
50
+ while (await fs.exists(budgetDir)) {
51
+ index++;
52
+ budgetDir = fs.getBudgetDir(id + index.toString());
53
+ }
54
+
55
+ // If a suffix was added, update the id
56
+ if (index > 0) {
57
+ id = id + index.toString();
58
+ }
59
+
60
+ return id;
61
+ }
@@ -0,0 +1,48 @@
1
+ export type Mappings = Map<string, Map<string, string>>;
2
+
3
+ export const mappingsToString = (mapping: Mappings): string =>
4
+ JSON.stringify(
5
+ Object.fromEntries(
6
+ [...mapping.entries()].map(([key, value]) => [
7
+ key,
8
+ Object.fromEntries(value),
9
+ ]),
10
+ ),
11
+ );
12
+
13
+ export const mappingsFromString = (str: string): Mappings => {
14
+ try {
15
+ const parsed = JSON.parse(str);
16
+ if (typeof parsed !== 'object' || parsed === null) {
17
+ throw new Error('Invalid mapping format');
18
+ }
19
+ return new Map(
20
+ Object.entries(parsed).map(([key, value]) => [
21
+ key,
22
+ new Map(Object.entries(value as object)),
23
+ ]),
24
+ );
25
+ } catch (e) {
26
+ const message = e instanceof Error ? e.message : e;
27
+ throw new Error(`Failed to parse mapping: ${String(message)}`);
28
+ }
29
+ };
30
+
31
+ export const defaultMappings: Mappings = new Map([
32
+ [
33
+ 'payment',
34
+ new Map([
35
+ ['date', 'date'],
36
+ ['payee', 'payeeName'],
37
+ ['notes', 'notes'],
38
+ ]),
39
+ ],
40
+ [
41
+ 'deposit',
42
+ new Map([
43
+ ['date', 'date'],
44
+ ['payee', 'payeeName'],
45
+ ['notes', 'notes'],
46
+ ]),
47
+ ],
48
+ ]);
@@ -0,0 +1,9 @@
1
+ import '@rschedule/standard-date-adapter/setup';
2
+ import { Schedule as OriginalSchedule } from '@rschedule/core/generators';
3
+
4
+ export * from '@rschedule/standard-date-adapter';
5
+ export * from '@rschedule/core';
6
+ export * from '@rschedule/core/generators';
7
+
8
+ // Creates a wrapper class to ensure constructor behavior when bundled with vite
9
+ export class RSchedule extends OriginalSchedule {}
@@ -0,0 +1,7 @@
1
+ export const isPlaywright = false;
2
+
3
+ export const OS: 'windows' | 'mac' | 'linux' | 'unknown' = 'unknown';
4
+ export const env: 'web' | 'mobile' | 'unknown' = 'unknown';
5
+ export const isBrowser = false;
6
+
7
+ export const isIOSAgent = false;
@@ -0,0 +1,21 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`range returns a full range 1`] = `
4
+ [
5
+ "2016-10",
6
+ "2016-11",
7
+ "2016-12",
8
+ "2017-01",
9
+ "2017-02",
10
+ "2017-03",
11
+ "2017-04",
12
+ "2017-05",
13
+ "2017-06",
14
+ "2017-07",
15
+ "2017-08",
16
+ "2017-09",
17
+ "2017-10",
18
+ "2017-11",
19
+ "2017-12",
20
+ ]
21
+ `;
@@ -0,0 +1,112 @@
1
+ import { evalArithmetic } from './arithmetic';
2
+ import { setNumberFormat } from './util';
3
+
4
+ describe('arithmetic', () => {
5
+ test('handles negative numbers', () => {
6
+ expect(evalArithmetic('-4')).toBe(-4);
7
+ expect(evalArithmetic('10 + -4')).toBe(6);
8
+ });
9
+
10
+ test('handles simple addition', () => {
11
+ expect(evalArithmetic('10 + 10')).toEqual(20);
12
+ expect(evalArithmetic('1.5 + 1.5')).toEqual(3);
13
+ expect(evalArithmetic('(12 + 3) + (10)')).toEqual(25);
14
+ expect(evalArithmetic('10 + 20 + 30 + 40')).toEqual(100);
15
+ });
16
+
17
+ test('handles simple subtraction', () => {
18
+ expect(evalArithmetic('10 - 10')).toEqual(0);
19
+ expect(evalArithmetic('4.5 - 1.5')).toEqual(3);
20
+ expect(evalArithmetic('(12 - 3) - (10)')).toEqual(-1);
21
+ expect(evalArithmetic('10 - 20 - 30 - 40')).toEqual(-80);
22
+ });
23
+
24
+ test('handles multiplication', () => {
25
+ expect(evalArithmetic('10 * 10')).toEqual(100);
26
+ expect(evalArithmetic('1.5 * 1.5')).toEqual(2.25);
27
+ expect(evalArithmetic('10 * 20 * 30 * 40')).toEqual(240000);
28
+ });
29
+
30
+ test('handles division', () => {
31
+ expect(evalArithmetic('10 / 10')).toEqual(1);
32
+ expect(evalArithmetic('1.5 / .5')).toEqual(3);
33
+ expect(evalArithmetic('2400 / 2 / 5')).toEqual(240);
34
+ });
35
+
36
+ test('handles order of operations', () => {
37
+ expect(evalArithmetic('(5 + 3) * 10')).toEqual(80);
38
+ expect(evalArithmetic('5 + 3 * 10')).toEqual(35);
39
+ expect(evalArithmetic('20^3 - 5 * (10 / 2)')).toEqual(7975);
40
+ });
41
+
42
+ test('handles exponent as right-associative', () => {
43
+ expect(evalArithmetic('2^3^2')).toEqual(512);
44
+ });
45
+
46
+ test('handles same-precedence operators left-to-right', () => {
47
+ expect(evalArithmetic('24 / 3 * 2')).toEqual(16);
48
+ expect(evalArithmetic('24 * 3 / 2')).toEqual(36);
49
+ expect(evalArithmetic('10 - 2 + 1')).toEqual(9);
50
+ expect(evalArithmetic('10 + 2 - 1')).toEqual(11);
51
+ });
52
+
53
+ test('respects current number format', () => {
54
+ expect(evalArithmetic('1,222.45')).toEqual(1222.45);
55
+
56
+ setNumberFormat({ format: 'space-comma', hideFraction: false });
57
+ expect(evalArithmetic('1\u202F222,45')).toEqual(1222.45);
58
+
59
+ setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
60
+ expect(evalArithmetic(`1\u2019222.45`)).toEqual(1222.45);
61
+ });
62
+
63
+ test('ignores leftover characters', () => {
64
+ expect(evalArithmetic('1+2)')).toBe(3);
65
+ expect(evalArithmetic('1+2)foo')).toBe(3);
66
+ expect(evalArithmetic('(1+2)x')).toBe(3);
67
+ expect(evalArithmetic('10+20 trailing')).toBe(30);
68
+ expect(evalArithmetic('1+2(3')).toBe(3);
69
+ });
70
+
71
+ test('handles apostrophe-dot format with keyboard apostrophe (U+0027)', () => {
72
+ setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
73
+
74
+ // Test with keyboard apostrophe (U+0027) - what users type
75
+ const keyboardApostrophe = '12\u0027345.67';
76
+ expect(keyboardApostrophe.charCodeAt(2)).toBe(0x0027); // Verify it's U+0027
77
+ expect(evalArithmetic(keyboardApostrophe)).toBe(12345.67);
78
+
79
+ // More test cases with keyboard apostrophe
80
+ expect(evalArithmetic('1\u0027234.56')).toBe(1234.56);
81
+ expect(evalArithmetic('1\u0027000.33')).toBe(1000.33);
82
+ expect(evalArithmetic('100\u0027000.99')).toBe(100000.99);
83
+ expect(evalArithmetic('1\u0027000\u0027000.50')).toBe(1000000.5);
84
+ });
85
+
86
+ test('handles apostrophe-dot format with typographic apostrophe (U+2019)', () => {
87
+ setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
88
+
89
+ // Test with right single quotation mark (U+2019) - what Intl.NumberFormat outputs
90
+ const intlApostrophe = '12\u2019345.67';
91
+ expect(intlApostrophe.charCodeAt(2)).toBe(0x2019); // Verify it's U+2019
92
+ expect(evalArithmetic(intlApostrophe)).toBe(12345.67);
93
+
94
+ // More test cases with typographic apostrophe
95
+ expect(evalArithmetic('1\u2019234.56')).toBe(1234.56);
96
+ expect(evalArithmetic('1\u2019000.33')).toBe(1000.33);
97
+ });
98
+
99
+ test('handles apostrophe-dot format in arithmetic expressions', () => {
100
+ setNumberFormat({ format: 'apostrophe-dot', hideFraction: false });
101
+
102
+ // Test arithmetic operations with keyboard apostrophe
103
+ expect(evalArithmetic('1\u0027000 + 2\u0027000')).toBe(3000);
104
+ expect(evalArithmetic('10\u0027000 - 2\u0027500')).toBe(7500);
105
+ expect(evalArithmetic('1\u0027000 * 2')).toBe(2000);
106
+ expect(evalArithmetic('4\u0027000 / 2')).toBe(2000);
107
+
108
+ // Test arithmetic operations with typographic apostrophe
109
+ expect(evalArithmetic('1\u2019000 + 2\u2019000')).toBe(3000);
110
+ expect(evalArithmetic('10\u2019000 - 2\u2019500')).toBe(7500);
111
+ });
112
+ });
@@ -0,0 +1,170 @@
1
+ import { currencyToAmount } from './util';
2
+
3
+ type ParserState = {
4
+ str: string;
5
+ index: number;
6
+ };
7
+
8
+ type Operator = '+' | '-' | '*' | '/' | '^';
9
+
10
+ type OperatorNode = {
11
+ op: Operator;
12
+ left: AstNode;
13
+ right: AstNode;
14
+ };
15
+
16
+ type AstNode = number | OperatorNode;
17
+
18
+ function fail(state: ParserState, msg: string): never {
19
+ throw new Error(
20
+ msg + ': ' + JSON.stringify(state.str.slice(state.index, 10)),
21
+ );
22
+ }
23
+
24
+ function char(state: ParserState): string | undefined {
25
+ return state.str[state.index];
26
+ }
27
+
28
+ function next(state: ParserState): string | null {
29
+ if (state.index >= state.str.length) {
30
+ return null;
31
+ }
32
+
33
+ const ch = char(state);
34
+ state.index++;
35
+ return ch ?? null;
36
+ }
37
+
38
+ function nextOperator(state: ParserState, op: string): boolean {
39
+ if (char(state) === op) {
40
+ next(state);
41
+ return true;
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ function parsePrimary(state: ParserState): number {
48
+ // We only support numbers
49
+ const isNegative = char(state) === '-';
50
+ if (isNegative) {
51
+ next(state);
52
+ }
53
+
54
+ let numberStr = '';
55
+ let currentChar = char(state);
56
+ while (
57
+ currentChar &&
58
+ currentChar.match(/[0-9,.'\u2019\u00A0\u202F ]|\p{Sc}/u)
59
+ ) {
60
+ const ch = next(state);
61
+ if (ch !== null) {
62
+ numberStr += ch;
63
+ }
64
+ currentChar = char(state);
65
+ }
66
+
67
+ if (numberStr === '') {
68
+ fail(state, 'Unexpected character');
69
+ }
70
+
71
+ const number = currencyToAmount(numberStr);
72
+ if (number === null) {
73
+ fail(state, 'Invalid number format');
74
+ }
75
+ return isNegative ? -number : number;
76
+ }
77
+
78
+ function parseParens(state: ParserState): AstNode {
79
+ if (char(state) === '(') {
80
+ next(state);
81
+ const expr = parseOperator(state);
82
+
83
+ if (char(state) !== ')') {
84
+ fail(state, 'Unbalanced parentheses');
85
+ }
86
+
87
+ next(state);
88
+ return expr;
89
+ }
90
+
91
+ return parsePrimary(state);
92
+ }
93
+
94
+ function parseExponent(state: ParserState): AstNode {
95
+ let node = parseParens(state);
96
+ if (nextOperator(state, '^')) {
97
+ node = { op: '^', left: node, right: parseExponent(state) };
98
+ }
99
+ return node;
100
+ }
101
+
102
+ function parseMultiplicative(state: ParserState): AstNode {
103
+ let node = parseExponent(state);
104
+ while (char(state) === '*' || char(state) === '/') {
105
+ const op = next(state) as '*' | '/';
106
+ node = { op, left: node, right: parseExponent(state) };
107
+ }
108
+ return node;
109
+ }
110
+
111
+ function parseAdditive(state: ParserState): AstNode {
112
+ let node = parseMultiplicative(state);
113
+ while (char(state) === '+' || char(state) === '-') {
114
+ const op = next(state) as '+' | '-';
115
+ node = { op, left: node, right: parseMultiplicative(state) };
116
+ }
117
+ return node;
118
+ }
119
+
120
+ // These operators go from high to low order of precedence
121
+ const parseOperator = parseAdditive;
122
+
123
+ function parse(expression: string): AstNode {
124
+ const state = { str: expression.replace(/\s/g, ''), index: 0 };
125
+ return parseOperator(state);
126
+ }
127
+
128
+ function evaluate(ast: AstNode): number {
129
+ if (typeof ast === 'number') {
130
+ return ast;
131
+ }
132
+
133
+ const { left, right, op } = ast;
134
+
135
+ switch (op) {
136
+ case '+':
137
+ return evaluate(left) + evaluate(right);
138
+ case '-':
139
+ return evaluate(left) - evaluate(right);
140
+ case '*':
141
+ return evaluate(left) * evaluate(right);
142
+ case '/':
143
+ return evaluate(left) / evaluate(right);
144
+ case '^':
145
+ return Math.pow(evaluate(left), evaluate(right));
146
+ default:
147
+ throw new Error('Unknown operator: ' + op);
148
+ }
149
+ }
150
+
151
+ export function evalArithmetic(
152
+ expression: string,
153
+ defaultValue: number | null = null,
154
+ ): number | null {
155
+ // An empty expression always evals to the default
156
+ if (expression === '') {
157
+ return defaultValue;
158
+ }
159
+
160
+ let result: number;
161
+ try {
162
+ result = evaluate(parse(expression));
163
+ } catch {
164
+ // If it errors, return the default value
165
+ return defaultValue;
166
+ }
167
+
168
+ // Never return NaN
169
+ return isNaN(result) ? defaultValue : result;
170
+ }
@@ -0,0 +1,135 @@
1
+ // @ts-strict-ignore
2
+ import { once, sequential } from './async';
3
+
4
+ function timeout(n) {
5
+ return new Promise(resolve => setTimeout(resolve, n));
6
+ }
7
+
8
+ function makeFunction(data) {
9
+ return async function fn(n, { throwError = false } = {}) {
10
+ data.push(n);
11
+ await timeout(10);
12
+
13
+ if (throwError) {
14
+ throw new Error('throwing error');
15
+ }
16
+
17
+ data.push(n);
18
+ await timeout(50);
19
+ data.push(n);
20
+ };
21
+ }
22
+
23
+ describe('async', () => {
24
+ test('sequential fn should force concurrent calls to be in order', async () => {
25
+ const test = async fn => {
26
+ fn(1);
27
+ fn(2);
28
+ await fn(3);
29
+ };
30
+
31
+ const data = [];
32
+ await test(makeFunction(data));
33
+ expect(data).toEqual([1, 2, 3, 1, 2, 3, 1, 2, 3]);
34
+
35
+ const seqData = [];
36
+ await test(sequential(makeFunction(seqData)));
37
+ expect(seqData).toEqual([1, 1, 1, 2, 2, 2, 3, 3, 3]);
38
+
39
+ expect(data.length).toEqual(seqData.length);
40
+ });
41
+
42
+ test('sequential fn should always call function when queue is empty', async () => {
43
+ const test = async fn => {
44
+ await fn(1);
45
+ await fn(2);
46
+ await fn(3);
47
+ };
48
+
49
+ const data = [];
50
+ await test(makeFunction(data));
51
+ expect(data).toEqual([1, 1, 1, 2, 2, 2, 3, 3, 3]);
52
+
53
+ const seqData = [];
54
+ await test(sequential(makeFunction(seqData)));
55
+ expect(seqData).toEqual([1, 1, 1, 2, 2, 2, 3, 3, 3]);
56
+
57
+ expect(data.length).toEqual(seqData.length);
58
+ });
59
+
60
+ test('sequential fn should still flush queue when error is thrown', async () => {
61
+ const test = async fn => {
62
+ fn(1);
63
+ fn(2, { throwError: true }).catch(() => {
64
+ // Ignore errors
65
+ });
66
+ await fn(3);
67
+ };
68
+
69
+ const data = [];
70
+ await test(makeFunction(data));
71
+ expect(data).toEqual([1, 2, 3, 1, 3, 1, 3]);
72
+
73
+ const seqData = [];
74
+ await test(sequential(makeFunction(seqData)));
75
+ expect(seqData).toEqual([1, 1, 1, 2, 3, 3, 3]);
76
+
77
+ expect(data.length).toEqual(seqData.length);
78
+ });
79
+
80
+ test('sequential fn should ignore promise chains in the future', async () => {
81
+ const data = [];
82
+ const fn = sequential(makeFunction(data));
83
+
84
+ void fn(1).then(() => {
85
+ // The next call should already have started (so it should have
86
+ // already appended 2 to the end). It shouldn't depend on this
87
+ // promise chain at all (important part being that if any errors
88
+ // happened in here, it wouldn't effect anything else)
89
+ expect(data).toEqual([1, 1, 1, 2]);
90
+ });
91
+ fn(2, { throwError: true }).catch(() => {
92
+ // Same as above
93
+ expect(data).toEqual([1, 1, 1, 2, 3]);
94
+ });
95
+ await fn(3);
96
+
97
+ expect(data).toEqual([1, 1, 1, 2, 3, 3, 3]);
98
+ });
99
+
100
+ test('once fn should only be called once', async () => {
101
+ let timesCalled = 0;
102
+ const fn = once(async () => {
103
+ await timeout(200);
104
+ timesCalled++;
105
+ });
106
+
107
+ await Promise.all([fn(), fn(), fn()]);
108
+
109
+ // It should only have been called once
110
+ expect(timesCalled).toBe(1);
111
+
112
+ // Make sure it's called again now that it's done executing
113
+ await Promise.all([fn(), fn()]);
114
+ expect(timesCalled).toBe(2);
115
+ });
116
+
117
+ test('once fn should coalesce multiple calls', async () => {
118
+ let timesCalled = 0;
119
+ const fn = once(async () => {
120
+ await timeout(200);
121
+ timesCalled++;
122
+ return {};
123
+ });
124
+
125
+ const results = await Promise.all([fn(), fn(), fn()]);
126
+
127
+ // It should only have been called once
128
+ expect(timesCalled).toBe(1);
129
+
130
+ // The results should all be identical (`toBe` is a strict
131
+ // comparison, like ===)
132
+ expect(results[0]).toBe(results[1]);
133
+ expect(results[0]).toBe(results[2]);
134
+ });
135
+ });
@@ -0,0 +1,76 @@
1
+ // oxlint-disable-next-line typescript/no-explicit-any
2
+ type AnyFunction = (...args: any[]) => any;
3
+
4
+ export function sequential<T extends AnyFunction>(
5
+ fn: T,
6
+ ): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> {
7
+ const sequenceState: {
8
+ running: Promise<Awaited<ReturnType<T>>> | null;
9
+ queue: Array<{
10
+ args: Parameters<T>;
11
+ resolve: (
12
+ value: Awaited<ReturnType<T>> | PromiseLike<Awaited<ReturnType<T>>>,
13
+ ) => void;
14
+ reject: (reason?: unknown) => void;
15
+ }>;
16
+ } = {
17
+ running: null,
18
+ queue: [],
19
+ };
20
+
21
+ function pump() {
22
+ const next = sequenceState.queue.shift();
23
+ if (next !== undefined) {
24
+ run(next.args, next.resolve, next.reject);
25
+ } else {
26
+ sequenceState.running = null;
27
+ }
28
+ }
29
+
30
+ function run(
31
+ args: Parameters<T>,
32
+ resolve: (
33
+ value: Awaited<ReturnType<T>> | PromiseLike<Awaited<ReturnType<T>>>,
34
+ ) => void,
35
+ reject: (reason?: unknown) => void,
36
+ ) {
37
+ sequenceState.running = fn.apply(null, args).then(
38
+ (val: Awaited<ReturnType<T>> | PromiseLike<Awaited<ReturnType<T>>>) => {
39
+ pump();
40
+ resolve(val);
41
+ },
42
+ (err: unknown) => {
43
+ pump();
44
+ reject(err);
45
+ },
46
+ );
47
+ }
48
+
49
+ return ((...args: Parameters<T>) => {
50
+ if (!sequenceState.running) {
51
+ return new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
52
+ return run(args, resolve, reject);
53
+ });
54
+ } else {
55
+ return new Promise<Awaited<ReturnType<T>>>((resolve, reject) => {
56
+ sequenceState.queue.push({ resolve, reject, args });
57
+ });
58
+ }
59
+ }) as T;
60
+ }
61
+
62
+ export function once<T extends AnyFunction>(
63
+ fn: T,
64
+ ): (...args: Parameters<T>) => Promise<Awaited<ReturnType<T>>> | null {
65
+ let promise: Promise<Awaited<ReturnType<T>>> | null = null;
66
+ return (...args: Parameters<T>) => {
67
+ if (!promise) {
68
+ promise = fn.apply(null, args).finally(() => {
69
+ promise = null;
70
+ });
71
+ return promise;
72
+ }
73
+
74
+ return promise;
75
+ };
76
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Default maximum distance (in meters) for nearby payee lookups.
3
+ * Payees with locations beyond this distance are not considered "nearby".
4
+ */
5
+ export const DEFAULT_MAX_DISTANCE_METERS = 500;