@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.
- package/.swcrc +11 -0
- package/bin/build-browser +40 -0
- package/bin/copy-migrations +9 -0
- package/db.sqlite +0 -0
- package/default-db.sqlite +0 -0
- package/migrations/.force-copy-windows +0 -0
- package/migrations/1548957970627_remove-db-version.sql +5 -0
- package/migrations/1550601598648_payees.sql +23 -0
- package/migrations/1555786194328_remove_category_group_unique.sql +25 -0
- package/migrations/1561751833510_indexes.sql +7 -0
- package/migrations/1567699552727_budget.sql +38 -0
- package/migrations/1582384163573_cleared.sql +6 -0
- package/migrations/1597756566448_rules.sql +10 -0
- package/migrations/1608652596043_parent_field.sql +13 -0
- package/migrations/1608652596044_trans_views.sql +56 -0
- package/migrations/1612625548236_optimize.sql +7 -0
- package/migrations/1614782639336_trans_views2.sql +33 -0
- package/migrations/1615745967948_meta.sql +10 -0
- package/migrations/1616167010796_accounts_order.sql +5 -0
- package/migrations/1618975177358_schedules.sql +28 -0
- package/migrations/1632571489012_remove_cache.js +136 -0
- package/migrations/1679728867040_rules_conditions.sql +5 -0
- package/migrations/1681115033845_add_schedule_name.sql +5 -0
- package/migrations/1682974838138_remove_payee_rules.sql +5 -0
- package/migrations/1685007876842_add_category_hidden.sql +6 -0
- package/migrations/1686139660866_remove_account_type.sql +5 -0
- package/migrations/1688749527273_transaction_filters.sql +10 -0
- package/migrations/1688841238000_add_account_type.sql +5 -0
- package/migrations/1691233396000_add_schedule_next_date_tombstone.sql +5 -0
- package/migrations/1694438752000_add_goal_targets.sql +7 -0
- package/migrations/1697046240000_add_reconciled.sql +5 -0
- package/migrations/1704572023730_add_account_sync_source.sql +5 -0
- package/migrations/1704572023731_add_missing_goCardless_sync_source.sql +9 -0
- package/migrations/1707267033000_reports.sql +28 -0
- package/migrations/1712784523000_unhide_input_group.sql +8 -0
- package/migrations/1716359441000_include_current.sql +5 -0
- package/migrations/1720310586000_link_transfer_schedules.sql +19 -0
- package/migrations/1720664867241_add_payee_favorite.sql +5 -0
- package/migrations/1720665000000_goal_context.sql +6 -0
- package/migrations/1722717601000_reports_move_selected_categories.js +55 -0
- package/migrations/1722804019000_create_dashboard_table.js +69 -0
- package/migrations/1723665565000_prefs.js +59 -0
- package/migrations/1730744182000_fix_dashboard_table.sql +7 -0
- package/migrations/1736640000000_custom_report_sorting.sql +7 -0
- package/migrations/1737158400000_add_learn_categories_to_payees.sql +5 -0
- package/migrations/1738491452000_sorting_rename.sql +13 -0
- package/migrations/1739139550000_bank_sync_page.sql +7 -0
- package/migrations/1740506588539_add_last_reconciled_at.sql +5 -0
- package/migrations/1745425408000_update_budgetType_pref.sql +7 -0
- package/migrations/1749799110000_add_tags.sql +10 -0
- package/migrations/1749799110001_tags_tombstone.sql +5 -0
- package/migrations/1754611200000_add_category_template_settings.sql +5 -0
- package/migrations/1759260219000_add_trim_interval_report_setting.sql +6 -0
- package/migrations/1759842823172_add_isGlobal_to_preferences.sql +1 -0
- package/migrations/1762178745667_rename_csv_skip_lines_pref.sql +8 -0
- package/migrations/1765518577215_multiple_dashboards.js +30 -0
- package/migrations/1768872504000_add_payee_locations.sql +21 -0
- package/package.json +128 -0
- package/src/mocks/arbitrary-schema.ts +162 -0
- package/src/mocks/budget.ts +901 -0
- package/src/mocks/files/8859-1.qfx +63 -0
- package/src/mocks/files/best.data-ever$.QFX +124 -0
- package/src/mocks/files/big.data.QiF +91 -0
- package/src/mocks/files/budgets/.commit-to-git +0 -0
- package/src/mocks/files/camt/camt.053.payee-memo.xml +127 -0
- package/src/mocks/files/camt/camt.053.xml +463 -0
- package/src/mocks/files/credit-card.ofx +11 -0
- package/src/mocks/files/data-multi-decimal.ofx +64 -0
- package/src/mocks/files/data-payee-memo.ofx +75 -0
- package/src/mocks/files/data-payee-memo.qif +17 -0
- package/src/mocks/files/data.ofx +124 -0
- package/src/mocks/files/data.qfx +124 -0
- package/src/mocks/files/data.qif +91 -0
- package/src/mocks/files/default-budget-template/db.sqlite +0 -0
- package/src/mocks/files/default-budget-template/metadata.json +6 -0
- package/src/mocks/files/html-vals.qfx +17 -0
- package/src/mocks/index.ts +221 -0
- package/src/mocks/migrations/1508717984291_up_add-poop.sql +13 -0
- package/src/mocks/migrations/1508718036311_up_modify-poop.sql +2 -0
- package/src/mocks/migrations/1508727787513_remove-is_income.sql +15 -0
- package/src/mocks/random.ts +16 -0
- package/src/mocks/setup.ts +180 -0
- package/src/mocks/spreadsheet.ts +101 -0
- package/src/mocks/util.ts +82 -0
- package/src/platform/client/connection/README.md +3 -0
- package/src/platform/client/connection/__mocks__/index.ts +67 -0
- package/src/platform/client/connection/index-types.ts +95 -0
- package/src/platform/client/connection/index.browser.ts +213 -0
- package/src/platform/client/connection/index.ts +155 -0
- package/src/platform/client/undo/index.ts +59 -0
- package/src/platform/exceptions/__mocks__/index.ts +7 -0
- package/src/platform/exceptions/index.ts +9 -0
- package/src/platform/server/asyncStorage/__mocks__/index.ts +50 -0
- package/src/platform/server/asyncStorage/index-types.ts +35 -0
- package/src/platform/server/asyncStorage/index.api.ts +2 -0
- package/src/platform/server/asyncStorage/index.electron.ts +88 -0
- package/src/platform/server/asyncStorage/index.ts +126 -0
- package/src/platform/server/connection/README.md +3 -0
- package/src/platform/server/connection/__mocks__/index.ts +15 -0
- package/src/platform/server/connection/index-types.ts +20 -0
- package/src/platform/server/connection/index.api.ts +13 -0
- package/src/platform/server/connection/index.electron.ts +102 -0
- package/src/platform/server/connection/index.ts +154 -0
- package/src/platform/server/fetch/__mocks__/index.ts +3 -0
- package/src/platform/server/fetch/index.api.ts +1 -0
- package/src/platform/server/fetch/index.electron.ts +18 -0
- package/src/platform/server/fetch/index.ts +20 -0
- package/src/platform/server/fs/index.api.ts +198 -0
- package/src/platform/server/fs/index.electron.ts +208 -0
- package/src/platform/server/fs/index.test.ts +117 -0
- package/src/platform/server/fs/index.ts +416 -0
- package/src/platform/server/fs/path-join.api.ts +1 -0
- package/src/platform/server/fs/path-join.electron.ts +1 -0
- package/src/platform/server/fs/path-join.ts +97 -0
- package/src/platform/server/fs/shared.ts +33 -0
- package/src/platform/server/indexeddb/index.ts +115 -0
- package/src/platform/server/log/index.ts +43 -0
- package/src/platform/server/sqlite/index.api.ts +2 -0
- package/src/platform/server/sqlite/index.electron.ts +134 -0
- package/src/platform/server/sqlite/index.test.ts +108 -0
- package/src/platform/server/sqlite/index.ts +241 -0
- package/src/platform/server/sqlite/normalise.ts +9 -0
- package/src/platform/server/sqlite/unicodeLike.test.ts +58 -0
- package/src/platform/server/sqlite/unicodeLike.ts +31 -0
- package/src/server/__mocks__/post.ts +9 -0
- package/src/server/__snapshots__/main.test.ts.snap +199 -0
- package/src/server/__snapshots__/sheet.test.ts.snap +9 -0
- package/src/server/accounts/__snapshots__/sync.test.ts.snap +136 -0
- package/src/server/accounts/app-bank-sync.test.ts +136 -0
- package/src/server/accounts/app.ts +1294 -0
- package/src/server/accounts/link.ts +25 -0
- package/src/server/accounts/payees.ts +36 -0
- package/src/server/accounts/sync.test.ts +679 -0
- package/src/server/accounts/sync.ts +1168 -0
- package/src/server/accounts/title/index.ts +60 -0
- package/src/server/accounts/title/lower-case.ts +93 -0
- package/src/server/accounts/title/specials.ts +21 -0
- package/src/server/admin/app.ts +241 -0
- package/src/server/api-models.ts +244 -0
- package/src/server/api.test.ts +36 -0
- package/src/server/api.ts +1030 -0
- package/src/server/app.ts +91 -0
- package/src/server/aql/compiler.test.ts +966 -0
- package/src/server/aql/compiler.ts +1222 -0
- package/src/server/aql/exec.test.ts +289 -0
- package/src/server/aql/exec.ts +128 -0
- package/src/server/aql/index.ts +41 -0
- package/src/server/aql/schema/executors.test.ts +420 -0
- package/src/server/aql/schema/executors.ts +345 -0
- package/src/server/aql/schema/index.test.ts +67 -0
- package/src/server/aql/schema/index.ts +409 -0
- package/src/server/aql/schema-helpers.test.ts +242 -0
- package/src/server/aql/schema-helpers.ts +208 -0
- package/src/server/aql/views.test.ts +62 -0
- package/src/server/aql/views.ts +57 -0
- package/src/server/auth/app.ts +387 -0
- package/src/server/bench.ts +29 -0
- package/src/server/budget/actions.ts +686 -0
- package/src/server/budget/app.ts +469 -0
- package/src/server/budget/base.test.ts +340 -0
- package/src/server/budget/base.ts +339 -0
- package/src/server/budget/category-template-context.test.ts +1658 -0
- package/src/server/budget/category-template-context.ts +862 -0
- package/src/server/budget/cleanup-template.pegjs +27 -0
- package/src/server/budget/cleanup-template.ts +408 -0
- package/src/server/budget/envelope.ts +403 -0
- package/src/server/budget/goal-template.pegjs +110 -0
- package/src/server/budget/goal-template.ts +309 -0
- package/src/server/budget/report.ts +308 -0
- package/src/server/budget/schedule-template.test.ts +184 -0
- package/src/server/budget/schedule-template.ts +351 -0
- package/src/server/budget/statements.ts +60 -0
- package/src/server/budget/template-notes.test.ts +393 -0
- package/src/server/budget/template-notes.ts +323 -0
- package/src/server/budget/util.ts +25 -0
- package/src/server/budgetfiles/__snapshots__/backups.test.ts.snap +101 -0
- package/src/server/budgetfiles/app.ts +672 -0
- package/src/server/budgetfiles/backups.test.ts +79 -0
- package/src/server/budgetfiles/backups.ts +251 -0
- package/src/server/cloud-storage.ts +467 -0
- package/src/server/dashboard/app.ts +373 -0
- package/src/server/db/__snapshots__/index.test.ts.snap +271 -0
- package/src/server/db/index.test.ts +300 -0
- package/src/server/db/index.ts +855 -0
- package/src/server/db/mappings.ts +59 -0
- package/src/server/db/sort.ts +58 -0
- package/src/server/db/types/index.ts +342 -0
- package/src/server/db/util.ts +36 -0
- package/src/server/encryption/app.ts +133 -0
- package/src/server/encryption/encryption-internals.api.ts +2 -0
- package/src/server/encryption/encryption-internals.electron.ts +89 -0
- package/src/server/encryption/encryption-internals.ts +109 -0
- package/src/server/encryption/encryption.test.ts +19 -0
- package/src/server/encryption/index.test.ts +19 -0
- package/src/server/encryption/index.ts +89 -0
- package/src/server/errors.ts +110 -0
- package/src/server/filters/app.ts +191 -0
- package/src/server/importers/actual.ts +49 -0
- package/src/server/importers/index.ts +58 -0
- package/src/server/importers/ynab4-types.ts +163 -0
- package/src/server/importers/ynab4.ts +470 -0
- package/src/server/importers/ynab5-types.ts +290 -0
- package/src/server/importers/ynab5.ts +1193 -0
- package/src/server/main-app.ts +25 -0
- package/src/server/main.test.ts +392 -0
- package/src/server/main.ts +336 -0
- package/src/server/migrate/__snapshots__/migrations.test.ts.snap +17 -0
- package/src/server/migrate/cli.ts +100 -0
- package/src/server/migrate/migrations.test.ts +81 -0
- package/src/server/migrate/migrations.ts +192 -0
- package/src/server/models.ts +184 -0
- package/src/server/mutators.ts +139 -0
- package/src/server/notes/app.ts +18 -0
- package/src/server/payees/app.ts +351 -0
- package/src/server/polyfills.ts +26 -0
- package/src/server/post.ts +219 -0
- package/src/server/preferences/app.ts +249 -0
- package/src/server/prefs.ts +91 -0
- package/src/server/reports/app.ts +187 -0
- package/src/server/rules/action.ts +344 -0
- package/src/server/rules/app.ts +193 -0
- package/src/server/rules/condition.ts +436 -0
- package/src/server/rules/customFunctions.ts +61 -0
- package/src/server/rules/formula-action.test.ts +175 -0
- package/src/server/rules/handlebars-helpers.ts +131 -0
- package/src/server/rules/index.test.ts +1095 -0
- package/src/server/rules/index.ts +22 -0
- package/src/server/rules/rule-indexer.ts +89 -0
- package/src/server/rules/rule-utils.ts +274 -0
- package/src/server/rules/rule.ts +193 -0
- package/src/server/schedules/app.test.ts +502 -0
- package/src/server/schedules/app.ts +644 -0
- package/src/server/schedules/find-schedules.ts +391 -0
- package/src/server/server-config.ts +59 -0
- package/src/server/sheet.test.ts +101 -0
- package/src/server/sheet.ts +280 -0
- package/src/server/spreadsheet/__snapshots__/spreadsheet.test.ts.snap +5 -0
- package/src/server/spreadsheet/app.ts +54 -0
- package/src/server/spreadsheet/globals.ts +13 -0
- package/src/server/spreadsheet/graph-data-structure.ts +165 -0
- package/src/server/spreadsheet/scratch +60 -0
- package/src/server/spreadsheet/spreadsheet.test.ts +191 -0
- package/src/server/spreadsheet/spreadsheet.ts +523 -0
- package/src/server/spreadsheet/util.ts +15 -0
- package/src/server/sql/init.sql +88 -0
- package/src/server/sync/__snapshots__/sync.test.ts.snap +31 -0
- package/src/server/sync/app.ts +29 -0
- package/src/server/sync/encoder.ts +129 -0
- package/src/server/sync/index.ts +820 -0
- package/src/server/sync/make-test-message.ts +19 -0
- package/src/server/sync/migrate.test.ts +169 -0
- package/src/server/sync/migrate.ts +48 -0
- package/src/server/sync/repair.ts +39 -0
- package/src/server/sync/reset.ts +91 -0
- package/src/server/sync/sync.property.test.ts +385 -0
- package/src/server/sync/sync.test.ts +349 -0
- package/src/server/sync/utils.ts +3 -0
- package/src/server/tags/app.ts +101 -0
- package/src/server/tests/mockData.json +9352 -0
- package/src/server/tests/mockSyncServer.ts +119 -0
- package/src/server/tools/app.ts +152 -0
- package/src/server/transactions/__snapshots__/transaction-rules.test.ts.snap +173 -0
- package/src/server/transactions/__snapshots__/transfer.test.ts.snap +655 -0
- package/src/server/transactions/app.ts +136 -0
- package/src/server/transactions/export/export-to-csv.ts +132 -0
- package/src/server/transactions/import/__snapshots__/parse-file.test.ts.snap +1582 -0
- package/src/server/transactions/import/ofx2json.test.ts +33 -0
- package/src/server/transactions/import/ofx2json.ts +157 -0
- package/src/server/transactions/import/parse-file.test.ts +224 -0
- package/src/server/transactions/import/parse-file.ts +286 -0
- package/src/server/transactions/import/qif2json.ts +110 -0
- package/src/server/transactions/import/xmlcamt2json.ts +168 -0
- package/src/server/transactions/index.ts +196 -0
- package/src/server/transactions/merge.test.ts +370 -0
- package/src/server/transactions/merge.ts +139 -0
- package/src/server/transactions/transaction-rules.test.ts +994 -0
- package/src/server/transactions/transaction-rules.ts +1038 -0
- package/src/server/transactions/transfer.test.ts +221 -0
- package/src/server/transactions/transfer.ts +173 -0
- package/src/server/undo.ts +271 -0
- package/src/server/update.ts +37 -0
- package/src/server/util/budget-name.ts +61 -0
- package/src/server/util/custom-sync-mapping.ts +48 -0
- package/src/server/util/rschedule.ts +9 -0
- package/src/shared/__mocks__/platform.ts +7 -0
- package/src/shared/__snapshots__/months.test.ts.snap +21 -0
- package/src/shared/arithmetic.test.ts +112 -0
- package/src/shared/arithmetic.ts +170 -0
- package/src/shared/async.test.ts +135 -0
- package/src/shared/async.ts +76 -0
- package/src/shared/constants.ts +5 -0
- package/src/shared/currencies.ts +70 -0
- package/src/shared/dashboard.ts +260 -0
- package/src/shared/environment.ts +18 -0
- package/src/shared/errors.ts +195 -0
- package/src/shared/locale.ts +27 -0
- package/src/shared/location-utils.test.ts +69 -0
- package/src/shared/location-utils.ts +49 -0
- package/src/shared/months.test.ts +5 -0
- package/src/shared/months.ts +485 -0
- package/src/shared/normalisation.ts +6 -0
- package/src/shared/platform.electron.ts +21 -0
- package/src/shared/platform.ts +20 -0
- package/src/shared/query.ts +176 -0
- package/src/shared/rules.test.ts +56 -0
- package/src/shared/rules.ts +371 -0
- package/src/shared/schedules.test.ts +570 -0
- package/src/shared/schedules.ts +560 -0
- package/src/shared/test-helpers.ts +156 -0
- package/src/shared/transactions.test.ts +275 -0
- package/src/shared/transactions.ts +433 -0
- package/src/shared/transfer.test.ts +75 -0
- package/src/shared/transfer.ts +16 -0
- package/src/shared/user.ts +4 -0
- package/src/shared/util.test.ts +240 -0
- package/src/shared/util.ts +633 -0
- package/src/types/api-handlers.ts +287 -0
- package/src/types/budget.ts +8 -0
- package/src/types/file.ts +47 -0
- package/src/types/handlers.ts +46 -0
- package/src/types/models/account.ts +24 -0
- package/src/types/models/bank-sync.ts +23 -0
- package/src/types/models/bank.ts +6 -0
- package/src/types/models/category-group.ts +11 -0
- package/src/types/models/category.ts +13 -0
- package/src/types/models/dashboard.ts +199 -0
- package/src/types/models/gocardless.ts +84 -0
- package/src/types/models/import-transaction.ts +56 -0
- package/src/types/models/index.ts +23 -0
- package/src/types/models/nearby-payee.ts +7 -0
- package/src/types/models/note.ts +4 -0
- package/src/types/models/openid.ts +8 -0
- package/src/types/models/payee-location.ts +8 -0
- package/src/types/models/payee.ts +10 -0
- package/src/types/models/pluggyai.ts +19 -0
- package/src/types/models/reports.ts +144 -0
- package/src/types/models/rule.ts +174 -0
- package/src/types/models/schedule.ts +49 -0
- package/src/types/models/simplefin.ts +28 -0
- package/src/types/models/tags.ts +6 -0
- package/src/types/models/templates.ts +135 -0
- package/src/types/models/transaction-filter.ts +9 -0
- package/src/types/models/transaction.ts +39 -0
- package/src/types/models/user-access.ts +10 -0
- package/src/types/models/user.ts +25 -0
- package/src/types/prefs.ts +167 -0
- package/src/types/server-events.ts +86 -0
- package/src/types/server-handlers.ts +27 -0
- package/src/types/util.ts +26 -0
- package/tsconfig.json +34 -0
- package/typings/pegjs.ts +1 -0
- package/typings/process-worker.ts +12 -0
- package/typings/vite-plugin-peggy-loader.ts +1 -0
- package/typings/window.ts +62 -0
- package/vite.config.ts +109 -0
- package/vite.desktop.config.ts +59 -0
- package/vitest.config.ts +43 -0
- 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, <world>!')).toBe('Hello, <world>!');
|
|
11
|
+
});
|
|
12
|
+
test('apostrophes are unescaped', async () => {
|
|
13
|
+
expect(html2Plain('Hello, 'world'!')).toBe("Hello, 'world'!");
|
|
14
|
+
});
|
|
15
|
+
test('quotes are unescaped', async () => {
|
|
16
|
+
expect(html2Plain('Hello, "world"!')).toBe('Hello, "world"!');
|
|
17
|
+
});
|
|
18
|
+
test('ampersands are unescaped', async () => {
|
|
19
|
+
expect(html2Plain('Hello, &world&!')).toBe('Hello, &world&!');
|
|
20
|
+
expect(html2Plain('Hello, &world&!')).toBe('Hello, &world&!');
|
|
21
|
+
});
|
|
22
|
+
test('no double unescaping with other entities', async () => {
|
|
23
|
+
expect(html2Plain('Hello, &#038;world&#038;!')).toBe(
|
|
24
|
+
'Hello, &world&!',
|
|
25
|
+
);
|
|
26
|
+
expect(html2Plain('Hello, &amp;world&amp;!')).toBe(
|
|
27
|
+
'Hello, &world&!',
|
|
28
|
+
);
|
|
29
|
+
expect(html2Plain('Hello, &quot;world&quot;!')).toBe(
|
|
30
|
+
'Hello, "world"!',
|
|
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, '&') // Replace ampersands
|
|
23
|
+
.replace(/&/g, '&')
|
|
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(/</g, '<') // lessthan
|
|
35
|
+
.replace(/>/g, '>') // greaterthan
|
|
36
|
+
.replace(/'/g, "'")
|
|
37
|
+
.replace(/"/g, '"')
|
|
38
|
+
.replace(/(&|&)/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
|
+
}
|