@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,33 @@
1
+ import { html2Plain } from './ofx2json';
2
+
3
+ describe('html2Plain', () => {
4
+ test('regular text works', async () => {
5
+ expect(html2Plain('Hello, world!')).toBe('Hello, world!');
6
+ expect(html2Plain('Hello, <b>world</b>!')).toBe('Hello, <b>world</b>!');
7
+ });
8
+
9
+ test('brackets are unescaped', async () => {
10
+ expect(html2Plain('Hello, &lt;world&gt;!')).toBe('Hello, <world>!');
11
+ });
12
+ test('apostrophes are unescaped', async () => {
13
+ expect(html2Plain('Hello, &#39;world&#39;!')).toBe("Hello, 'world'!");
14
+ });
15
+ test('quotes are unescaped', async () => {
16
+ expect(html2Plain('Hello, &quot;world&quot;!')).toBe('Hello, "world"!');
17
+ });
18
+ test('ampersands are unescaped', async () => {
19
+ expect(html2Plain('Hello, &amp;world&amp;!')).toBe('Hello, &world&!');
20
+ expect(html2Plain('Hello, &#038;world&#038;!')).toBe('Hello, &world&!');
21
+ });
22
+ test('no double unescaping with other entities', async () => {
23
+ expect(html2Plain('Hello, &amp;#038;world&amp;#038;!')).toBe(
24
+ 'Hello, &#038;world&#038;!',
25
+ );
26
+ expect(html2Plain('Hello, &#038;amp;world&#038;amp;!')).toBe(
27
+ 'Hello, &amp;world&amp;!',
28
+ );
29
+ expect(html2Plain('Hello, &amp;quot;world&amp;quot;!')).toBe(
30
+ 'Hello, &quot;world&quot;!',
31
+ );
32
+ });
33
+ });
@@ -0,0 +1,157 @@
1
+ // @ts-strict-ignore
2
+ import { parseStringPromise } from 'xml2js';
3
+
4
+ import { dayFromDate } from '../../../shared/months';
5
+
6
+ type OFXTransaction = {
7
+ amount: string;
8
+ fitId: string;
9
+ name: string;
10
+ date: string;
11
+ memo: string;
12
+ type: string;
13
+ };
14
+
15
+ type OFXParseResult = {
16
+ headers: Record<string, unknown>;
17
+ transactions: OFXTransaction[];
18
+ };
19
+
20
+ function sgml2Xml(sgml) {
21
+ return sgml
22
+ .replace(/&/g, '&#038;') // Replace ampersands
23
+ .replace(/&amp;/g, '&#038;')
24
+ .replace(/>\s+</g, '><') // remove whitespace inbetween tag close/open
25
+ .replace(/\s+</g, '<') // remove whitespace before a close tag
26
+ .replace(/>\s+/g, '>') // remove whitespace after a close tag
27
+ .replace(/\.(?=[^<>]*>)/g, '') // Remove dots in tag names
28
+ .replace(/<(\w+?)>([^<]+)/g, '<$1>$2</<added>$1>') // Add a new end-tags for the ofx elements
29
+ .replace(/<\/<added>(\w+?)>(<\/\1>)?/g, '</$1>'); // Remove duplicate end-tags
30
+ }
31
+
32
+ export function html2Plain(value) {
33
+ return value
34
+ ?.replace(/&lt;/g, '<') // lessthan
35
+ .replace(/&gt;/g, '>') // greaterthan
36
+ .replace(/&#39;/g, "'")
37
+ .replace(/&quot;/g, '"')
38
+ .replace(/(&amp;|&#038;)/g, '&'); // ampersands
39
+ }
40
+
41
+ async function parseXml(content) {
42
+ return await parseStringPromise(content, {
43
+ explicitArray: false,
44
+ trim: true,
45
+ });
46
+ }
47
+
48
+ function getStmtTrn(data) {
49
+ const ofx = data?.['OFX'];
50
+ if (ofx?.['CREDITCARDMSGSRSV1'] != null) {
51
+ return getCcStmtTrn(ofx);
52
+ } else if (ofx?.['INVSTMTMSGSRSV1'] != null) {
53
+ return getInvStmtTrn(ofx);
54
+ } else {
55
+ return getBankStmtTrn(ofx);
56
+ }
57
+ }
58
+
59
+ function getBankStmtTrn(ofx) {
60
+ // Somes values could be an array or a single object.
61
+ // xml2js serializes single item to an object and multiple to an array.
62
+ const msg = ofx?.['BANKMSGSRSV1'];
63
+ const stmtTrnRs = getAsArray(msg?.['STMTTRNRS']);
64
+ const result = stmtTrnRs.flatMap(s => {
65
+ const stmtRs = s?.['STMTRS'];
66
+ const tranList = stmtRs?.['BANKTRANLIST'];
67
+ const stmtTrn = tranList?.['STMTTRN'];
68
+ return getAsArray(stmtTrn);
69
+ });
70
+ return result;
71
+ }
72
+
73
+ function getCcStmtTrn(ofx) {
74
+ // Some values could be an array or a single object.
75
+ // xml2js serializes single item to an object and multiple to an array.
76
+ const msg = ofx?.['CREDITCARDMSGSRSV1'];
77
+ const stmtTrnRs = getAsArray(msg?.['CCSTMTTRNRS']);
78
+ const result = stmtTrnRs.flatMap(s => {
79
+ const stmtRs = s?.['CCSTMTRS'];
80
+ const tranList = stmtRs?.['BANKTRANLIST'];
81
+ const stmtTrn = tranList?.['STMTTRN'];
82
+ return getAsArray(stmtTrn);
83
+ });
84
+ return result;
85
+ }
86
+
87
+ function getInvStmtTrn(ofx) {
88
+ // Somes values could be an array or a single object.
89
+ // xml2js serializes single item to an object and multiple to an array.
90
+ const msg = ofx?.['INVSTMTMSGSRSV1'];
91
+ const stmtTrnRs = getAsArray(msg?.['INVSTMTTRNRS']);
92
+ const result = stmtTrnRs.flatMap(s => {
93
+ const stmtRs = s?.['INVSTMTRS'];
94
+ const tranList = stmtRs?.['INVTRANLIST'];
95
+ const stmtTrn = tranList?.['INVBANKTRAN']?.flatMap(t => t?.['STMTTRN']);
96
+ return getAsArray(stmtTrn);
97
+ });
98
+ return result;
99
+ }
100
+
101
+ function getAsArray(value) {
102
+ return Array.isArray(value) ? value : value === undefined ? [] : [value];
103
+ }
104
+
105
+ function mapOfxTransaction(stmtTrn): OFXTransaction {
106
+ // YYYYMMDDHHMMSS format. We just need the date.
107
+ const dtPosted = stmtTrn['DTPOSTED'];
108
+ const transactionDate = dtPosted
109
+ ? new Date(
110
+ Number(dtPosted.substring(0, 4)), // year
111
+ Number(dtPosted.substring(4, 6)) - 1, // month (zero-based index)
112
+ Number(dtPosted.substring(6, 8)), // date
113
+ )
114
+ : null;
115
+
116
+ return {
117
+ amount: stmtTrn['TRNAMT'],
118
+ type: stmtTrn['TRNTYPE'],
119
+ fitId: stmtTrn['FITID'],
120
+ date: dayFromDate(transactionDate),
121
+ name: html2Plain(stmtTrn['NAME']),
122
+ memo: html2Plain(stmtTrn['MEMO']),
123
+ };
124
+ }
125
+
126
+ export async function ofx2json(ofx: string): Promise<OFXParseResult> {
127
+ // firstly, split into the header attributes and the footer sgml
128
+ const contents = ofx.split(/<OFX\s?>/, 2);
129
+
130
+ // firstly, parse the headers
131
+ const headerString = contents[0].split(/\r?\n/);
132
+ const headers = {};
133
+ headerString.forEach(attrs => {
134
+ if (attrs) {
135
+ const headAttr = attrs.split(/:/, 2);
136
+ headers[headAttr[0]] = headAttr[1];
137
+ }
138
+ });
139
+
140
+ // make the SGML and the XML
141
+ const content = `<OFX>${contents[1]}`;
142
+
143
+ // Parse the XML/SGML portion of the file into an object
144
+ // Try as XML first, and if that fails do the SGML->XML mangling
145
+ let dataParsed = null;
146
+ try {
147
+ dataParsed = await parseXml(content);
148
+ } catch {
149
+ const sanitized = sgml2Xml(content);
150
+ dataParsed = await parseXml(sanitized);
151
+ }
152
+
153
+ return {
154
+ headers,
155
+ transactions: getStmtTrn(dataParsed).map(mapOfxTransaction),
156
+ };
157
+ }
@@ -0,0 +1,224 @@
1
+ // @ts-strict-ignore
2
+ import * as d from 'date-fns';
3
+
4
+ import { amountToInteger } from '../../../shared/util';
5
+ import { reconcileTransactions } from '../../accounts/sync';
6
+ import * as db from '../../db';
7
+ import * as prefs from '../../prefs';
8
+
9
+ import { parseFile } from './parse-file';
10
+
11
+ beforeEach(global.emptyDatabase());
12
+
13
+ // libofx spits out errors that contain the entire
14
+ // source code of the file in the stack which makes
15
+ // it hard to test.
16
+ const old = console.warn;
17
+ beforeAll(() => {
18
+ console.warn = vi.fn();
19
+ });
20
+ afterAll(() => {
21
+ console.warn = old;
22
+ });
23
+
24
+ type Transaction = {
25
+ id: string;
26
+ amount: number;
27
+ date: string;
28
+ payee_name: string;
29
+ imported_payee: string;
30
+ notes: string | null;
31
+ };
32
+
33
+ async function getTransactions(accountId: string): Promise<Transaction[]> {
34
+ return db.runQuery(
35
+ 'SELECT * FROM transactions WHERE acct = ?',
36
+ [accountId],
37
+ true,
38
+ );
39
+ }
40
+
41
+ async function importFileWithRealTime(
42
+ accountId,
43
+ filepath,
44
+ dateFormat?: string,
45
+ options?: { importNotes: boolean },
46
+ ) {
47
+ // Emscripten requires a real Date.now!
48
+ global.restoreDateNow();
49
+ const { errors, transactions: originalTransactions } = await parseFile(
50
+ filepath,
51
+ options,
52
+ );
53
+ global.restoreFakeDateNow();
54
+
55
+ let transactions = originalTransactions;
56
+ if (transactions) {
57
+ // oxlint-disable-next-line typescript/no-explicit-any
58
+ transactions = (transactions as any[]).map(trans => ({
59
+ ...trans,
60
+ amount: amountToInteger(trans.amount),
61
+ date: dateFormat
62
+ ? d.format(d.parse(trans.date, dateFormat, new Date()), 'yyyy-MM-dd')
63
+ : trans.date,
64
+ }));
65
+ }
66
+ if (errors.length > 0) {
67
+ return { errors, added: [] };
68
+ }
69
+
70
+ const { added } = await reconcileTransactions(accountId, transactions);
71
+ return { errors, added };
72
+ }
73
+
74
+ describe('File import', () => {
75
+ test('qif import works', async () => {
76
+ await prefs.loadPrefs();
77
+ await db.insertAccount({ id: 'one', name: 'one' });
78
+ const { errors } = await importFileWithRealTime(
79
+ 'one',
80
+ __dirname + '/../../../mocks/files/data.qif',
81
+ 'MM/dd/yy',
82
+ { importNotes: true },
83
+ );
84
+ expect(errors.length).toBe(0);
85
+ expect(await getTransactions('one')).toMatchSnapshot();
86
+ });
87
+
88
+ test('ofx import works', async () => {
89
+ await prefs.loadPrefs();
90
+ await db.insertAccount({ id: 'one', name: 'one' });
91
+
92
+ const { errors } = await importFileWithRealTime(
93
+ 'one',
94
+ __dirname + '/../../../mocks/files/data.ofx',
95
+ null,
96
+ { importNotes: true },
97
+ );
98
+ expect(errors.length).toBe(0);
99
+ expect(await getTransactions('one')).toMatchSnapshot();
100
+ }, 45000);
101
+
102
+ test('ofx import works (credit card)', async () => {
103
+ await prefs.loadPrefs();
104
+ await db.insertAccount({ id: 'one', name: 'one' });
105
+
106
+ const { errors } = await importFileWithRealTime(
107
+ 'one',
108
+ __dirname + '/../../../mocks/files/credit-card.ofx',
109
+ null,
110
+ { importNotes: true },
111
+ );
112
+ expect(errors.length).toBe(0);
113
+ expect(await getTransactions('one')).toMatchSnapshot();
114
+ }, 45000);
115
+
116
+ test('qfx import works', async () => {
117
+ await prefs.loadPrefs();
118
+ await db.insertAccount({ id: 'one', name: 'one' });
119
+
120
+ const { errors } = await importFileWithRealTime(
121
+ 'one',
122
+ __dirname + '/../../../mocks/files/data.qfx',
123
+ null,
124
+ { importNotes: true },
125
+ );
126
+ expect(errors.length).toBe(0);
127
+ expect(await getTransactions('one')).toMatchSnapshot();
128
+ }, 45000);
129
+
130
+ test('import notes are respected when importing', async () => {
131
+ await prefs.loadPrefs();
132
+ await db.insertAccount({ id: 'one', name: 'one' });
133
+
134
+ // Test with importNotes enabled
135
+ const { errors: errorsWithNotes } = await importFileWithRealTime(
136
+ 'one',
137
+ __dirname + '/../../../mocks/files/data.ofx',
138
+ null,
139
+ { importNotes: true },
140
+ );
141
+ expect(errorsWithNotes.length).toBe(0);
142
+ expect(await getTransactions('one')).toMatchSnapshot(
143
+ 'transactions with notes',
144
+ );
145
+
146
+ // Clear transactions
147
+ db.runQuery('DELETE FROM transactions WHERE acct = ?', ['one']);
148
+
149
+ // Test with importNotes disabled
150
+ const { errors: errorsWithoutNotes } = await importFileWithRealTime(
151
+ 'one',
152
+ __dirname + '/../../../mocks/files/data.ofx',
153
+ null,
154
+ { importNotes: false },
155
+ );
156
+ expect(errorsWithoutNotes.length).toBe(0);
157
+ const transactionsWithoutNotes = await getTransactions('one');
158
+ expect(transactionsWithoutNotes.every(t => t.notes === null)).toBe(true);
159
+ }, 45000);
160
+
161
+ test('matches extensions correctly (case-insensitive, etc)', async () => {
162
+ await prefs.loadPrefs();
163
+ await db.insertAccount({ id: 'one', name: 'one' });
164
+
165
+ let res = await importFileWithRealTime(
166
+ 'one',
167
+ __dirname + '/../../../mocks/files/best.data-ever$.QFX',
168
+ );
169
+ expect(res.errors.length).toBe(0);
170
+
171
+ res = await importFileWithRealTime(
172
+ 'one',
173
+ __dirname + '/../../../mocks/files/big.data.QiF',
174
+ 'MM/dd/yy',
175
+ );
176
+ expect(res.errors.length).toBe(0);
177
+
178
+ res = await importFileWithRealTime('one', 'foo.txt');
179
+ expect(res.errors.length).toBe(1);
180
+ expect(res.errors[0].message).toBe('Invalid file type');
181
+ }, 45000);
182
+
183
+ test('handles non-ASCII characters', async () => {
184
+ await prefs.loadPrefs();
185
+ await db.insertAccount({ id: 'one', name: 'one' });
186
+
187
+ const { errors } = await importFileWithRealTime(
188
+ 'one',
189
+ __dirname + '/../../../mocks/files/8859-1.qfx',
190
+ 'yyyy-MM-dd',
191
+ { importNotes: true },
192
+ );
193
+ expect(errors.length).toBe(0);
194
+ expect(await getTransactions('one')).toMatchSnapshot();
195
+ });
196
+
197
+ test('handles html escaped plaintext', async () => {
198
+ await prefs.loadPrefs();
199
+ await db.insertAccount({ id: 'one', name: 'one' });
200
+
201
+ const { errors } = await importFileWithRealTime(
202
+ 'one',
203
+ __dirname + '/../../../mocks/files/html-vals.qfx',
204
+ 'yyyy-MM-dd',
205
+ { importNotes: true },
206
+ );
207
+ expect(errors.length).toBe(0);
208
+ expect(await getTransactions('one')).toMatchSnapshot();
209
+ });
210
+
211
+ test('CAMT.053 import works', async () => {
212
+ await prefs.loadPrefs();
213
+ await db.insertAccount({ id: 'one', name: 'one' });
214
+
215
+ const { errors } = await importFileWithRealTime(
216
+ 'one',
217
+ __dirname + '/../../../mocks/files/camt/camt.053.xml',
218
+ null,
219
+ { importNotes: true },
220
+ );
221
+ expect(errors.length).toBe(0);
222
+ expect(await getTransactions('one')).toMatchSnapshot();
223
+ });
224
+ });
@@ -0,0 +1,286 @@
1
+ // @ts-strict-ignore
2
+ import { parse as csv2json } from 'csv-parse/sync';
3
+
4
+ import * as fs from '../../../platform/server/fs';
5
+ import { logger } from '../../../platform/server/log';
6
+ import { looselyParseAmount } from '../../../shared/util';
7
+
8
+ import { ofx2json } from './ofx2json';
9
+ import { qif2json } from './qif2json';
10
+ import { xmlCAMT2json } from './xmlcamt2json';
11
+
12
+ /**
13
+ * Parse OFX amount strings to numbers.
14
+ * Handles various OFX amount formats including currency symbols, parentheses, and multiple decimal places.
15
+ * Returns null for invalid amounts instead of NaN.
16
+ */
17
+ function parseOfxAmount(amount: string): number | null {
18
+ if (!amount || typeof amount !== 'string') {
19
+ return null;
20
+ }
21
+
22
+ // Handle parentheses for negative amounts (e.g., "(30.00)" -> "-30.00")
23
+ let cleaned = amount.trim();
24
+ if (cleaned.startsWith('(') && cleaned.endsWith(')')) {
25
+ cleaned = '-' + cleaned.slice(1, -1);
26
+ }
27
+
28
+ // Remove currency symbols and other non-numeric characters except decimal point and minus sign
29
+ cleaned = cleaned.replace(/[^\d.-]/g, '');
30
+
31
+ // Handle multiple decimal points by keeping only the first one
32
+ const decimalIndex = cleaned.indexOf('.');
33
+ if (decimalIndex !== -1) {
34
+ const beforeDecimal = cleaned.slice(0, decimalIndex);
35
+ const afterDecimal = cleaned.slice(decimalIndex + 1).replace(/\./g, '');
36
+ cleaned = beforeDecimal + '.' + afterDecimal;
37
+ }
38
+
39
+ // Ensure we have a valid number format
40
+ if (!cleaned || cleaned === '-' || cleaned === '.') {
41
+ return null;
42
+ }
43
+
44
+ const parsed = parseFloat(cleaned);
45
+ return isNaN(parsed) ? null : parsed;
46
+ }
47
+
48
+ type StructuredTransaction = {
49
+ amount: number;
50
+ date: string;
51
+ payee_name: string;
52
+ imported_payee: string;
53
+ notes: string;
54
+ };
55
+
56
+ // CSV files return raw data that are not guaranteed to be StructuredTransactions
57
+ type CsvTransaction = Record<string, string> | string[];
58
+
59
+ type Transaction = StructuredTransaction | CsvTransaction;
60
+
61
+ type ParseError = { message: string; internal: string };
62
+ export type ParseFileResult = {
63
+ errors: ParseError[];
64
+ transactions?: Transaction[];
65
+ };
66
+
67
+ export type ParseFileOptions = {
68
+ hasHeaderRow?: boolean;
69
+ delimiter?: string;
70
+ fallbackMissingPayeeToMemo?: boolean;
71
+ swapPayeeAndMemo?: boolean;
72
+ skipStartLines?: number;
73
+ skipEndLines?: number;
74
+ importNotes?: boolean;
75
+ };
76
+
77
+ export async function parseFile(
78
+ filepath: string,
79
+ options: ParseFileOptions = {},
80
+ ): Promise<ParseFileResult> {
81
+ const errors = Array<ParseError>();
82
+ const m = filepath.match(/\.[^.]*$/);
83
+
84
+ if (m) {
85
+ const ext = m[0];
86
+
87
+ switch (ext.toLowerCase()) {
88
+ case '.qif':
89
+ return parseQIF(filepath, options);
90
+ case '.csv':
91
+ case '.tsv':
92
+ return parseCSV(filepath, options);
93
+ case '.ofx':
94
+ case '.qfx':
95
+ return parseOFX(filepath, options);
96
+ case '.xml':
97
+ return parseCAMT(filepath, options);
98
+ default:
99
+ }
100
+ }
101
+
102
+ errors.push({
103
+ message: 'Invalid file type',
104
+ internal: '',
105
+ });
106
+ return { errors, transactions: [] };
107
+ }
108
+
109
+ async function parseCSV(
110
+ filepath: string,
111
+ options: ParseFileOptions,
112
+ ): Promise<ParseFileResult> {
113
+ const errors = Array<ParseError>();
114
+ let contents = await fs.readFile(filepath);
115
+
116
+ const skipStart = Math.max(0, options.skipStartLines || 0);
117
+ const skipEnd = Math.max(0, options.skipEndLines || 0);
118
+
119
+ if (skipStart > 0 || skipEnd > 0) {
120
+ const lines = contents.split(/\r?\n/);
121
+
122
+ if (skipStart + skipEnd >= lines.length) {
123
+ errors.push({
124
+ message: 'Cannot skip more lines than exist in the file',
125
+ internal: `Attempted to skip ${skipStart} start + ${skipEnd} end lines from ${lines.length} total lines`,
126
+ });
127
+ return { errors, transactions: [] };
128
+ }
129
+
130
+ const startLine = skipStart;
131
+ const endLine = skipEnd > 0 ? lines.length - skipEnd : lines.length;
132
+ contents = lines.slice(startLine, endLine).join('\r\n');
133
+ }
134
+
135
+ let data: ReturnType<typeof csv2json>;
136
+ try {
137
+ data = csv2json(contents, {
138
+ columns: options?.hasHeaderRow,
139
+ bom: true,
140
+ delimiter: options?.delimiter || ',',
141
+
142
+ quote: '"',
143
+ trim: true,
144
+ relax_column_count: true,
145
+ skip_empty_lines: true,
146
+ });
147
+ } catch (err) {
148
+ errors.push({
149
+ message: 'Failed parsing: ' + err.message,
150
+ internal: err.message,
151
+ });
152
+ return { errors, transactions: [] };
153
+ }
154
+
155
+ return { errors, transactions: data };
156
+ }
157
+
158
+ async function parseQIF(
159
+ filepath: string,
160
+ options: ParseFileOptions = {},
161
+ ): Promise<ParseFileResult> {
162
+ const errors = Array<ParseError>();
163
+ const contents = await fs.readFile(filepath);
164
+
165
+ let data: ReturnType<typeof qif2json>;
166
+ try {
167
+ data = qif2json(contents);
168
+ } catch (err) {
169
+ errors.push({
170
+ message: "Failed parsing: doesn't look like a valid QIF file.",
171
+ internal: err.stack,
172
+ });
173
+ return { errors, transactions: [] };
174
+ }
175
+
176
+ const swap = options.swapPayeeAndMemo;
177
+
178
+ return {
179
+ errors: [],
180
+ transactions: data.transactions
181
+ .map(trans => {
182
+ const payeeSource = swap ? trans.memo : trans.payee;
183
+ const memoSource = swap ? trans.payee : trans.memo;
184
+ const fallbackUsed = !payeeSource && swap;
185
+
186
+ return {
187
+ amount:
188
+ trans.amount != null ? looselyParseAmount(trans.amount) : null,
189
+ date: trans.date,
190
+ payee_name: payeeSource || (fallbackUsed ? memoSource : null),
191
+ imported_payee: payeeSource || (fallbackUsed ? memoSource : null),
192
+ notes:
193
+ options.importNotes && !fallbackUsed ? memoSource || null : null,
194
+ };
195
+ })
196
+ .filter(trans => trans.date != null && trans.amount != null),
197
+ };
198
+ }
199
+
200
+ async function parseOFX(
201
+ filepath: string,
202
+ options: ParseFileOptions,
203
+ ): Promise<ParseFileResult> {
204
+ const errors = Array<ParseError>();
205
+ const contents = await fs.readFile(filepath);
206
+
207
+ let data: Awaited<ReturnType<typeof ofx2json>>;
208
+ try {
209
+ data = await ofx2json(contents);
210
+ } catch (err) {
211
+ errors.push({
212
+ message: 'Failed importing file',
213
+ internal: err.stack,
214
+ });
215
+ return { errors };
216
+ }
217
+
218
+ // Banks don't always implement the OFX standard properly
219
+ // If no payee is available try and fallback to memo
220
+ const useMemoFallback = options.fallbackMissingPayeeToMemo;
221
+ const swap = options.swapPayeeAndMemo;
222
+
223
+ return {
224
+ errors,
225
+ transactions: data.transactions.map(trans => {
226
+ const parsedAmount = parseOfxAmount(trans.amount);
227
+ if (parsedAmount === null) {
228
+ errors.push({
229
+ message: `Invalid amount format: ${trans.amount}`,
230
+ internal: `Failed to parse amount: ${trans.amount}`,
231
+ });
232
+ }
233
+
234
+ const payeeSource = swap ? trans.memo : trans.name;
235
+ const memoSource = swap ? trans.name : trans.memo;
236
+ const fallbackUsed = !payeeSource && useMemoFallback;
237
+
238
+ return {
239
+ amount: parsedAmount || 0,
240
+ imported_id: trans.fitId,
241
+ date: trans.date,
242
+ payee_name: payeeSource || (fallbackUsed ? memoSource : null),
243
+ imported_payee: payeeSource || (fallbackUsed ? memoSource : null),
244
+ notes: options.importNotes && !fallbackUsed ? memoSource || null : null,
245
+ };
246
+ }),
247
+ };
248
+ }
249
+
250
+ async function parseCAMT(
251
+ filepath: string,
252
+ options: ParseFileOptions = {},
253
+ ): Promise<ParseFileResult> {
254
+ const errors = Array<ParseError>();
255
+ const contents = await fs.readFile(filepath);
256
+
257
+ let data: Awaited<ReturnType<typeof xmlCAMT2json>>;
258
+ try {
259
+ data = await xmlCAMT2json(contents);
260
+ } catch (err) {
261
+ logger.error(err);
262
+ errors.push({
263
+ message: 'Failed importing file',
264
+ internal: err.stack,
265
+ });
266
+ return { errors };
267
+ }
268
+
269
+ const swap = options.swapPayeeAndMemo;
270
+
271
+ return {
272
+ errors,
273
+ transactions: data.map(trans => {
274
+ const payeeSource = swap ? trans.notes : trans.payee_name;
275
+ const memoSource = swap ? trans.payee_name : trans.notes;
276
+ const fallbackUsed = !payeeSource && swap;
277
+
278
+ return {
279
+ ...trans,
280
+ payee_name: payeeSource || (fallbackUsed ? memoSource : null),
281
+ imported_payee: payeeSource || (fallbackUsed ? memoSource : null),
282
+ notes: options.importNotes && !fallbackUsed ? memoSource || null : null,
283
+ };
284
+ }),
285
+ };
286
+ }