@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,1168 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import * as dateFns from 'date-fns';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
import * as asyncStorage from '../../platform/server/asyncStorage';
|
|
6
|
+
import { logger } from '../../platform/server/log';
|
|
7
|
+
import * as monthUtils from '../../shared/months';
|
|
8
|
+
import { q } from '../../shared/query';
|
|
9
|
+
import {
|
|
10
|
+
makeChild as makeChildTransaction,
|
|
11
|
+
recalculateSplit,
|
|
12
|
+
} from '../../shared/transactions';
|
|
13
|
+
import {
|
|
14
|
+
amountToInteger,
|
|
15
|
+
hasFieldsChanged,
|
|
16
|
+
integerToAmount,
|
|
17
|
+
} from '../../shared/util';
|
|
18
|
+
import type {
|
|
19
|
+
AccountEntity,
|
|
20
|
+
BankSyncResponse,
|
|
21
|
+
TransactionEntity,
|
|
22
|
+
} from '../../types/models';
|
|
23
|
+
import { aqlQuery } from '../aql';
|
|
24
|
+
import * as db from '../db';
|
|
25
|
+
import { runMutator } from '../mutators';
|
|
26
|
+
import { post } from '../post';
|
|
27
|
+
import { getServer } from '../server-config';
|
|
28
|
+
import { batchMessages } from '../sync';
|
|
29
|
+
import { batchUpdateTransactions } from '../transactions';
|
|
30
|
+
import { runRules } from '../transactions/transaction-rules';
|
|
31
|
+
import {
|
|
32
|
+
defaultMappings,
|
|
33
|
+
mappingsFromString,
|
|
34
|
+
} from '../util/custom-sync-mapping';
|
|
35
|
+
|
|
36
|
+
import { getStartingBalancePayee } from './payees';
|
|
37
|
+
import { title } from './title';
|
|
38
|
+
|
|
39
|
+
function BankSyncError(type: string, code: string, details?: object) {
|
|
40
|
+
return { type: 'BankSyncError', category: type, code, details };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function makeSplitTransaction(trans, subtransactions) {
|
|
44
|
+
// We need to calculate the final state of split transactions
|
|
45
|
+
const { subtransactions: sub, ...parent } = recalculateSplit({
|
|
46
|
+
...trans,
|
|
47
|
+
is_parent: true,
|
|
48
|
+
subtransactions: subtransactions.map((transaction, idx) =>
|
|
49
|
+
makeChildTransaction(trans, {
|
|
50
|
+
...transaction,
|
|
51
|
+
sort_order: 0 - idx,
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
});
|
|
55
|
+
return [parent, ...sub];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getAccountBalance(account) {
|
|
59
|
+
// Debt account types need their balance reversed
|
|
60
|
+
switch (account.type) {
|
|
61
|
+
case 'credit':
|
|
62
|
+
case 'loan':
|
|
63
|
+
return -account.balances.current;
|
|
64
|
+
default:
|
|
65
|
+
return account.balances.current;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function updateAccountBalance(id: AccountEntity['id'], balance: number) {
|
|
70
|
+
db.runQuery('UPDATE accounts SET balance_current = ? WHERE id = ?', [
|
|
71
|
+
balance,
|
|
72
|
+
id,
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function getAccountOldestTransaction(id): Promise<TransactionEntity> {
|
|
77
|
+
return (
|
|
78
|
+
await aqlQuery(
|
|
79
|
+
q('transactions')
|
|
80
|
+
.filter({
|
|
81
|
+
account: id,
|
|
82
|
+
date: { $lte: monthUtils.currentDay() },
|
|
83
|
+
})
|
|
84
|
+
.select('date')
|
|
85
|
+
.orderBy('date')
|
|
86
|
+
.limit(1),
|
|
87
|
+
)
|
|
88
|
+
).data?.[0];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function getAccountSyncStartDate(id) {
|
|
92
|
+
// Many GoCardless integrations do not support getting more than 90 days
|
|
93
|
+
// worth of data, so make that the earliest possible limit.
|
|
94
|
+
const dates = [monthUtils.subDays(monthUtils.currentDay(), 90)];
|
|
95
|
+
|
|
96
|
+
const oldestTransaction = await getAccountOldestTransaction(id);
|
|
97
|
+
|
|
98
|
+
if (oldestTransaction) dates.push(oldestTransaction.date);
|
|
99
|
+
|
|
100
|
+
return monthUtils.dayFromDate(
|
|
101
|
+
dateFns.max(dates.map(d => monthUtils.parseDate(d))),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function getGoCardlessAccounts(userId, userKey, id) {
|
|
106
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
107
|
+
if (!userToken) return;
|
|
108
|
+
|
|
109
|
+
const res = await post(
|
|
110
|
+
getServer().GOCARDLESS_SERVER + '/accounts',
|
|
111
|
+
{
|
|
112
|
+
userId,
|
|
113
|
+
key: userKey,
|
|
114
|
+
item_id: id,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const { accounts } = res;
|
|
122
|
+
|
|
123
|
+
accounts.forEach(acct => {
|
|
124
|
+
acct.balances.current = getAccountBalance(acct);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return accounts;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function downloadGoCardlessTransactions(
|
|
131
|
+
userId,
|
|
132
|
+
userKey,
|
|
133
|
+
acctId,
|
|
134
|
+
bankId,
|
|
135
|
+
since,
|
|
136
|
+
includeBalance = true,
|
|
137
|
+
) {
|
|
138
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
139
|
+
if (!userToken) return;
|
|
140
|
+
|
|
141
|
+
logger.log('Pulling transactions from GoCardless');
|
|
142
|
+
|
|
143
|
+
const res = await post(
|
|
144
|
+
getServer().GOCARDLESS_SERVER + '/transactions',
|
|
145
|
+
{
|
|
146
|
+
userId,
|
|
147
|
+
key: userKey,
|
|
148
|
+
requisitionId: bankId,
|
|
149
|
+
accountId: acctId,
|
|
150
|
+
startDate: since,
|
|
151
|
+
includeBalance,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (res.error_code) {
|
|
159
|
+
const errorDetails = {
|
|
160
|
+
rateLimitHeaders: res.rateLimitHeaders,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
throw BankSyncError(res.error_type, res.error_code, errorDetails);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (includeBalance) {
|
|
167
|
+
const {
|
|
168
|
+
transactions: { all },
|
|
169
|
+
balances,
|
|
170
|
+
startingBalance,
|
|
171
|
+
} = res;
|
|
172
|
+
|
|
173
|
+
logger.log('Response:', res);
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
transactions: all,
|
|
177
|
+
accountBalance: balances,
|
|
178
|
+
startingBalance,
|
|
179
|
+
};
|
|
180
|
+
} else {
|
|
181
|
+
logger.log('Response:', res);
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
transactions: res.transactions.all,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function downloadSimpleFinTransactions(
|
|
190
|
+
acctId: AccountEntity['id'] | AccountEntity['id'][],
|
|
191
|
+
since: string | string[],
|
|
192
|
+
) {
|
|
193
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
194
|
+
if (!userToken) return;
|
|
195
|
+
|
|
196
|
+
const batchSync = Array.isArray(acctId);
|
|
197
|
+
|
|
198
|
+
logger.log('Pulling transactions from SimpleFin');
|
|
199
|
+
|
|
200
|
+
let res;
|
|
201
|
+
try {
|
|
202
|
+
res = await post(
|
|
203
|
+
getServer().SIMPLEFIN_SERVER + '/transactions',
|
|
204
|
+
{
|
|
205
|
+
accountId: acctId,
|
|
206
|
+
startDate: since,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
210
|
+
},
|
|
211
|
+
// 5 minute timeout for batch sync, one minute for individual accounts
|
|
212
|
+
Array.isArray(acctId) ? 300000 : 60000,
|
|
213
|
+
);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
logger.error('Suspected timeout during bank sync:', error);
|
|
216
|
+
throw BankSyncError('TIMED_OUT', 'TIMED_OUT');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (Object.keys(res).length === 0) {
|
|
220
|
+
throw BankSyncError('NO_DATA', 'NO_DATA');
|
|
221
|
+
}
|
|
222
|
+
if (res.error_code) {
|
|
223
|
+
throw BankSyncError(res.error_type, res.error_code);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let retVal = {};
|
|
227
|
+
if (batchSync) {
|
|
228
|
+
const batchErrors = res.errors;
|
|
229
|
+
for (const accountId of Object.keys(res)) {
|
|
230
|
+
if (accountId === 'errors') continue;
|
|
231
|
+
|
|
232
|
+
const data = res[accountId];
|
|
233
|
+
const error = batchErrors?.[accountId]?.[0];
|
|
234
|
+
|
|
235
|
+
retVal[accountId] = {
|
|
236
|
+
transactions: data?.transactions?.all,
|
|
237
|
+
accountBalance: data?.balances,
|
|
238
|
+
startingBalance: data?.startingBalance,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
if (error) {
|
|
242
|
+
retVal[accountId].error_type = error.error_type;
|
|
243
|
+
retVal[accountId].error_code = error.error_code;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Add entries for accounts that only have errors (no data in the response)
|
|
248
|
+
if (batchErrors) {
|
|
249
|
+
for (const [accountId, errorList] of Object.entries(batchErrors)) {
|
|
250
|
+
if (
|
|
251
|
+
!retVal[accountId] &&
|
|
252
|
+
Array.isArray(errorList) &&
|
|
253
|
+
errorList.length > 0
|
|
254
|
+
) {
|
|
255
|
+
const error = errorList[0];
|
|
256
|
+
retVal[accountId] = {
|
|
257
|
+
transactions: [],
|
|
258
|
+
accountBalance: [],
|
|
259
|
+
startingBalance: 0,
|
|
260
|
+
error_type: error.error_type,
|
|
261
|
+
error_code: error.error_code,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
retVal = {
|
|
268
|
+
transactions: res.transactions.all,
|
|
269
|
+
accountBalance: res.balances,
|
|
270
|
+
startingBalance: res.startingBalance,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
logger.log('Response:', retVal);
|
|
275
|
+
return retVal;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function downloadPluggyAiTransactions(
|
|
279
|
+
acctId: AccountEntity['id'],
|
|
280
|
+
since: string,
|
|
281
|
+
) {
|
|
282
|
+
const userToken = await asyncStorage.getItem('user-token');
|
|
283
|
+
if (!userToken) return;
|
|
284
|
+
|
|
285
|
+
logger.log('Pulling transactions from Pluggy.ai');
|
|
286
|
+
|
|
287
|
+
const res = await post(
|
|
288
|
+
getServer().PLUGGYAI_SERVER + '/transactions',
|
|
289
|
+
{
|
|
290
|
+
accountId: acctId,
|
|
291
|
+
startDate: since,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
'X-ACTUAL-TOKEN': userToken,
|
|
295
|
+
},
|
|
296
|
+
60000,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (res.error_code) {
|
|
300
|
+
throw BankSyncError(res.error_type, res.error_code);
|
|
301
|
+
} else if ('error' in res) {
|
|
302
|
+
throw BankSyncError('Connection', res.error);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let retVal = {};
|
|
306
|
+
const singleRes = res as BankSyncResponse;
|
|
307
|
+
retVal = {
|
|
308
|
+
transactions: singleRes.transactions.all,
|
|
309
|
+
accountBalance: singleRes.balances,
|
|
310
|
+
startingBalance: singleRes.startingBalance,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
logger.log('Response:', retVal);
|
|
314
|
+
return retVal;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function resolvePayee(trans, payeeName, payeesToCreate) {
|
|
318
|
+
if (trans.payee == null && payeeName) {
|
|
319
|
+
// First check our registry of new payees (to avoid a db access)
|
|
320
|
+
// then check the db for existing payees
|
|
321
|
+
let payee = payeesToCreate.get(payeeName.toLowerCase());
|
|
322
|
+
payee = payee || (await db.getPayeeByName(payeeName));
|
|
323
|
+
|
|
324
|
+
if (payee != null) {
|
|
325
|
+
return payee.id;
|
|
326
|
+
} else {
|
|
327
|
+
// Otherwise we're going to create a new one
|
|
328
|
+
const newPayee = { id: uuidv4(), name: payeeName };
|
|
329
|
+
payeesToCreate.set(payeeName.toLowerCase(), newPayee);
|
|
330
|
+
return newPayee.id;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return trans.payee;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function normalizeTransactions(
|
|
338
|
+
transactions,
|
|
339
|
+
acctId,
|
|
340
|
+
{ rawPayeeName = false } = {},
|
|
341
|
+
) {
|
|
342
|
+
const payeesToCreate = new Map();
|
|
343
|
+
|
|
344
|
+
const normalized = [];
|
|
345
|
+
for (let trans of transactions) {
|
|
346
|
+
// Validate the date because we do some stuff with it. The db
|
|
347
|
+
// layer does better validation, but this will give nicer errors
|
|
348
|
+
if (trans.date == null) {
|
|
349
|
+
throw new Error('`date` is required when adding a transaction');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Strip off the irregular properties
|
|
353
|
+
const { payee_name: originalPayeeName, subtransactions, ...rest } = trans;
|
|
354
|
+
trans = rest;
|
|
355
|
+
|
|
356
|
+
let payee_name = originalPayeeName;
|
|
357
|
+
if (payee_name) {
|
|
358
|
+
const trimmed = payee_name.trim();
|
|
359
|
+
if (trimmed === '') {
|
|
360
|
+
payee_name = null;
|
|
361
|
+
} else {
|
|
362
|
+
payee_name = rawPayeeName ? trimmed : title(trimmed);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
trans.imported_payee = trans.imported_payee || payee_name;
|
|
367
|
+
if (trans.imported_payee) {
|
|
368
|
+
trans.imported_payee = trans.imported_payee.trim();
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// It's important to resolve both the account and payee early so
|
|
372
|
+
// when rules are run, they have the right data. Resolving payees
|
|
373
|
+
// also simplifies the payee creation process
|
|
374
|
+
trans.account = acctId;
|
|
375
|
+
trans.payee = await resolvePayee(trans, payee_name, payeesToCreate);
|
|
376
|
+
|
|
377
|
+
trans.category = trans.category ?? null;
|
|
378
|
+
|
|
379
|
+
normalized.push({
|
|
380
|
+
payee_name,
|
|
381
|
+
subtransactions: subtransactions
|
|
382
|
+
? subtransactions.map(t => ({ ...t, account: acctId }))
|
|
383
|
+
: null,
|
|
384
|
+
trans,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { normalized, payeesToCreate };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function normalizeBankSyncTransactions(transactions, acctId) {
|
|
392
|
+
const payeesToCreate = new Map();
|
|
393
|
+
|
|
394
|
+
const [customMappingsRaw, importPending, importNotes] = await Promise.all([
|
|
395
|
+
aqlQuery(
|
|
396
|
+
q('preferences')
|
|
397
|
+
.filter({ id: `custom-sync-mappings-${acctId}` })
|
|
398
|
+
.select('value'),
|
|
399
|
+
).then(data => data?.data?.[0]?.value),
|
|
400
|
+
aqlQuery(
|
|
401
|
+
q('preferences')
|
|
402
|
+
.filter({ id: `sync-import-pending-${acctId}` })
|
|
403
|
+
.select('value'),
|
|
404
|
+
).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true'),
|
|
405
|
+
aqlQuery(
|
|
406
|
+
q('preferences')
|
|
407
|
+
.filter({ id: `sync-import-notes-${acctId}` })
|
|
408
|
+
.select('value'),
|
|
409
|
+
).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true'),
|
|
410
|
+
]);
|
|
411
|
+
|
|
412
|
+
const mappings = customMappingsRaw
|
|
413
|
+
? mappingsFromString(customMappingsRaw)
|
|
414
|
+
: defaultMappings;
|
|
415
|
+
|
|
416
|
+
const normalized = [];
|
|
417
|
+
for (const trans of transactions) {
|
|
418
|
+
trans.cleared = Boolean(trans.booked);
|
|
419
|
+
|
|
420
|
+
if (!importPending && !trans.cleared) continue;
|
|
421
|
+
|
|
422
|
+
if (!trans.amount) {
|
|
423
|
+
trans.amount = trans.transactionAmount.amount;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const mapping = mappings.get(trans.amount <= 0 ? 'payment' : 'deposit');
|
|
427
|
+
|
|
428
|
+
const date = trans[mapping.get('date')] ?? trans.date;
|
|
429
|
+
const payeeName = trans[mapping.get('payee')] ?? trans.payeeName;
|
|
430
|
+
const notes = trans[mapping.get('notes')];
|
|
431
|
+
|
|
432
|
+
// Validate the date because we do some stuff with it. The db
|
|
433
|
+
// layer does better validation, but this will give nicer errors
|
|
434
|
+
if (date == null) {
|
|
435
|
+
throw new Error('`date` is required when adding a transaction');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (payeeName == null) {
|
|
439
|
+
throw new Error('`payeeName` is required when adding a transaction');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
trans.imported_payee = trans.imported_payee || payeeName;
|
|
443
|
+
if (trans.imported_payee) {
|
|
444
|
+
trans.imported_payee = trans.imported_payee.trim();
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let imported_id = trans.transactionId;
|
|
448
|
+
if (trans.cleared && !trans.transactionId && trans.internalTransactionId) {
|
|
449
|
+
imported_id = `${trans.account}-${trans.internalTransactionId}`;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// It's important to resolve both the account and payee early so
|
|
453
|
+
// when rules are run, they have the right data. Resolving payees
|
|
454
|
+
// also simplifies the payee creation process
|
|
455
|
+
trans.account = acctId;
|
|
456
|
+
trans.payee = await resolvePayee(trans, payeeName, payeesToCreate);
|
|
457
|
+
|
|
458
|
+
normalized.push({
|
|
459
|
+
payee_name: payeeName,
|
|
460
|
+
trans: {
|
|
461
|
+
amount: amountToInteger(trans.amount),
|
|
462
|
+
payee: trans.payee,
|
|
463
|
+
account: trans.account,
|
|
464
|
+
date,
|
|
465
|
+
notes: importNotes && notes ? notes.trim().replace(/#/g, '##') : null,
|
|
466
|
+
category: trans.category ?? null,
|
|
467
|
+
imported_id,
|
|
468
|
+
imported_payee: trans.imported_payee,
|
|
469
|
+
cleared: trans.cleared,
|
|
470
|
+
raw_synced_data: JSON.stringify(trans),
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return { normalized, payeesToCreate };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function createNewPayees(payeesToCreate, addsAndUpdates) {
|
|
479
|
+
const usedPayeeIds = new Set(addsAndUpdates.map(t => t.payee));
|
|
480
|
+
|
|
481
|
+
await batchMessages(async () => {
|
|
482
|
+
for (const payee of payeesToCreate.values()) {
|
|
483
|
+
// Only create the payee if it ended up being used
|
|
484
|
+
if (usedPayeeIds.has(payee.id)) {
|
|
485
|
+
await db.insertPayee(payee);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
export type ReconcileTransactionsResult = {
|
|
492
|
+
added: string[];
|
|
493
|
+
updated: string[];
|
|
494
|
+
updatedPreview: Array<{
|
|
495
|
+
transaction: TransactionEntity;
|
|
496
|
+
existing?: TransactionEntity;
|
|
497
|
+
ignored?: boolean;
|
|
498
|
+
tombstone?: boolean;
|
|
499
|
+
}>;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
export async function reconcileTransactions(
|
|
503
|
+
acctId,
|
|
504
|
+
transactions,
|
|
505
|
+
isBankSyncAccount = false,
|
|
506
|
+
strictIdChecking = true,
|
|
507
|
+
isPreview = false,
|
|
508
|
+
defaultCleared = true,
|
|
509
|
+
updateDates = false,
|
|
510
|
+
): Promise<ReconcileTransactionsResult> {
|
|
511
|
+
logger.log('Performing transaction reconciliation');
|
|
512
|
+
|
|
513
|
+
const updated = [];
|
|
514
|
+
const added = [];
|
|
515
|
+
const updatedPreview = [];
|
|
516
|
+
const existingPayeeMap = new Map<string, string>();
|
|
517
|
+
|
|
518
|
+
const {
|
|
519
|
+
payeesToCreate,
|
|
520
|
+
transactionsStep1,
|
|
521
|
+
transactionsStep2,
|
|
522
|
+
transactionsStep3,
|
|
523
|
+
} = await matchTransactions(
|
|
524
|
+
acctId,
|
|
525
|
+
transactions,
|
|
526
|
+
isBankSyncAccount,
|
|
527
|
+
strictIdChecking,
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
// Finally, generate & commit the changes
|
|
531
|
+
for (const { trans, subtransactions, match } of transactionsStep3) {
|
|
532
|
+
if (match && !trans.forceAddTransaction) {
|
|
533
|
+
// Skip updating already reconciled (locked) transactions
|
|
534
|
+
if (match.reconciled) {
|
|
535
|
+
updatedPreview.push({ transaction: trans, ignored: true });
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// TODO: change the above sql query to use aql
|
|
540
|
+
const existing = {
|
|
541
|
+
...match,
|
|
542
|
+
cleared: match.cleared === 1,
|
|
543
|
+
date: db.fromDateRepr(match.date),
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
// Update the transaction
|
|
547
|
+
const updates = {
|
|
548
|
+
imported_id: trans.imported_id || null,
|
|
549
|
+
payee: existing.payee || trans.payee || null,
|
|
550
|
+
category: existing.category || trans.category || null,
|
|
551
|
+
imported_payee: trans.imported_payee || null,
|
|
552
|
+
notes: existing.notes || trans.notes || null,
|
|
553
|
+
cleared: existing.cleared || trans.cleared || false,
|
|
554
|
+
raw_synced_data:
|
|
555
|
+
existing.raw_synced_data ?? trans.raw_synced_data ?? null,
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
if (updateDates && trans.date) {
|
|
559
|
+
updates['date'] = trans.date;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const fieldsToMarkUpdated = Object.keys(updates).filter(k => {
|
|
563
|
+
// do not mark raw_synced_data if it's gone from falsy to falsy
|
|
564
|
+
if (!existing.raw_synced_data && !trans.raw_synced_data) {
|
|
565
|
+
return k !== 'raw_synced_data';
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return true;
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
if (hasFieldsChanged(existing, updates, fieldsToMarkUpdated)) {
|
|
572
|
+
updated.push({ id: existing.id, ...updates });
|
|
573
|
+
if (!existingPayeeMap.has(existing.payee)) {
|
|
574
|
+
const payee = await db.getPayee(existing.payee);
|
|
575
|
+
existingPayeeMap.set(existing.payee, payee?.name);
|
|
576
|
+
}
|
|
577
|
+
existing.payee_name = existingPayeeMap.get(existing.payee);
|
|
578
|
+
existing.amount = integerToAmount(existing.amount);
|
|
579
|
+
updatedPreview.push({ transaction: trans, existing });
|
|
580
|
+
} else {
|
|
581
|
+
updatedPreview.push({ transaction: trans, ignored: true });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const clearedUpdated = existing.cleared !== updates.cleared;
|
|
585
|
+
const dateUpdated =
|
|
586
|
+
updateDates && trans.date && existing.date !== trans.date;
|
|
587
|
+
|
|
588
|
+
if (existing.is_parent && (clearedUpdated || dateUpdated)) {
|
|
589
|
+
const children = await db.all<Pick<db.DbViewTransaction, 'id'>>(
|
|
590
|
+
'SELECT id FROM v_transactions WHERE parent_id = ?',
|
|
591
|
+
[existing.id],
|
|
592
|
+
);
|
|
593
|
+
const childUpdates = {};
|
|
594
|
+
|
|
595
|
+
if (clearedUpdated) {
|
|
596
|
+
childUpdates['cleared'] = updates.cleared;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (dateUpdated) {
|
|
600
|
+
childUpdates['date'] = trans.date;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
for (const child of children) {
|
|
604
|
+
updated.push({ id: child.id, ...childUpdates });
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
} else if (trans.tombstone) {
|
|
608
|
+
if (isPreview) {
|
|
609
|
+
updatedPreview.push({
|
|
610
|
+
transaction: trans,
|
|
611
|
+
existing: false,
|
|
612
|
+
tombstone: true,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
} else {
|
|
616
|
+
// Insert a new transaction
|
|
617
|
+
const { forceAddTransaction: _forceAddTransaction, ...newTrans } = trans;
|
|
618
|
+
const finalTransaction = {
|
|
619
|
+
...newTrans,
|
|
620
|
+
id: uuidv4(),
|
|
621
|
+
category: trans.category || null,
|
|
622
|
+
cleared: trans.cleared ?? defaultCleared,
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
if (subtransactions && subtransactions.length > 0) {
|
|
626
|
+
added.push(...makeSplitTransaction(finalTransaction, subtransactions));
|
|
627
|
+
} else {
|
|
628
|
+
added.push(finalTransaction);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Maintain the sort order of the server
|
|
634
|
+
const now = Date.now();
|
|
635
|
+
added.forEach((t, index) => {
|
|
636
|
+
t.sort_order ??= now - index;
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
if (!isPreview) {
|
|
640
|
+
await createNewPayees(payeesToCreate, [...added, ...updated]);
|
|
641
|
+
await batchUpdateTransactions({ added, updated });
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
logger.log('Debug data for the operations:', {
|
|
645
|
+
transactionsStep1,
|
|
646
|
+
transactionsStep2,
|
|
647
|
+
transactionsStep3,
|
|
648
|
+
added,
|
|
649
|
+
updated,
|
|
650
|
+
updatedPreview,
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
return {
|
|
654
|
+
added: added.map(trans => trans.id),
|
|
655
|
+
updated: updated.map(trans => trans.id),
|
|
656
|
+
updatedPreview,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export async function matchTransactions(
|
|
661
|
+
acctId,
|
|
662
|
+
transactions,
|
|
663
|
+
isBankSyncAccount = false,
|
|
664
|
+
strictIdChecking = true,
|
|
665
|
+
) {
|
|
666
|
+
logger.log('Performing transaction reconciliation matching');
|
|
667
|
+
|
|
668
|
+
const reimportDeleted = await aqlQuery(
|
|
669
|
+
q('preferences')
|
|
670
|
+
.filter({ id: `sync-reimport-deleted-${acctId}` })
|
|
671
|
+
.select('value'),
|
|
672
|
+
).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true');
|
|
673
|
+
|
|
674
|
+
const hasMatched = new Set();
|
|
675
|
+
|
|
676
|
+
const transactionNormalization = isBankSyncAccount
|
|
677
|
+
? normalizeBankSyncTransactions
|
|
678
|
+
: normalizeTransactions;
|
|
679
|
+
|
|
680
|
+
const { normalized, payeesToCreate } = await transactionNormalization(
|
|
681
|
+
transactions,
|
|
682
|
+
acctId,
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
// The first pass runs the rules, and preps data for fuzzy matching
|
|
686
|
+
const accounts: db.DbAccount[] = await db.getAccounts();
|
|
687
|
+
const accountsMap = new Map(accounts.map(account => [account.id, account]));
|
|
688
|
+
|
|
689
|
+
const transactionsStep1 = [];
|
|
690
|
+
for (const {
|
|
691
|
+
payee_name,
|
|
692
|
+
trans: originalTrans,
|
|
693
|
+
subtransactions,
|
|
694
|
+
} of normalized) {
|
|
695
|
+
// Run the rules
|
|
696
|
+
const trans = await runRules(originalTrans, accountsMap);
|
|
697
|
+
|
|
698
|
+
let match = null;
|
|
699
|
+
let fuzzyDataset = null;
|
|
700
|
+
|
|
701
|
+
// First, match with an existing transaction's imported_id. This
|
|
702
|
+
// is the highest fidelity match and should always be attempted
|
|
703
|
+
// first.
|
|
704
|
+
if (trans.imported_id) {
|
|
705
|
+
const table = reimportDeleted
|
|
706
|
+
? 'v_transactions'
|
|
707
|
+
: 'v_transactions_internal';
|
|
708
|
+
match = await db.first<db.DbTransaction>(
|
|
709
|
+
`SELECT * FROM ${table} WHERE imported_id = ? AND account = ?`,
|
|
710
|
+
[trans.imported_id, acctId],
|
|
711
|
+
);
|
|
712
|
+
|
|
713
|
+
if (match) {
|
|
714
|
+
hasMatched.add(match.id);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// If it didn't match, query data needed for fuzzy matching
|
|
719
|
+
if (!match) {
|
|
720
|
+
// Fuzzy matching looks 7 days ahead and 7 days back. This
|
|
721
|
+
// needs to select all fields that need to be read from the
|
|
722
|
+
// matched transaction. See the final pass below for the needed
|
|
723
|
+
// fields.
|
|
724
|
+
const sevenDaysBefore = db.toDateRepr(monthUtils.subDays(trans.date, 7));
|
|
725
|
+
const sevenDaysAfter = db.toDateRepr(monthUtils.addDays(trans.date, 7));
|
|
726
|
+
// strictIdChecking has the added behaviour of only matching on transactions with no import ID
|
|
727
|
+
// if the transaction being imported has an import ID.
|
|
728
|
+
if (strictIdChecking) {
|
|
729
|
+
fuzzyDataset = await db.all<
|
|
730
|
+
Pick<
|
|
731
|
+
db.DbViewTransaction,
|
|
732
|
+
| 'id'
|
|
733
|
+
| 'is_parent'
|
|
734
|
+
| 'date'
|
|
735
|
+
| 'imported_id'
|
|
736
|
+
| 'payee'
|
|
737
|
+
| 'imported_payee'
|
|
738
|
+
| 'category'
|
|
739
|
+
| 'notes'
|
|
740
|
+
| 'reconciled'
|
|
741
|
+
| 'cleared'
|
|
742
|
+
| 'amount'
|
|
743
|
+
>
|
|
744
|
+
>(
|
|
745
|
+
`SELECT id, is_parent, date, imported_id, payee, imported_payee, category, notes, reconciled, cleared, amount
|
|
746
|
+
FROM v_transactions
|
|
747
|
+
WHERE
|
|
748
|
+
-- If both ids are set, and we didn't match earlier then skip dedup
|
|
749
|
+
(imported_id IS NULL OR ? IS NULL)
|
|
750
|
+
AND date >= ? AND date <= ? AND amount = ?
|
|
751
|
+
AND account = ?`,
|
|
752
|
+
[
|
|
753
|
+
trans.imported_id || null,
|
|
754
|
+
sevenDaysBefore,
|
|
755
|
+
sevenDaysAfter,
|
|
756
|
+
trans.amount || 0,
|
|
757
|
+
acctId,
|
|
758
|
+
],
|
|
759
|
+
);
|
|
760
|
+
} else {
|
|
761
|
+
fuzzyDataset = await db.all<
|
|
762
|
+
Pick<
|
|
763
|
+
db.DbViewTransaction,
|
|
764
|
+
| 'id'
|
|
765
|
+
| 'is_parent'
|
|
766
|
+
| 'date'
|
|
767
|
+
| 'imported_id'
|
|
768
|
+
| 'payee'
|
|
769
|
+
| 'imported_payee'
|
|
770
|
+
| 'category'
|
|
771
|
+
| 'notes'
|
|
772
|
+
| 'reconciled'
|
|
773
|
+
| 'cleared'
|
|
774
|
+
| 'amount'
|
|
775
|
+
>
|
|
776
|
+
>(
|
|
777
|
+
`SELECT id, is_parent, date, imported_id, payee, imported_payee, category, notes, reconciled, cleared, amount
|
|
778
|
+
FROM v_transactions
|
|
779
|
+
WHERE date >= ? AND date <= ? AND amount = ? AND account = ?`,
|
|
780
|
+
[sevenDaysBefore, sevenDaysAfter, trans.amount || 0, acctId],
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Sort the matched transactions according to the distance from the original
|
|
785
|
+
// transactions date. i.e. if the original transaction is in 21-02-2024 and
|
|
786
|
+
// the matched transactions are: 20-02-2024, 21-02-2024, 29-02-2024 then
|
|
787
|
+
// the resulting data-set should be: 21-02-2024, 20-02-2024, 29-02-2024.
|
|
788
|
+
fuzzyDataset = fuzzyDataset.sort((a, b) => {
|
|
789
|
+
const aDistance = Math.abs(
|
|
790
|
+
dateFns.differenceInMilliseconds(
|
|
791
|
+
dateFns.parseISO(trans.date),
|
|
792
|
+
dateFns.parseISO(db.fromDateRepr(a.date)),
|
|
793
|
+
),
|
|
794
|
+
);
|
|
795
|
+
const bDistance = Math.abs(
|
|
796
|
+
dateFns.differenceInMilliseconds(
|
|
797
|
+
dateFns.parseISO(trans.date),
|
|
798
|
+
dateFns.parseISO(db.fromDateRepr(b.date)),
|
|
799
|
+
),
|
|
800
|
+
);
|
|
801
|
+
return aDistance > bDistance ? 1 : -1;
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
transactionsStep1.push({
|
|
806
|
+
payee_name,
|
|
807
|
+
trans,
|
|
808
|
+
subtransactions: trans.subtransactions || subtransactions,
|
|
809
|
+
match,
|
|
810
|
+
fuzzyDataset,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Next, do the fuzzy matching. This first pass matches based on the
|
|
815
|
+
// payee id. We do this in multiple passes so that higher fidelity
|
|
816
|
+
// matching always happens first, i.e. a transaction should match
|
|
817
|
+
// match with low fidelity if a later transaction is going to match
|
|
818
|
+
// the same one with high fidelity.
|
|
819
|
+
const transactionsStep2 = transactionsStep1.map(data => {
|
|
820
|
+
if (!data.match && data.fuzzyDataset) {
|
|
821
|
+
// Try to find one where the payees match.
|
|
822
|
+
const match = data.fuzzyDataset.find(
|
|
823
|
+
row => !hasMatched.has(row.id) && data.trans.payee === row.payee,
|
|
824
|
+
);
|
|
825
|
+
|
|
826
|
+
if (match) {
|
|
827
|
+
hasMatched.add(match.id);
|
|
828
|
+
return { ...data, match };
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return data;
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// The final fuzzy matching pass. This is the lowest fidelity
|
|
835
|
+
// matching: it just find the first transaction that hasn't been
|
|
836
|
+
// matched yet. Remember the dataset only contains transactions
|
|
837
|
+
// around the same date with the same amount.
|
|
838
|
+
const transactionsStep3 = transactionsStep2.map(data => {
|
|
839
|
+
if (!data.match && data.fuzzyDataset) {
|
|
840
|
+
const match = data.fuzzyDataset.find(row => !hasMatched.has(row.id));
|
|
841
|
+
if (match) {
|
|
842
|
+
hasMatched.add(match.id);
|
|
843
|
+
return { ...data, match };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return data;
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
return {
|
|
850
|
+
payeesToCreate,
|
|
851
|
+
transactionsStep1,
|
|
852
|
+
transactionsStep2,
|
|
853
|
+
transactionsStep3,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// This is similar to `reconcileTransactions` except much simpler: it
|
|
858
|
+
// does not try to match any transactions. It just adds them
|
|
859
|
+
export async function addTransactions(
|
|
860
|
+
acctId,
|
|
861
|
+
transactions,
|
|
862
|
+
{ runTransfers = true, learnCategories = false } = {},
|
|
863
|
+
) {
|
|
864
|
+
const added = [];
|
|
865
|
+
|
|
866
|
+
const { normalized, payeesToCreate } = await normalizeTransactions(
|
|
867
|
+
transactions,
|
|
868
|
+
acctId,
|
|
869
|
+
{ rawPayeeName: true },
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
const accounts: db.DbAccount[] = await db.getAccounts();
|
|
873
|
+
const accountsMap = new Map(accounts.map(account => [account.id, account]));
|
|
874
|
+
|
|
875
|
+
for (const { trans: originalTrans, subtransactions } of normalized) {
|
|
876
|
+
// Run the rules
|
|
877
|
+
const trans = await runRules(originalTrans, accountsMap);
|
|
878
|
+
|
|
879
|
+
const finalTransaction = {
|
|
880
|
+
id: uuidv4(),
|
|
881
|
+
...trans,
|
|
882
|
+
account: acctId,
|
|
883
|
+
cleared: trans.cleared != null ? trans.cleared : true,
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
// Add split transactions if they are given
|
|
887
|
+
const updatedSubtransactions =
|
|
888
|
+
finalTransaction.subtransactions || subtransactions;
|
|
889
|
+
if (updatedSubtransactions && updatedSubtransactions.length > 0) {
|
|
890
|
+
added.push(
|
|
891
|
+
...makeSplitTransaction(finalTransaction, updatedSubtransactions),
|
|
892
|
+
);
|
|
893
|
+
} else {
|
|
894
|
+
added.push(finalTransaction);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
await createNewPayees(payeesToCreate, added);
|
|
899
|
+
|
|
900
|
+
let newTransactions;
|
|
901
|
+
if (runTransfers || learnCategories) {
|
|
902
|
+
const res = await batchUpdateTransactions({
|
|
903
|
+
added,
|
|
904
|
+
learnCategories,
|
|
905
|
+
runTransfers,
|
|
906
|
+
});
|
|
907
|
+
newTransactions = res.added.map(t => t.id);
|
|
908
|
+
} else {
|
|
909
|
+
await batchMessages(async () => {
|
|
910
|
+
newTransactions = await Promise.all(
|
|
911
|
+
added.map(async trans => db.insertTransaction(trans)),
|
|
912
|
+
);
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
return newTransactions;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
async function processBankSyncDownload(
|
|
919
|
+
download,
|
|
920
|
+
id,
|
|
921
|
+
acctRow,
|
|
922
|
+
initialSync = false,
|
|
923
|
+
customStartingBalance?: number,
|
|
924
|
+
customStartingDate?: string,
|
|
925
|
+
) {
|
|
926
|
+
// If syncing an account from sync source it must not use strictIdChecking. This allows
|
|
927
|
+
// the fuzzy search to match transactions where the import IDs are different. It is a known quirk
|
|
928
|
+
// that account sync sources can give two different transaction IDs even though it's the same transaction.
|
|
929
|
+
const useStrictIdChecking = !acctRow.account_sync_source;
|
|
930
|
+
|
|
931
|
+
const importTransactions = await aqlQuery(
|
|
932
|
+
q('preferences')
|
|
933
|
+
.filter({ id: `sync-import-transactions-${id}` })
|
|
934
|
+
.select('value'),
|
|
935
|
+
).then(data => String(data?.data?.[0]?.value ?? 'true') === 'true');
|
|
936
|
+
|
|
937
|
+
const updateDates = await aqlQuery(
|
|
938
|
+
q('preferences')
|
|
939
|
+
.filter({ id: `sync-update-dates-${id}` })
|
|
940
|
+
.select('value'),
|
|
941
|
+
).then(data => String(data?.data?.[0]?.value ?? 'false') === 'true');
|
|
942
|
+
|
|
943
|
+
/** Starting balance is actually the current balance of the account. */
|
|
944
|
+
const {
|
|
945
|
+
transactions: originalTransactions,
|
|
946
|
+
startingBalance: currentBalance,
|
|
947
|
+
} = download;
|
|
948
|
+
|
|
949
|
+
if (initialSync) {
|
|
950
|
+
const { transactions } = download;
|
|
951
|
+
let balanceToUse = currentBalance;
|
|
952
|
+
|
|
953
|
+
// Use custom starting balance if provided, otherwise calculate it
|
|
954
|
+
if (customStartingBalance !== undefined) {
|
|
955
|
+
balanceToUse = customStartingBalance;
|
|
956
|
+
} else if (acctRow.account_sync_source === 'simpleFin') {
|
|
957
|
+
const previousBalance = transactions.reduce((total, trans) => {
|
|
958
|
+
return (
|
|
959
|
+
total - parseInt(trans.transactionAmount.amount.replace('.', ''))
|
|
960
|
+
);
|
|
961
|
+
}, currentBalance);
|
|
962
|
+
balanceToUse = previousBalance;
|
|
963
|
+
} else if (acctRow.account_sync_source === 'pluggyai') {
|
|
964
|
+
const currentBalance = download.startingBalance;
|
|
965
|
+
const previousBalance = transactions.reduce(
|
|
966
|
+
(total, trans) => total - trans.transactionAmount.amount * 100,
|
|
967
|
+
currentBalance,
|
|
968
|
+
);
|
|
969
|
+
balanceToUse = Math.round(previousBalance);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const oldestTransaction = transactions[transactions.length - 1];
|
|
973
|
+
|
|
974
|
+
// Use custom starting date if provided, otherwise use oldest transaction date or current day
|
|
975
|
+
let startingBalanceDate: string;
|
|
976
|
+
if (customStartingDate) {
|
|
977
|
+
startingBalanceDate = customStartingDate;
|
|
978
|
+
} else if (transactions.length > 0) {
|
|
979
|
+
startingBalanceDate = oldestTransaction.date;
|
|
980
|
+
} else {
|
|
981
|
+
startingBalanceDate = monthUtils.currentDay();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const payee = await getStartingBalancePayee();
|
|
985
|
+
|
|
986
|
+
return runMutator(async () => {
|
|
987
|
+
const initialId = await db.insertTransaction({
|
|
988
|
+
account: id,
|
|
989
|
+
amount: balanceToUse,
|
|
990
|
+
category: acctRow.offbudget === 0 ? payee.category : null,
|
|
991
|
+
payee: payee.id,
|
|
992
|
+
date: startingBalanceDate,
|
|
993
|
+
cleared: true,
|
|
994
|
+
starting_balance_flag: true,
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
const result = await reconcileTransactions(
|
|
998
|
+
id,
|
|
999
|
+
transactions,
|
|
1000
|
+
true,
|
|
1001
|
+
useStrictIdChecking,
|
|
1002
|
+
false,
|
|
1003
|
+
true,
|
|
1004
|
+
updateDates,
|
|
1005
|
+
);
|
|
1006
|
+
return {
|
|
1007
|
+
...result,
|
|
1008
|
+
added: [initialId, ...result.added],
|
|
1009
|
+
};
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const transactions = originalTransactions.map(trans => ({
|
|
1014
|
+
...trans,
|
|
1015
|
+
account: id,
|
|
1016
|
+
}));
|
|
1017
|
+
|
|
1018
|
+
return runMutator(async () => {
|
|
1019
|
+
const result = await reconcileTransactions(
|
|
1020
|
+
id,
|
|
1021
|
+
importTransactions ? transactions : [],
|
|
1022
|
+
true,
|
|
1023
|
+
useStrictIdChecking,
|
|
1024
|
+
false,
|
|
1025
|
+
true,
|
|
1026
|
+
updateDates,
|
|
1027
|
+
);
|
|
1028
|
+
|
|
1029
|
+
if (currentBalance != null) {
|
|
1030
|
+
await updateAccountBalance(id, currentBalance);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return result;
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
export async function syncAccount(
|
|
1038
|
+
userId: string | undefined,
|
|
1039
|
+
userKey: string | undefined,
|
|
1040
|
+
id: string,
|
|
1041
|
+
acctId: string,
|
|
1042
|
+
bankId: string,
|
|
1043
|
+
customStartingDate?: string,
|
|
1044
|
+
customStartingBalance?: number,
|
|
1045
|
+
) {
|
|
1046
|
+
const acctRow = await db.select('accounts', id);
|
|
1047
|
+
|
|
1048
|
+
const syncStartDate =
|
|
1049
|
+
customStartingDate ?? (await getAccountSyncStartDate(id));
|
|
1050
|
+
const oldestTransaction = await getAccountOldestTransaction(id);
|
|
1051
|
+
const newAccount = oldestTransaction == null;
|
|
1052
|
+
|
|
1053
|
+
let download;
|
|
1054
|
+
if (acctRow.account_sync_source === 'simpleFin') {
|
|
1055
|
+
download = await downloadSimpleFinTransactions(acctId, syncStartDate);
|
|
1056
|
+
} else if (acctRow.account_sync_source === 'pluggyai') {
|
|
1057
|
+
download = await downloadPluggyAiTransactions(acctId, syncStartDate);
|
|
1058
|
+
} else if (acctRow.account_sync_source === 'goCardless') {
|
|
1059
|
+
download = await downloadGoCardlessTransactions(
|
|
1060
|
+
userId,
|
|
1061
|
+
userKey,
|
|
1062
|
+
acctId,
|
|
1063
|
+
bankId,
|
|
1064
|
+
syncStartDate,
|
|
1065
|
+
newAccount,
|
|
1066
|
+
);
|
|
1067
|
+
} else {
|
|
1068
|
+
throw new Error(
|
|
1069
|
+
`Unrecognized bank-sync provider: ${acctRow.account_sync_source}`,
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return processBankSyncDownload(
|
|
1074
|
+
download,
|
|
1075
|
+
id,
|
|
1076
|
+
acctRow,
|
|
1077
|
+
newAccount,
|
|
1078
|
+
customStartingBalance,
|
|
1079
|
+
customStartingDate,
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
export async function simpleFinBatchSync(
|
|
1084
|
+
accounts: Array<Pick<AccountEntity, 'id' | 'account_id'>>,
|
|
1085
|
+
) {
|
|
1086
|
+
const startDates = await Promise.all(
|
|
1087
|
+
accounts.map(async a => getAccountSyncStartDate(a.id)),
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
const res = await downloadSimpleFinTransactions(
|
|
1091
|
+
accounts.map(a => a.account_id),
|
|
1092
|
+
startDates,
|
|
1093
|
+
);
|
|
1094
|
+
|
|
1095
|
+
if (!res) {
|
|
1096
|
+
return accounts.map(account => ({
|
|
1097
|
+
accountId: account.id,
|
|
1098
|
+
res: {
|
|
1099
|
+
error_type: 'NO_DATA',
|
|
1100
|
+
error_code: 'NO_DATA',
|
|
1101
|
+
},
|
|
1102
|
+
}));
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const promises = [];
|
|
1106
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
1107
|
+
const account = accounts[i];
|
|
1108
|
+
const download = res[account.account_id];
|
|
1109
|
+
|
|
1110
|
+
if (!download || Object.keys(download).length === 0) {
|
|
1111
|
+
promises.push(
|
|
1112
|
+
Promise.resolve({
|
|
1113
|
+
accountId: account.id,
|
|
1114
|
+
res: {
|
|
1115
|
+
error_type: 'ACCOUNT_MISSING',
|
|
1116
|
+
error_code: 'ACCOUNT_MISSING',
|
|
1117
|
+
},
|
|
1118
|
+
}),
|
|
1119
|
+
);
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
const acctRow = await db.select('accounts', account.id);
|
|
1124
|
+
const oldestTransaction = await getAccountOldestTransaction(account.id);
|
|
1125
|
+
const newAccount = oldestTransaction == null;
|
|
1126
|
+
|
|
1127
|
+
if (download.error_code) {
|
|
1128
|
+
promises.push(
|
|
1129
|
+
Promise.resolve({
|
|
1130
|
+
accountId: account.id,
|
|
1131
|
+
res: download,
|
|
1132
|
+
}),
|
|
1133
|
+
);
|
|
1134
|
+
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (!download.transactions) {
|
|
1139
|
+
promises.push(
|
|
1140
|
+
Promise.resolve({
|
|
1141
|
+
accountId: account.id,
|
|
1142
|
+
res: {
|
|
1143
|
+
error_type: 'ACCOUNT_MISSING',
|
|
1144
|
+
error_code: 'ACCOUNT_MISSING',
|
|
1145
|
+
},
|
|
1146
|
+
}),
|
|
1147
|
+
);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
promises.push(
|
|
1152
|
+
processBankSyncDownload(download, account.id, acctRow, newAccount)
|
|
1153
|
+
.then(res => ({
|
|
1154
|
+
accountId: account.id,
|
|
1155
|
+
res,
|
|
1156
|
+
}))
|
|
1157
|
+
.catch(err => ({
|
|
1158
|
+
accountId: account.id,
|
|
1159
|
+
res: {
|
|
1160
|
+
error_type: err?.category || 'INTERNAL_ERROR',
|
|
1161
|
+
error_code: err?.code || 'INTERNAL_ERROR',
|
|
1162
|
+
},
|
|
1163
|
+
})),
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return await Promise.all(promises);
|
|
1168
|
+
}
|