@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,110 @@
1
+ // @ts-strict-ignore
2
+ type Division = {
3
+ category?: string;
4
+ subcategory?: string;
5
+ description?: string;
6
+ amount?: number;
7
+ };
8
+
9
+ type QIFTransaction = {
10
+ date?: string;
11
+ amount?: string;
12
+ number?: string;
13
+ memo?: string;
14
+ address?: string[];
15
+ clearedStatus?: string;
16
+ category?: string;
17
+ subcategory?: string;
18
+ payee?: string;
19
+ division?: Division[];
20
+ };
21
+
22
+ export function qif2json(qif, options: { dateFormat?: string } = {}) {
23
+ const lines = qif.split('\n').filter(Boolean);
24
+ let line = lines.shift();
25
+ const type = /!Type:([^$]*)$/.exec(line.trim());
26
+ const data: {
27
+ dateFormat: string | undefined;
28
+ type?;
29
+ transactions: QIFTransaction[];
30
+ } = {
31
+ dateFormat: options.dateFormat,
32
+ transactions: [],
33
+ };
34
+ const transactions = data.transactions;
35
+ let transaction: QIFTransaction = {};
36
+
37
+ if (!type || !type.length) {
38
+ throw new Error('File does not appear to be a valid qif file: ' + line);
39
+ }
40
+ data.type = type[1];
41
+
42
+ let division: Division = {};
43
+
44
+ while ((line = lines.shift())) {
45
+ line = line.trim();
46
+ if (line === '^') {
47
+ transactions.push(transaction);
48
+ transaction = {};
49
+ continue;
50
+ }
51
+ switch (line[0]) {
52
+ case 'D':
53
+ transaction.date = line.substring(1);
54
+ break;
55
+ case 'T':
56
+ transaction.amount = line.substring(1);
57
+ break;
58
+ case 'N':
59
+ transaction.number = line.substring(1);
60
+ break;
61
+ case 'M':
62
+ transaction.memo = line.substring(1);
63
+ break;
64
+ case 'A':
65
+ transaction.address = (transaction.address || []).concat(
66
+ line.substring(1),
67
+ );
68
+ break;
69
+ case 'P':
70
+ transaction.payee = line.substring(1).replace(/&/g, '&');
71
+ break;
72
+ case 'L':
73
+ const lArray = line.substring(1).split(':');
74
+ transaction.category = lArray[0];
75
+ if (lArray[1] !== undefined) {
76
+ transaction.subcategory = lArray[1];
77
+ }
78
+ break;
79
+ case 'C':
80
+ transaction.clearedStatus = line.substring(1);
81
+ break;
82
+ case 'S':
83
+ const sArray = line.substring(1).split(':');
84
+ division.category = sArray[0];
85
+ if (sArray[1] !== undefined) {
86
+ division.subcategory = sArray[1];
87
+ }
88
+ break;
89
+ case 'E':
90
+ division.description = line.substring(1);
91
+ break;
92
+ case '$':
93
+ division.amount = parseFloat(line.substring(1));
94
+ if (!(transaction.division instanceof Array)) {
95
+ transaction.division = [];
96
+ }
97
+ transaction.division.push(division);
98
+ division = {};
99
+ break;
100
+
101
+ default:
102
+ throw new Error('Unknown Detail Code: ' + line[0]);
103
+ }
104
+ }
105
+
106
+ if (Object.keys(transaction).length) {
107
+ transactions.push(transaction);
108
+ }
109
+ return data;
110
+ }
@@ -0,0 +1,168 @@
1
+ // @ts-strict-ignore
2
+ import { parseStringPromise } from 'xml2js';
3
+
4
+ type DateRef = { DtTm: string } | { Dt: string };
5
+ type Amt = { _: string };
6
+
7
+ type Ntry = {
8
+ AcctSvcrRef?: string;
9
+ Amt?: Amt;
10
+ CdtDbtInd: 'CRDT' | 'DBIT';
11
+ ValDt?: DateRef;
12
+ BookgDt?: DateRef;
13
+ NtryDtls?: NtryDtls;
14
+ AddtlNtryInf?: string;
15
+ NtryRef?: string;
16
+ };
17
+
18
+ type NtryDtls = {
19
+ TxDtls: TxDtls | TxDtls[];
20
+ };
21
+
22
+ type TxDtls = {
23
+ RltdPties?: {
24
+ Cdtr: {
25
+ Nm: string;
26
+ };
27
+ Dbtr: {
28
+ Nm: string;
29
+ };
30
+ };
31
+ RmtInf?: {
32
+ Ustrd: string | string[];
33
+ };
34
+ };
35
+
36
+ type TransactionCAMT = {
37
+ amount: number;
38
+ date: string;
39
+ payee_name: string | null;
40
+ imported_payee: string | null;
41
+ notes: string | null;
42
+ imported_id?: string;
43
+ };
44
+
45
+ function findKeys(obj: object, key: string): unknown[] {
46
+ let result = [];
47
+ for (const i in obj) {
48
+ if (!obj.hasOwnProperty(i)) continue;
49
+ if (i === key) {
50
+ if (Array.isArray(obj[i])) {
51
+ result = result.concat(obj[i]);
52
+ } else {
53
+ result.push(obj[i]);
54
+ }
55
+ }
56
+ if (typeof obj[i] === 'object') {
57
+ result = result.concat(findKeys(obj[i], key));
58
+ }
59
+ }
60
+ return result;
61
+ }
62
+
63
+ function getPayeeNameFromTxDtls(
64
+ TxDtls: TxDtls,
65
+ isDebit: boolean,
66
+ ): string | null {
67
+ if (TxDtls?.RltdPties) {
68
+ const key = isDebit ? TxDtls.RltdPties.Cdtr : TxDtls.RltdPties.Dbtr;
69
+ const Nm = findKeys(key, 'Nm');
70
+ return Nm.length > 0 ? (Nm[0] as string) : null;
71
+ }
72
+ return null;
73
+ }
74
+
75
+ function getNotesFromTxDtls(TxDtls: TxDtls): string | null {
76
+ if (TxDtls?.RmtInf) {
77
+ const Ustrd = TxDtls.RmtInf.Ustrd;
78
+ return Array.isArray(Ustrd) ? Ustrd.join(' ') : Ustrd;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ function convertToNumberOrNull(value: string): number | null {
84
+ const number = Number(value);
85
+ return isNaN(number) ? null : number;
86
+ }
87
+
88
+ function getDtOrDtTm(Date: DateRef | null): string | null {
89
+ if (!Date) {
90
+ return null;
91
+ }
92
+ if ('DtTm' in Date) {
93
+ return Date.DtTm.slice(0, 10);
94
+ }
95
+ return Date?.Dt;
96
+ }
97
+
98
+ export async function xmlCAMT2json(
99
+ content: string,
100
+ ): Promise<TransactionCAMT[]> {
101
+ const data = await parseStringPromise(content, { explicitArray: false });
102
+ const entries = findKeys(data, 'Ntry') as Ntry[];
103
+
104
+ const transactions: TransactionCAMT[] = [];
105
+
106
+ for (const entry of entries) {
107
+ /*
108
+ For (camt.052/054) could filter on entry.Sts= BOOK or PDNG, currently importing all entries
109
+ */
110
+
111
+ const id = entry.AcctSvcrRef;
112
+
113
+ const amount = convertToNumberOrNull(entry.Amt?._);
114
+ const isDebit = entry.CdtDbtInd === 'DBIT';
115
+
116
+ const date = getDtOrDtTm(entry.ValDt) || getDtOrDtTm(entry.BookgDt);
117
+
118
+ if (Array.isArray(entry.NtryDtls?.TxDtls)) {
119
+ // we add subtransactions as normal transactions as importing split with subtransactions is not supported
120
+ // amount, and payee_name are not processed correctly for subtransaction.
121
+ entry.NtryDtls.TxDtls.forEach((TxDtls: TxDtls) => {
122
+ const subPayee = getPayeeNameFromTxDtls(TxDtls, isDebit);
123
+ const subNotes = getNotesFromTxDtls(TxDtls);
124
+ const Amt = findKeys(TxDtls, 'Amt') as Amt[];
125
+ const amount = Amt.length > 0 ? convertToNumberOrNull(Amt[0]._) : null;
126
+ transactions.push({
127
+ amount: isDebit ? -amount : amount,
128
+ date,
129
+ payee_name: subPayee,
130
+ imported_payee: subPayee,
131
+ notes: subNotes,
132
+ });
133
+ });
134
+ } else {
135
+ let payee_name: string | null;
136
+ let notes: string | null;
137
+ payee_name = getPayeeNameFromTxDtls(entry.NtryDtls?.TxDtls, isDebit);
138
+ if (!payee_name && entry.AddtlNtryInf) {
139
+ payee_name = entry.AddtlNtryInf;
140
+ }
141
+ notes = getNotesFromTxDtls(entry.NtryDtls?.TxDtls);
142
+ if (!notes && entry.AddtlNtryInf && entry.AddtlNtryInf !== payee_name) {
143
+ notes = entry.AddtlNtryInf;
144
+ }
145
+ if (!payee_name && !notes && entry.NtryRef) {
146
+ notes = entry.NtryRef;
147
+ }
148
+ if (payee_name && notes && payee_name.includes(notes)) {
149
+ notes = null;
150
+ }
151
+
152
+ const transaction: TransactionCAMT = {
153
+ amount: isDebit ? -amount : amount,
154
+ date,
155
+ payee_name,
156
+ imported_payee: payee_name,
157
+ notes,
158
+ };
159
+ if (id) {
160
+ transaction.imported_id = id;
161
+ }
162
+ transactions.push(transaction);
163
+ }
164
+ }
165
+ return transactions.filter(
166
+ trans => trans.date != null && trans.amount != null,
167
+ );
168
+ }
@@ -0,0 +1,196 @@
1
+ // @ts-strict-ignore
2
+
3
+ import * as connection from '../../platform/server/connection';
4
+ import type { Diff } from '../../shared/util';
5
+ import type { PayeeEntity, TransactionEntity } from '../../types/models';
6
+ import * as db from '../db';
7
+ import { incrFetch, whereIn } from '../db/util';
8
+ import { batchMessages } from '../sync';
9
+
10
+ import * as rules from './transaction-rules';
11
+ import * as transfer from './transfer';
12
+
13
+ async function idsWithChildren(ids: string[]) {
14
+ const whereIds = whereIn(ids, 'parent_id');
15
+ const rows = await db.all<Pick<db.DbViewTransactionInternal, 'id'>>(
16
+ `SELECT id FROM v_transactions_internal WHERE ${whereIds}`,
17
+ );
18
+ const set = new Set(ids);
19
+ for (const row of rows) {
20
+ set.add(row.id);
21
+ }
22
+ return [...set];
23
+ }
24
+
25
+ async function getTransactionsByIds(
26
+ ids: string[],
27
+ ): Promise<TransactionEntity[]> {
28
+ // TODO: convert to whereIn
29
+ //
30
+ // or better yet, use ActualQL
31
+ return incrFetch(
32
+ (query, params) => db.selectWithSchema('transactions', query, params),
33
+ ids,
34
+
35
+ id => `id = '${id}'`,
36
+ where => `SELECT * FROM v_transactions_internal WHERE ${where}`,
37
+ );
38
+ }
39
+
40
+ export async function batchUpdateTransactions({
41
+ added,
42
+ deleted,
43
+ updated,
44
+ learnCategories = false,
45
+ detectOrphanPayees = true,
46
+ runTransfers = true,
47
+ }: Partial<Diff<TransactionEntity>> & {
48
+ learnCategories?: boolean;
49
+ detectOrphanPayees?: boolean;
50
+ runTransfers?: boolean;
51
+ }) {
52
+ // Track the ids of each type of transaction change (see below for why)
53
+ let addedIds = [];
54
+ const updatedIds = updated ? updated.map(u => u.id) : [];
55
+ const deletedIds = deleted
56
+ ? await idsWithChildren(deleted.map(d => d.id))
57
+ : [];
58
+
59
+ const oldPayees = new Set<PayeeEntity['id']>();
60
+ const accounts = await db.all<db.DbAccount>(
61
+ 'SELECT * FROM accounts WHERE tombstone = 0',
62
+ );
63
+
64
+ // We need to get all the payees of updated transactions _before_
65
+ // making changes
66
+ if (updated) {
67
+ const descUpdatedIds = updated
68
+ .filter(update => update.payee)
69
+ .map(update => update.id);
70
+
71
+ const transactions = await getTransactionsByIds(descUpdatedIds);
72
+
73
+ for (let i = 0; i < transactions.length; i++) {
74
+ oldPayees.add(transactions[i].payee);
75
+ }
76
+ }
77
+
78
+ // Apply all the updates. We can batch this now! This is important
79
+ // and makes bulk updates much faster
80
+ await batchMessages(async () => {
81
+ if (added) {
82
+ addedIds = await Promise.all(
83
+ added.map(async t => {
84
+ // Offbudget account transactions and parent transactions should not have categories.
85
+ const account = accounts.find(acct => acct.id === t.account);
86
+ if (t.is_parent || account?.offbudget === 1) {
87
+ t.category = null;
88
+ }
89
+ return db.insertTransaction(t);
90
+ }),
91
+ );
92
+ }
93
+
94
+ if (deleted) {
95
+ await Promise.all(
96
+ // It's important to use `deletedIds` and not `deleted` here
97
+ // because we've expanded it to include children above. The
98
+ // inconsistency of the delete APIs is annoying and should
99
+ // be fixed (it should only take an id)
100
+ deletedIds.map(async id => {
101
+ await db.deleteTransaction({ id });
102
+ }),
103
+ );
104
+ }
105
+
106
+ if (updated) {
107
+ await Promise.all(
108
+ updated.map(async t => {
109
+ if (t.account) {
110
+ // Moving transactions off budget should always clear the
111
+ // category. Parent transactions should not have categories.
112
+ const account = accounts.find(acct => acct.id === t.account);
113
+ if (t.is_parent || account?.offbudget === 1) {
114
+ t.category = null;
115
+ }
116
+ }
117
+
118
+ await db.updateTransaction(t);
119
+ }),
120
+ );
121
+ }
122
+ });
123
+
124
+ // Get all of the full transactions that were changed. This is
125
+ // needed to run any cascading logic that depends on the full
126
+ // transaction. Things like transfers, analyzing rule updates, and
127
+ // more
128
+ const allAdded = await getTransactionsByIds(addedIds);
129
+ const allUpdated = await getTransactionsByIds(updatedIds);
130
+ const allDeleted = await getTransactionsByIds(deletedIds);
131
+
132
+ // Post-processing phase: first do any updates to transfers.
133
+ // Transfers update the transactions and we need to return updates
134
+ // to the client so that can apply them. Note that added
135
+ // transactions just return the full transaction.
136
+ const resultAdded = allAdded;
137
+ const resultUpdated = allUpdated;
138
+ let transfersUpdated: Awaited<ReturnType<typeof transfer.onUpdate>>[];
139
+
140
+ if (runTransfers) {
141
+ await batchMessages(async () => {
142
+ await Promise.all(allAdded.map(t => transfer.onInsert(t)));
143
+
144
+ // Return any updates from here
145
+ transfersUpdated = (
146
+ await Promise.all(allUpdated.map(t => transfer.onUpdate(t)))
147
+ ).filter(Boolean);
148
+
149
+ await Promise.all(allDeleted.map(t => transfer.onDelete(t)));
150
+ });
151
+ }
152
+
153
+ if (learnCategories) {
154
+ // Analyze any updated categories and update rules to learn from
155
+ // the user's activity
156
+ const ids = new Set([
157
+ ...(added ? added.filter(add => add.category).map(add => add.id) : []),
158
+ ...(updated
159
+ ? updated.filter(update => update.category).map(update => update.id)
160
+ : []),
161
+ ]);
162
+ await rules.updateCategoryRules(
163
+ allAdded.concat(allUpdated).filter(trans => ids.has(trans.id)),
164
+ );
165
+ }
166
+
167
+ if (detectOrphanPayees) {
168
+ // Look for any orphaned payees and notify the user about merging
169
+ // them
170
+
171
+ if (updated) {
172
+ const newPayeeIds = updated.map(u => u.payee).filter(Boolean);
173
+ if (newPayeeIds.length > 0) {
174
+ const allOrphaned = new Set(await db.getOrphanedPayees());
175
+
176
+ const orphanedIds = [...oldPayees].filter(id => allOrphaned.has(id));
177
+
178
+ if (orphanedIds.length > 0) {
179
+ connection.send('orphaned-payees', {
180
+ orphanedIds,
181
+ updatedPayeeIds: newPayeeIds,
182
+ });
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ return {
189
+ added: resultAdded,
190
+ updated: runTransfers ? transfersUpdated : resultUpdated,
191
+ deleted: allDeleted,
192
+ errors: ((added || []) as Partial<TransactionEntity>[])
193
+ .concat(updated || [])
194
+ .flatMap(t => t._ruleErrors || []),
195
+ };
196
+ }