@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,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
|
+
}
|