@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,679 @@
1
+ // @ts-strict-ignore
2
+ import * as asyncStorage from '../../platform/server/asyncStorage';
3
+ import * as monthUtils from '../../shared/months';
4
+ import type { SyncedPrefs } from '../../types/prefs';
5
+ import * as db from '../db';
6
+ import { loadMappings } from '../db/mappings';
7
+ import { post } from '../post';
8
+ import { getServer } from '../server-config';
9
+ import { handlers } from '../tests/mockSyncServer';
10
+ import { insertRule, loadRules } from '../transactions/transaction-rules';
11
+
12
+ import {
13
+ addTransactions,
14
+ reconcileTransactions,
15
+ simpleFinBatchSync,
16
+ } from './sync';
17
+
18
+ vi.mock('../../shared/months', async () => ({
19
+ ...(await vi.importActual('../../shared/months')),
20
+ currentDay: vi.fn(),
21
+ currentMonth: vi.fn(),
22
+ }));
23
+
24
+ beforeEach(async () => {
25
+ vi.resetAllMocks();
26
+ vi.mocked(monthUtils.currentDay).mockReturnValue('2017-10-15');
27
+ vi.mocked(monthUtils.currentMonth).mockReturnValue('2017-10');
28
+ await global.emptyDatabase()();
29
+ await loadMappings();
30
+ await loadRules();
31
+ });
32
+
33
+ function getAllTransactions() {
34
+ return db.all<
35
+ db.DbViewTransactionInternal & { payee_name: db.DbPayee['name'] }
36
+ >(
37
+ `SELECT t.*, p.name as payee_name
38
+ FROM v_transactions_internal t
39
+ LEFT JOIN payees p ON p.id = t.payee
40
+ ORDER BY date DESC, amount DESC, id
41
+ `,
42
+ );
43
+ }
44
+
45
+ async function prepareDatabase() {
46
+ await db.insertCategoryGroup({ id: 'group1', name: 'group1', is_income: 1 });
47
+ await db.insertCategory({
48
+ name: 'income',
49
+ cat_group: 'group1',
50
+ is_income: 1,
51
+ });
52
+
53
+ const { accounts } = await post(getServer().GOCARDLESS_SERVER + '/accounts', {
54
+ client_id: '',
55
+ group_id: '',
56
+ item_id: '1',
57
+ });
58
+ const acct = accounts[0];
59
+
60
+ const id = await db.insertAccount({
61
+ id: 'one',
62
+ account_id: acct.account_id,
63
+ name: acct.official_name,
64
+ balance_current: acct.balances.current,
65
+ });
66
+ await db.insertPayee({
67
+ id: 'transfer-' + id,
68
+ name: '',
69
+ transfer_acct: id,
70
+ });
71
+
72
+ return { id, account_id: acct.account_id };
73
+ }
74
+
75
+ async function getAllPayees() {
76
+ return (await db.getPayees()).filter(p => p.transfer_acct == null);
77
+ }
78
+
79
+ describe('Account sync', () => {
80
+ test('reconcile creates payees correctly', async () => {
81
+ const { id } = await prepareDatabase();
82
+
83
+ let payees = await getAllPayees();
84
+ expect(payees.length).toBe(0);
85
+
86
+ await reconcileTransactions(id, [
87
+ { date: '2020-01-02', payee_name: 'bakkerij', amount: 4133 },
88
+ { date: '2020-01-03', payee_name: 'kroger', amount: 5000 },
89
+ ]);
90
+
91
+ payees = await getAllPayees();
92
+ expect(payees.length).toBe(2);
93
+
94
+ const transactions = await getAllTransactions();
95
+ expect(transactions.length).toBe(2);
96
+ expect(transactions.find(t => t.amount === 4133).payee).toBe(
97
+ payees.find(p => p.name === 'Bakkerij').id,
98
+ );
99
+ expect(transactions.find(t => t.amount === 5000).payee).toBe(
100
+ payees.find(p => p.name === 'Kroger').id,
101
+ );
102
+ });
103
+
104
+ test('reconcile handles transactions with undefined fields', async () => {
105
+ const { id: acctId } = await prepareDatabase();
106
+
107
+ await db.insertTransaction({
108
+ id: 'one',
109
+ account: acctId,
110
+ amount: 2948,
111
+ date: '2020-01-01',
112
+ });
113
+
114
+ await reconcileTransactions(acctId, [
115
+ { date: '2020-01-02' },
116
+ { date: '2020-01-01', amount: 2948 },
117
+ ]);
118
+
119
+ const transactions = await getAllTransactions();
120
+ expect(transactions.length).toBe(2);
121
+ expect(transactions).toMatchSnapshot();
122
+
123
+ // No payees should be created
124
+ const payees = await getAllPayees();
125
+ expect(payees.length).toBe(0);
126
+
127
+ // Make _at least_ the date is required
128
+ await expect(reconcileTransactions(acctId, [{}])).rejects.toThrow(
129
+ /`date` is required/,
130
+ );
131
+ });
132
+
133
+ test('reconcile doesnt rematch deleted transactions if reimport disabled', async () => {
134
+ const { id: acctId } = await prepareDatabase();
135
+ const reimportKey =
136
+ `sync-reimport-deleted-${acctId}` satisfies keyof SyncedPrefs;
137
+ await db.update('preferences', { id: reimportKey, value: 'false' });
138
+
139
+ await reconcileTransactions(acctId, [
140
+ { date: '2020-01-01', imported_id: 'finid' },
141
+ ]);
142
+
143
+ const transactions1 = await getAllTransactions();
144
+ expect(transactions1.length).toBe(1);
145
+
146
+ await db.deleteTransaction(transactions1[0]);
147
+
148
+ await reconcileTransactions(acctId, [
149
+ { date: '2020-01-01', imported_id: 'finid' },
150
+ ]);
151
+ const transactions2 = await getAllTransactions();
152
+ expect(transactions2.length).toBe(1);
153
+ expect(transactions2).toMatchSnapshot();
154
+ });
155
+
156
+ test('reconcile does rematch deleted transactions by default', async () => {
157
+ const { id: acctId } = await prepareDatabase();
158
+
159
+ await reconcileTransactions(acctId, [
160
+ { date: '2020-01-01', imported_id: 'finid' },
161
+ ]);
162
+
163
+ const transactions1 = await getAllTransactions();
164
+ expect(transactions1.length).toBe(1);
165
+
166
+ await db.deleteTransaction(transactions1[0]);
167
+
168
+ await reconcileTransactions(acctId, [
169
+ { date: '2020-01-01', imported_id: 'finid' },
170
+ ]);
171
+ const transactions2 = await getAllTransactions();
172
+ expect(transactions2.length).toBe(2);
173
+ expect(transactions2).toMatchSnapshot();
174
+ });
175
+
176
+ test('reconcile run rules with inferred payee', async () => {
177
+ const { id: acctId } = await prepareDatabase();
178
+ await db.insertCategoryGroup({
179
+ id: 'group2',
180
+ name: 'group2',
181
+ });
182
+ const catId = await db.insertCategory({
183
+ name: 'Food',
184
+ cat_group: 'group2',
185
+ });
186
+
187
+ const payeeId = await db.insertPayee({ name: 'bakkerij' });
188
+
189
+ await insertRule({
190
+ stage: null,
191
+ conditionsOp: 'and',
192
+ conditions: [{ op: 'is', field: 'payee', value: payeeId }],
193
+ actions: [{ op: 'set', field: 'category', value: catId }],
194
+ });
195
+
196
+ await reconcileTransactions(acctId, [
197
+ { date: '2020-01-02', payee_name: 'Bakkerij', amount: 4133 },
198
+ ]);
199
+
200
+ const transactions = await getAllTransactions();
201
+ // Even though the payee was inferred from the string name (no
202
+ // renaming rules ran), it should match the above rule and set the
203
+ // category
204
+ expect(transactions.length).toBe(1);
205
+ expect(transactions[0].payee).toBe(payeeId);
206
+ expect(transactions[0].category).toBe(catId);
207
+
208
+ // It also should not have created a payee
209
+ const payees = await getAllPayees();
210
+ expect(payees.length).toBe(1);
211
+ expect(payees[0].id).toBe(payeeId);
212
+ });
213
+
214
+ test('reconcile avoids creating blank payees', async () => {
215
+ const { id: acctId } = await prepareDatabase();
216
+
217
+ await reconcileTransactions(acctId, [
218
+ { date: '2020-01-02', payee_name: ' ', amount: 4133 },
219
+ ]);
220
+
221
+ const transactions = await getAllTransactions();
222
+ // Even though the payee was inferred from the string name (no
223
+ // renaming rules ran), it should match the above rule and set the
224
+ // category
225
+ expect(transactions.length).toBe(1);
226
+ expect(transactions[0].payee).toBe(null);
227
+ expect(transactions[0].amount).toBe(4133);
228
+ expect(transactions[0].date).toBe(20200102);
229
+
230
+ // It also should not have created a payee
231
+ const payees = await getAllPayees();
232
+ expect(payees.length).toBe(0);
233
+ });
234
+
235
+ test('reconcile run rules dont create unnecessary payees', async () => {
236
+ const { id: acctId } = await prepareDatabase();
237
+
238
+ const payeeId = await db.insertPayee({ name: 'bakkerij-renamed' });
239
+
240
+ await insertRule({
241
+ stage: null,
242
+ conditionsOp: 'and',
243
+ conditions: [{ op: 'is', field: 'imported_payee', value: 'Bakkerij' }],
244
+ actions: [{ op: 'set', field: 'payee', value: payeeId }],
245
+ });
246
+
247
+ await reconcileTransactions(acctId, [
248
+ { date: '2020-01-02', payee_name: 'bakkerij', amount: 4133 },
249
+ ]);
250
+
251
+ const payees = await getAllPayees();
252
+ expect(payees.length).toBe(1);
253
+ expect(payees[0].id).toBe(payeeId);
254
+
255
+ const transactions = await getAllTransactions();
256
+ expect(transactions.length).toBe(1);
257
+ expect(transactions[0].payee).toBe(payeeId);
258
+ });
259
+
260
+ const testMapped = version => {
261
+ test(`reconcile matches unmapped and mapped payees (${version})`, async () => {
262
+ const { id: acctId } = await prepareDatabase();
263
+
264
+ if (version === 'v1') {
265
+ // This is quite complicated, but important to test. If a payee is
266
+ // merged with another, a rule sets the payee of a transaction to
267
+ // the updated one, make sure it still matches an existing
268
+ // transaction that points to the old merged payee
269
+ } else if (version === 'v2') {
270
+ // This is similar to v1, but inverted: make sure that
271
+ // if a rule sets the payee to an *old* payee, that it still
272
+ // matches to a transaction with the new payee that it was merged
273
+ // to
274
+ }
275
+
276
+ const payeeId1 = await db.insertPayee({ name: 'bakkerij2' });
277
+ const payeeId2 = await db.insertPayee({ name: 'bakkerij-renamed' });
278
+
279
+ // Insert a rule *before* payees are merged. Not that v2 would
280
+ // fail if we inserted this rule after, because the rule would
281
+ // set to an *old* payee but the matching would take place on a
282
+ // *new* payee. But that's ok - it would fallback to matching
283
+ // amount anyway, so while it loses some fidelity, it's an edge
284
+ // case that we don't need to worry much about because the user
285
+ // shouldn't be able able to create rules for a merged payee.
286
+ // Unless they sync in a rule...
287
+ await insertRule({
288
+ stage: null,
289
+ conditionsOp: 'and',
290
+ conditions: [{ op: 'is', field: 'imported_payee', value: 'Bakkerij' }],
291
+ actions: [{ op: 'set', field: 'payee', value: payeeId2 }],
292
+ });
293
+
294
+ if (version === 'v1') {
295
+ await db.mergePayees(payeeId2, [payeeId1]);
296
+ } else if (version === 'v2') {
297
+ await db.mergePayees(payeeId1, [payeeId2]);
298
+ }
299
+
300
+ await db.insertTransaction({
301
+ id: 'one',
302
+ account: acctId,
303
+ amount: -2947,
304
+ date: '2017-10-15',
305
+ payee: payeeId1,
306
+ });
307
+ // It will try to match to this one first, make sure it matches
308
+ // the above transaction though
309
+ await db.insertTransaction({
310
+ id: 'two',
311
+ account: acctId,
312
+ amount: -2947,
313
+ date: '2017-10-17',
314
+ payee: null,
315
+ });
316
+
317
+ const { updated } = await reconcileTransactions(acctId, [
318
+ {
319
+ date: '2017-10-17',
320
+ payee_name: 'bakkerij',
321
+ amount: -2947,
322
+ imported_id: 'imported1',
323
+ },
324
+ ]);
325
+
326
+ const payees = await getAllPayees();
327
+ expect(payees.length).toBe(1);
328
+ expect(payees[0].id).toBe(version === 'v1' ? payeeId2 : payeeId1);
329
+
330
+ expect(updated.length).toBe(1);
331
+ expect(updated[0]).toBe('one');
332
+
333
+ const transactions = await getAllTransactions();
334
+ expect(transactions.length).toBe(2);
335
+ expect(transactions.find(t => t.id === 'one').imported_id).toBe(
336
+ 'imported1',
337
+ );
338
+ });
339
+ };
340
+
341
+ testMapped('v1');
342
+ testMapped('v2');
343
+
344
+ test('addTransactions simply adds transactions', async () => {
345
+ const { id: acctId } = await prepareDatabase();
346
+
347
+ const payeeId = await db.insertPayee({ name: 'bakkerij-renamed' });
348
+
349
+ // Make sure it still runs rules
350
+ await insertRule({
351
+ stage: null,
352
+ conditionsOp: 'and',
353
+ conditions: [{ op: 'is', field: 'imported_payee', value: 'Bakkerij' }],
354
+ actions: [{ op: 'set', field: 'payee', value: payeeId }],
355
+ });
356
+
357
+ const transactions = [
358
+ {
359
+ date: '2017-10-17',
360
+ payee_name: 'BAKKerij',
361
+ amount: -2947,
362
+ },
363
+ {
364
+ date: '2017-10-18',
365
+ payee_name: 'bakkERIj2',
366
+ amount: -2947,
367
+ },
368
+ {
369
+ date: '2017-10-19',
370
+ payee_name: 'bakkerij3',
371
+ amount: -2947,
372
+ },
373
+ {
374
+ date: '2017-10-20',
375
+ payee_name: 'BakkeriJ3',
376
+ amount: -2947,
377
+ },
378
+ ];
379
+
380
+ const added = await addTransactions(acctId, transactions);
381
+ expect(added.length).toBe(transactions.length);
382
+
383
+ const payees = await getAllPayees();
384
+ expect(payees.length).toBe(3);
385
+
386
+ const getName = id => payees.find(p => p.id === id).name;
387
+
388
+ const allTransactions = await getAllTransactions();
389
+ expect(allTransactions.length).toBe(4);
390
+ expect(allTransactions.map(t => getName(t.payee))).toEqual([
391
+ 'bakkerij3',
392
+ 'bakkerij3',
393
+ 'bakkERIj2',
394
+ 'bakkerij-renamed',
395
+ ]);
396
+ });
397
+
398
+ test("reconcile does not merge transactions with different 'imported_id' values", async () => {
399
+ const { id } = await prepareDatabase();
400
+
401
+ let payees = await getAllPayees();
402
+ expect(payees.length).toBe(0);
403
+
404
+ // Add first transaction
405
+ await reconcileTransactions(id, [
406
+ {
407
+ date: '2024-04-05',
408
+ amount: -1239,
409
+ imported_payee: 'Acme Inc.',
410
+ payee_name: 'Acme Inc.',
411
+ imported_id: 'b85cdd57-5a1c-4ca5-bd54-12e5b56fa02c',
412
+ notes: 'TEST TRANSACTION',
413
+ cleared: true,
414
+ },
415
+ ]);
416
+
417
+ payees = await getAllPayees();
418
+ expect(payees.length).toBe(1);
419
+
420
+ let transactions = await getAllTransactions();
421
+ expect(transactions.length).toBe(1);
422
+
423
+ // Add second transaction
424
+ await reconcileTransactions(id, [
425
+ {
426
+ date: '2024-04-06',
427
+ amount: -1239,
428
+ imported_payee: 'Acme Inc.',
429
+ payee_name: 'Acme Inc.',
430
+ imported_id: 'ca1589b2-7bc3-4587-a157-476170b383a7',
431
+ notes: 'TEST TRANSACTION',
432
+ cleared: true,
433
+ },
434
+ ]);
435
+
436
+ payees = await getAllPayees();
437
+ expect(payees.length).toBe(1);
438
+
439
+ transactions = await getAllTransactions();
440
+ expect(transactions.length).toBe(2);
441
+
442
+ expect(
443
+ transactions.find(
444
+ t => t.imported_id === 'b85cdd57-5a1c-4ca5-bd54-12e5b56fa02c',
445
+ ).amount,
446
+ ).toBe(-1239);
447
+ expect(
448
+ transactions.find(
449
+ t => t.imported_id === 'ca1589b2-7bc3-4587-a157-476170b383a7',
450
+ ).amount,
451
+ ).toBe(-1239);
452
+ });
453
+
454
+ test(
455
+ 'given an imported tx with no imported_id, ' +
456
+ 'when using fuzzy search V2, existing transaction has an imported_id, matches amount, and is within 7 days of imported tx, ' +
457
+ 'then imported tx should reconcile with existing transaction from fuzzy match',
458
+ async () => {
459
+ const { id } = await prepareDatabase();
460
+
461
+ let payees = await getAllPayees();
462
+ expect(payees.length).toBe(0);
463
+
464
+ const existingTx = {
465
+ date: '2024-04-05',
466
+ amount: -1239,
467
+ imported_payee: 'Acme Inc.',
468
+ payee_name: 'Acme Inc.',
469
+ imported_id: 'b85cdd57-5a1c-4ca5-bd54-12e5b56fa02c',
470
+ notes: 'TEST TRANSACTION',
471
+ cleared: true,
472
+ };
473
+
474
+ // Add transaction to represent existing transaction with imoprted_id
475
+ await reconcileTransactions(id, [existingTx]);
476
+
477
+ payees = await getAllPayees();
478
+ expect(payees.length).toBe(1);
479
+
480
+ let transactions = await getAllTransactions();
481
+ expect(transactions.length).toBe(1);
482
+
483
+ // Import transaction similar to existing but with different date and no imported_id
484
+ await reconcileTransactions(id, [
485
+ {
486
+ ...existingTx,
487
+ date: '2024-04-06',
488
+ imported_id: null,
489
+ },
490
+ ]);
491
+
492
+ payees = await getAllPayees();
493
+ expect(payees.length).toBe(1);
494
+
495
+ transactions = await getAllTransactions();
496
+ expect(transactions.length).toBe(1);
497
+
498
+ expect(transactions[0].amount).toBe(-1239);
499
+ },
500
+ );
501
+
502
+ test(
503
+ 'given an imported tx has an imported_id, ' +
504
+ 'when not using fuzzy search V2, existing transaction has an imported_id, matches amount, and is within 7 days of imported tx, ' +
505
+ 'then imported tx should reconcile with existing transaction from fuzzy match',
506
+ async () => {
507
+ const { id } = await prepareDatabase();
508
+
509
+ let payees = await getAllPayees();
510
+ expect(payees.length).toBe(0);
511
+
512
+ const existingTx = {
513
+ date: '2024-04-05',
514
+ amount: -1239,
515
+ imported_payee: 'Acme Inc.',
516
+ payee_name: 'Acme Inc.',
517
+ imported_id: 'b85cdd57-5a1c-4ca5-bd54-12e5b56fa02c',
518
+ notes: 'TEST TRANSACTION',
519
+ cleared: true,
520
+ };
521
+
522
+ // Add transaction to represent existing transaction with imoprted_id
523
+ await reconcileTransactions(id, [existingTx]);
524
+
525
+ payees = await getAllPayees();
526
+ expect(payees.length).toBe(1);
527
+
528
+ let transactions = await getAllTransactions();
529
+ expect(transactions.length).toBe(1);
530
+
531
+ // Import transaction similar to existing but with different date and imported_id
532
+ await reconcileTransactions(
533
+ id,
534
+ [
535
+ {
536
+ ...existingTx,
537
+ date: '2024-04-06',
538
+ imported_id: 'something-else-entirely',
539
+ },
540
+ ],
541
+ false,
542
+ false,
543
+ );
544
+
545
+ payees = await getAllPayees();
546
+ expect(payees.length).toBe(1);
547
+
548
+ transactions = await getAllTransactions();
549
+ expect(transactions.length).toBe(1);
550
+
551
+ expect(transactions[0].amount).toBe(-1239);
552
+ },
553
+ );
554
+ });
555
+
556
+ describe('SimpleFin batch sync', () => {
557
+ function mockSimpleFinTransactions(response) {
558
+ vi.mocked(asyncStorage.getItem).mockResolvedValue('test-token');
559
+ handlers['/simplefin/transactions'] = () => response;
560
+ }
561
+
562
+ afterEach(() => {
563
+ delete handlers['/simplefin/transactions'];
564
+ });
565
+
566
+ test('returns ACCOUNT_MISSING error when an account is not in the response', async () => {
567
+ const presentAccountId = 'sf-account-1';
568
+ const missingAccountId = 'sf-account-2';
569
+
570
+ // Mock SimpleFin response that only returns data for one of two accounts
571
+ mockSimpleFinTransactions({
572
+ [presentAccountId]: {
573
+ transactions: { all: [], booked: [], pending: [] },
574
+ balances: [],
575
+ startingBalance: 0,
576
+ },
577
+ errors: {},
578
+ });
579
+
580
+ // Insert two accounts linked to SimpleFin
581
+ const acct1Id = await db.insertAccount({
582
+ id: 'acct-1',
583
+ account_id: presentAccountId,
584
+ name: 'Account 1',
585
+ account_sync_source: 'simpleFin',
586
+ });
587
+ await db.insertPayee({
588
+ id: 'transfer-' + acct1Id,
589
+ name: '',
590
+ transfer_acct: acct1Id,
591
+ });
592
+
593
+ const acct2Id = await db.insertAccount({
594
+ id: 'acct-2',
595
+ account_id: missingAccountId,
596
+ name: 'Account 2',
597
+ account_sync_source: 'simpleFin',
598
+ });
599
+ await db.insertPayee({
600
+ id: 'transfer-' + acct2Id,
601
+ name: '',
602
+ transfer_acct: acct2Id,
603
+ });
604
+
605
+ const results = await simpleFinBatchSync([
606
+ { id: 'acct-1', account_id: presentAccountId },
607
+ { id: 'acct-2', account_id: missingAccountId },
608
+ ]);
609
+
610
+ // The present account should succeed (no error_code)
611
+ const presentResult = results.find(r => r.accountId === 'acct-1');
612
+ expect(presentResult).toBeDefined();
613
+ expect(presentResult.res.error_code).toBeUndefined();
614
+
615
+ // The missing account should have ACCOUNT_MISSING error
616
+ const missingResult = results.find(r => r.accountId === 'acct-2');
617
+ expect(missingResult).toBeDefined();
618
+ expect(missingResult.res.error_code).toBe('ACCOUNT_MISSING');
619
+ expect(missingResult.res.error_type).toBe('ACCOUNT_MISSING');
620
+ });
621
+
622
+ test('propagates ACCOUNT_MISSING error from SimpleFin response errors', async () => {
623
+ const presentAccountId = 'sf-account-1';
624
+ const missingAccountId = 'sf-account-2';
625
+
626
+ // Mock SimpleFin response with error entry for missing account
627
+ mockSimpleFinTransactions({
628
+ [presentAccountId]: {
629
+ transactions: { all: [], booked: [], pending: [] },
630
+ balances: [],
631
+ startingBalance: 0,
632
+ },
633
+ errors: {
634
+ [missingAccountId]: [
635
+ {
636
+ error_type: 'ACCOUNT_MISSING',
637
+ error_code: 'ACCOUNT_MISSING',
638
+ reason: 'Account not found',
639
+ },
640
+ ],
641
+ },
642
+ });
643
+
644
+ const acct1Id = await db.insertAccount({
645
+ id: 'acct-1',
646
+ account_id: presentAccountId,
647
+ name: 'Account 1',
648
+ account_sync_source: 'simpleFin',
649
+ });
650
+ await db.insertPayee({
651
+ id: 'transfer-' + acct1Id,
652
+ name: '',
653
+ transfer_acct: acct1Id,
654
+ });
655
+
656
+ const acct2Id = await db.insertAccount({
657
+ id: 'acct-2',
658
+ account_id: missingAccountId,
659
+ name: 'Account 2',
660
+ account_sync_source: 'simpleFin',
661
+ });
662
+ await db.insertPayee({
663
+ id: 'transfer-' + acct2Id,
664
+ name: '',
665
+ transfer_acct: acct2Id,
666
+ });
667
+
668
+ const results = await simpleFinBatchSync([
669
+ { id: 'acct-1', account_id: presentAccountId },
670
+ { id: 'acct-2', account_id: missingAccountId },
671
+ ]);
672
+
673
+ // The missing account should get the ACCOUNT_MISSING error from the errors map
674
+ const missingResult = results.find(r => r.accountId === 'acct-2');
675
+ expect(missingResult).toBeDefined();
676
+ expect(missingResult.res.error_code).toBe('ACCOUNT_MISSING');
677
+ expect(missingResult.res.error_type).toBe('ACCOUNT_MISSING');
678
+ });
679
+ });