@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,1193 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
|
|
4
|
+
import { logger } from '../../platform/server/log';
|
|
5
|
+
import * as monthUtils from '../../shared/months';
|
|
6
|
+
import { q } from '../../shared/query';
|
|
7
|
+
import { groupBy, sortByKey } from '../../shared/util';
|
|
8
|
+
import type { RecurConfig, RecurPattern, RuleEntity } from '../../types/models';
|
|
9
|
+
import { send } from '../main-app';
|
|
10
|
+
import { ruleModel } from '../transactions/transaction-rules';
|
|
11
|
+
|
|
12
|
+
import type {
|
|
13
|
+
Budget,
|
|
14
|
+
Payee,
|
|
15
|
+
ScheduledSubtransaction,
|
|
16
|
+
ScheduledTransaction,
|
|
17
|
+
Subtransaction,
|
|
18
|
+
Transaction,
|
|
19
|
+
} from './ynab5-types';
|
|
20
|
+
|
|
21
|
+
const MAX_RETRY = 20;
|
|
22
|
+
|
|
23
|
+
function normalizeError(e: unknown): string {
|
|
24
|
+
if (e instanceof Error) {
|
|
25
|
+
return e.message;
|
|
26
|
+
}
|
|
27
|
+
if (typeof e === 'string') {
|
|
28
|
+
return e;
|
|
29
|
+
}
|
|
30
|
+
return String(e);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type FlaggedTransaction = Pick<
|
|
34
|
+
Transaction | ScheduledTransaction,
|
|
35
|
+
'flag_name' | 'flag_color' | 'deleted'
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
const flagColorMap: Record<string, string | null> = {
|
|
39
|
+
red: '#FF6666',
|
|
40
|
+
orange: '#F57C00',
|
|
41
|
+
yellow: '#FBC02D',
|
|
42
|
+
green: '#689F38',
|
|
43
|
+
blue: '#1976D2',
|
|
44
|
+
purple: '#512DA8',
|
|
45
|
+
null: null,
|
|
46
|
+
'': null,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function equalsIgnoreCase(stringa: string, stringb: string): boolean {
|
|
50
|
+
return (
|
|
51
|
+
stringa.localeCompare(stringb, undefined, {
|
|
52
|
+
sensitivity: 'base',
|
|
53
|
+
}) === 0
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findByNameIgnoreCase<T extends { name: string }>(
|
|
58
|
+
categories: T[],
|
|
59
|
+
name: string,
|
|
60
|
+
) {
|
|
61
|
+
return categories.find(cat => equalsIgnoreCase(cat.name, name));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findIdByName<T extends { id: string; name: string }>(
|
|
65
|
+
categories: Array<T>,
|
|
66
|
+
name: string,
|
|
67
|
+
) {
|
|
68
|
+
return findByNameIgnoreCase<T>(categories, name)?.id;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function amountFromYnab(amount: number) {
|
|
72
|
+
// YNAB multiplies amount by 1000 and Actual by 100
|
|
73
|
+
// so, this function divides by 10
|
|
74
|
+
return Math.round(amount / 10);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getDayOfMonth(date: string) {
|
|
78
|
+
return monthUtils.parseDate(date).getDate();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getYnabMonthlyPatterns(dateFirst: string): RecurPattern[] | undefined {
|
|
82
|
+
if (getDayOfMonth(dateFirst) !== 31) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return [
|
|
87
|
+
{
|
|
88
|
+
type: 'day',
|
|
89
|
+
value: -1,
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Use Actual's "specific days" to avoid drifting every 15 days.
|
|
95
|
+
// This approximates YNAB's "second occurrence is 15 days after the chosen day"
|
|
96
|
+
// by locking to two day-of-month values.
|
|
97
|
+
function getYnabTwiceMonthlyPatterns(dateFirst: string): RecurPattern[] {
|
|
98
|
+
const firstDay = getDayOfMonth(dateFirst);
|
|
99
|
+
// Compute the second occurrence as 15 calendar days after the first.
|
|
100
|
+
const secondDay = getDayOfMonth(monthUtils.addDays(dateFirst, 15));
|
|
101
|
+
|
|
102
|
+
return [
|
|
103
|
+
{ type: 'day', value: firstDay === 31 ? -1 : firstDay },
|
|
104
|
+
{ type: 'day', value: secondDay === 31 ? -1 : secondDay },
|
|
105
|
+
];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function mapYnabFrequency(
|
|
109
|
+
frequency: string,
|
|
110
|
+
dateFirst: string,
|
|
111
|
+
): {
|
|
112
|
+
frequency: RecurConfig['frequency'];
|
|
113
|
+
interval?: number;
|
|
114
|
+
patterns?: RecurPattern[];
|
|
115
|
+
} {
|
|
116
|
+
switch (frequency) {
|
|
117
|
+
case 'daily':
|
|
118
|
+
return { frequency: 'daily' };
|
|
119
|
+
case 'weekly':
|
|
120
|
+
return { frequency: 'weekly' };
|
|
121
|
+
case 'monthly':
|
|
122
|
+
return {
|
|
123
|
+
frequency: 'monthly',
|
|
124
|
+
patterns: getYnabMonthlyPatterns(dateFirst),
|
|
125
|
+
};
|
|
126
|
+
case 'yearly':
|
|
127
|
+
return { frequency: 'yearly' };
|
|
128
|
+
case 'everyOtherWeek':
|
|
129
|
+
return { frequency: 'weekly', interval: 2 };
|
|
130
|
+
case 'every4Weeks':
|
|
131
|
+
return { frequency: 'weekly', interval: 4 };
|
|
132
|
+
case 'everyOtherMonth':
|
|
133
|
+
return {
|
|
134
|
+
frequency: 'monthly',
|
|
135
|
+
interval: 2,
|
|
136
|
+
patterns: getYnabMonthlyPatterns(dateFirst),
|
|
137
|
+
};
|
|
138
|
+
case 'every3Months':
|
|
139
|
+
return {
|
|
140
|
+
frequency: 'monthly',
|
|
141
|
+
interval: 3,
|
|
142
|
+
patterns: getYnabMonthlyPatterns(dateFirst),
|
|
143
|
+
};
|
|
144
|
+
case 'every4Months':
|
|
145
|
+
return {
|
|
146
|
+
frequency: 'monthly',
|
|
147
|
+
interval: 4,
|
|
148
|
+
patterns: getYnabMonthlyPatterns(dateFirst),
|
|
149
|
+
};
|
|
150
|
+
case 'everyOtherYear':
|
|
151
|
+
return { frequency: 'yearly', interval: 2 };
|
|
152
|
+
case 'twiceAMonth': {
|
|
153
|
+
return {
|
|
154
|
+
frequency: 'monthly',
|
|
155
|
+
patterns: getYnabTwiceMonthlyPatterns(dateFirst),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
case 'twiceAYear': {
|
|
159
|
+
return {
|
|
160
|
+
frequency: 'monthly',
|
|
161
|
+
interval: 6,
|
|
162
|
+
patterns: getYnabMonthlyPatterns(dateFirst),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
default:
|
|
166
|
+
throw new Error(`Unsupported scheduled frequency: ${frequency}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getScheduleDateValue(
|
|
171
|
+
scheduled: ScheduledTransaction,
|
|
172
|
+
): RecurConfig | string {
|
|
173
|
+
const dateFirst = scheduled.date_first;
|
|
174
|
+
const frequency = scheduled.frequency;
|
|
175
|
+
|
|
176
|
+
if (frequency === 'never') {
|
|
177
|
+
return scheduled.date_next;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const mapped = mapYnabFrequency(frequency, dateFirst);
|
|
181
|
+
return {
|
|
182
|
+
frequency: mapped.frequency,
|
|
183
|
+
interval: mapped.interval,
|
|
184
|
+
patterns: mapped.patterns,
|
|
185
|
+
skipWeekend: false,
|
|
186
|
+
weekendSolveMode: 'after',
|
|
187
|
+
endMode: 'never',
|
|
188
|
+
start: dateFirst,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function getFlaggedTransactions(data: Budget): FlaggedTransaction[] {
|
|
193
|
+
return [...data.transactions, ...data.scheduled_transactions];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function getFlagTag(
|
|
197
|
+
transaction: FlaggedTransaction,
|
|
198
|
+
flagNameConflicts: Set<string>,
|
|
199
|
+
): string {
|
|
200
|
+
const tagName = transaction.flag_name?.trim() ?? '';
|
|
201
|
+
const colorKey = transaction.flag_color?.trim() ?? '';
|
|
202
|
+
|
|
203
|
+
if (tagName.length === 0) {
|
|
204
|
+
return colorKey.length > 0 ? `#${colorKey}` : '';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (flagNameConflicts.has(tagName)) {
|
|
208
|
+
return `#${tagName}-${colorKey}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return `#${tagName}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getFlagNameConflicts(data: Budget): Set<string> {
|
|
215
|
+
const colorsByName = new Map<string, Set<string>>();
|
|
216
|
+
const flaggedTransactions = getFlaggedTransactions(data);
|
|
217
|
+
|
|
218
|
+
for (const transaction of flaggedTransactions) {
|
|
219
|
+
if (transaction.deleted) {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const tagName = transaction.flag_name?.trim() ?? '';
|
|
224
|
+
const colorKey = transaction.flag_color?.trim() ?? '';
|
|
225
|
+
if (tagName.length === 0 || !flagColorMap[colorKey]) {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let colors = colorsByName.get(tagName);
|
|
230
|
+
if (!colors) {
|
|
231
|
+
colors = new Set();
|
|
232
|
+
colorsByName.set(tagName, colors);
|
|
233
|
+
}
|
|
234
|
+
colors.add(colorKey);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const conflicts = new Set<string>();
|
|
238
|
+
colorsByName.forEach((colors, name) => {
|
|
239
|
+
if (colors.size > 1) {
|
|
240
|
+
conflicts.add(name);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return conflicts;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildTransactionNotes(
|
|
248
|
+
transaction: Transaction | ScheduledTransaction,
|
|
249
|
+
flagNameConflicts: Set<string>,
|
|
250
|
+
): string | null {
|
|
251
|
+
const normalizedMemo = transaction.memo?.trim() ?? '';
|
|
252
|
+
const tagText = getFlagTag(transaction, flagNameConflicts);
|
|
253
|
+
const notes = `${normalizedMemo} ${tagText}`.trim();
|
|
254
|
+
return notes.length > 0 ? notes : null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function buildRuleUpdate(
|
|
258
|
+
rule: RuleEntity,
|
|
259
|
+
actions: RuleEntity['actions'],
|
|
260
|
+
): RuleEntity {
|
|
261
|
+
return {
|
|
262
|
+
id: rule.id,
|
|
263
|
+
stage: rule.stage ?? null,
|
|
264
|
+
conditionsOp: rule.conditionsOp ?? 'and',
|
|
265
|
+
conditions: rule.conditions,
|
|
266
|
+
actions,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function importAccounts(data: Budget, entityIdMap: Map<string, string>) {
|
|
271
|
+
return Promise.all(
|
|
272
|
+
data.accounts.map(async account => {
|
|
273
|
+
if (!account.deleted) {
|
|
274
|
+
const id = await send('api/account-create', {
|
|
275
|
+
account: {
|
|
276
|
+
name: account.name,
|
|
277
|
+
offbudget: account.on_budget ? false : true,
|
|
278
|
+
closed: account.closed,
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
entityIdMap.set(account.id, id);
|
|
282
|
+
}
|
|
283
|
+
}),
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function importCategories(
|
|
288
|
+
data: Budget,
|
|
289
|
+
entityIdMap: Map<string, string>,
|
|
290
|
+
) {
|
|
291
|
+
// Hidden categories are put in its own group by YNAB,
|
|
292
|
+
// so it's already handled.
|
|
293
|
+
|
|
294
|
+
const categories = await send('api/categories-get', {
|
|
295
|
+
grouped: false,
|
|
296
|
+
});
|
|
297
|
+
const incomeCatId = findIdByName(categories, 'Income');
|
|
298
|
+
const ynabIncomeCategories = ['To be Budgeted', 'Inflow: Ready to Assign'];
|
|
299
|
+
|
|
300
|
+
function checkSpecialCat(cat) {
|
|
301
|
+
if (
|
|
302
|
+
cat.category_group_id ===
|
|
303
|
+
findIdByName(data.category_groups, 'Internal Master Category')
|
|
304
|
+
) {
|
|
305
|
+
if (
|
|
306
|
+
ynabIncomeCategories.some(ynabIncomeCategory =>
|
|
307
|
+
equalsIgnoreCase(cat.name, ynabIncomeCategory),
|
|
308
|
+
)
|
|
309
|
+
) {
|
|
310
|
+
return 'income';
|
|
311
|
+
} else {
|
|
312
|
+
return 'internal';
|
|
313
|
+
}
|
|
314
|
+
} else if (
|
|
315
|
+
cat.category_group_id ===
|
|
316
|
+
findIdByName(data.category_groups, 'Credit Card Payments')
|
|
317
|
+
) {
|
|
318
|
+
return 'creditCard';
|
|
319
|
+
} else if (
|
|
320
|
+
cat.category_group_id === findIdByName(data.category_groups, 'Income')
|
|
321
|
+
) {
|
|
322
|
+
return 'income';
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
// Can't be done in parallel to have
|
|
326
|
+
// correct sort order.
|
|
327
|
+
|
|
328
|
+
async function createCategoryGroupWithUniqueName(params: {
|
|
329
|
+
name: string;
|
|
330
|
+
is_income: boolean;
|
|
331
|
+
hidden: boolean;
|
|
332
|
+
}) {
|
|
333
|
+
const baseName = params.hidden ? `${params.name} (hidden)` : params.name;
|
|
334
|
+
let count = 0;
|
|
335
|
+
|
|
336
|
+
while (true) {
|
|
337
|
+
const name = count === 0 ? baseName : `${baseName} (${count})`;
|
|
338
|
+
try {
|
|
339
|
+
const id = await send('api/category-group-create', {
|
|
340
|
+
group: { ...params, name },
|
|
341
|
+
});
|
|
342
|
+
return { id, name };
|
|
343
|
+
} catch (e) {
|
|
344
|
+
if (count >= MAX_RETRY) {
|
|
345
|
+
const errorMsg = normalizeError(e);
|
|
346
|
+
throw Error('Unable to create category group: ' + errorMsg);
|
|
347
|
+
}
|
|
348
|
+
count += 1;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function createCategoryWithUniqueName(params: {
|
|
354
|
+
name: string;
|
|
355
|
+
group_id: string;
|
|
356
|
+
hidden: boolean;
|
|
357
|
+
}) {
|
|
358
|
+
const baseName = params.hidden ? `${params.name} (hidden)` : params.name;
|
|
359
|
+
let count = 0;
|
|
360
|
+
|
|
361
|
+
while (true) {
|
|
362
|
+
const name = count === 0 ? baseName : `${baseName} (${count})`;
|
|
363
|
+
try {
|
|
364
|
+
const id = await send('api/category-create', {
|
|
365
|
+
category: { ...params, name },
|
|
366
|
+
});
|
|
367
|
+
return { id, name };
|
|
368
|
+
} catch (e) {
|
|
369
|
+
if (count >= MAX_RETRY) {
|
|
370
|
+
const errorMsg = normalizeError(e);
|
|
371
|
+
throw Error('Unable to create category: ' + errorMsg);
|
|
372
|
+
}
|
|
373
|
+
count += 1;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const group of data.category_groups) {
|
|
379
|
+
if (!group.deleted) {
|
|
380
|
+
let groupId: string;
|
|
381
|
+
// Ignores internal category and credit cards
|
|
382
|
+
if (
|
|
383
|
+
!equalsIgnoreCase(group.name, 'Internal Master Category') &&
|
|
384
|
+
!equalsIgnoreCase(group.name, 'Credit Card Payments') &&
|
|
385
|
+
!equalsIgnoreCase(group.name, 'Hidden Categories') &&
|
|
386
|
+
!equalsIgnoreCase(group.name, 'Income')
|
|
387
|
+
) {
|
|
388
|
+
const createdGroup = await createCategoryGroupWithUniqueName({
|
|
389
|
+
name: group.name,
|
|
390
|
+
is_income: false,
|
|
391
|
+
hidden: group.hidden,
|
|
392
|
+
});
|
|
393
|
+
groupId = createdGroup.id;
|
|
394
|
+
entityIdMap.set(group.id, groupId);
|
|
395
|
+
if (group.note) {
|
|
396
|
+
void send('notes-save', {
|
|
397
|
+
id: groupId,
|
|
398
|
+
note: group.note,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (equalsIgnoreCase(group.name, 'Income')) {
|
|
404
|
+
groupId = incomeCatId;
|
|
405
|
+
entityIdMap.set(group.id, groupId);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const cats = data.categories.filter(
|
|
409
|
+
cat => cat.category_group_id === group.id,
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
for (const cat of cats.reverse()) {
|
|
413
|
+
if (!cat.deleted) {
|
|
414
|
+
// Handles special categories. Starting balance is a payee
|
|
415
|
+
// in YNAB so it's handled in importTransactions
|
|
416
|
+
switch (checkSpecialCat(cat)) {
|
|
417
|
+
case 'income': {
|
|
418
|
+
// doesn't create new category, only assigns id
|
|
419
|
+
const id = incomeCatId;
|
|
420
|
+
entityIdMap.set(cat.id, id);
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
case 'creditCard': // ignores it
|
|
424
|
+
case 'internal': // uncategorized is ignored too, handled by actual
|
|
425
|
+
break;
|
|
426
|
+
default: {
|
|
427
|
+
if (!groupId) {
|
|
428
|
+
break;
|
|
429
|
+
}
|
|
430
|
+
const createdCategory = await createCategoryWithUniqueName({
|
|
431
|
+
name: cat.name,
|
|
432
|
+
group_id: groupId,
|
|
433
|
+
hidden: cat.hidden,
|
|
434
|
+
});
|
|
435
|
+
entityIdMap.set(cat.id, createdCategory.id);
|
|
436
|
+
if (cat.note) {
|
|
437
|
+
void send('notes-save', {
|
|
438
|
+
id: createdCategory.id,
|
|
439
|
+
note: cat.note,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function importPayees(data: Budget, entityIdMap: Map<string, string>) {
|
|
451
|
+
return Promise.all(
|
|
452
|
+
data.payees.map(async payee => {
|
|
453
|
+
if (!payee.deleted) {
|
|
454
|
+
const id = await send('api/payee-create', {
|
|
455
|
+
payee: { name: payee.name },
|
|
456
|
+
});
|
|
457
|
+
entityIdMap.set(payee.id, id);
|
|
458
|
+
}
|
|
459
|
+
}),
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function importPayeeLocations(
|
|
464
|
+
data: Budget,
|
|
465
|
+
entityIdMap: Map<string, string>,
|
|
466
|
+
) {
|
|
467
|
+
// If no payee locations data provided, skip import
|
|
468
|
+
if (!data?.payee_locations) {
|
|
469
|
+
logger.log('No payee locations data provided, skipping...');
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const payeeLocations = data.payee_locations;
|
|
474
|
+
|
|
475
|
+
for (const location of payeeLocations) {
|
|
476
|
+
// Skip deleted locations
|
|
477
|
+
if (location.deleted) {
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Get the mapped payee ID
|
|
482
|
+
const actualPayeeId = entityIdMap.get(location.payee_id);
|
|
483
|
+
if (!actualPayeeId) {
|
|
484
|
+
logger.log(`Skipping location for unknown payee: ${location.payee_id}`);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Validate latitude/longitude before attempting import
|
|
489
|
+
const latitude = parseFloat(location.latitude);
|
|
490
|
+
const longitude = parseFloat(location.longitude);
|
|
491
|
+
|
|
492
|
+
if (isNaN(latitude) || isNaN(longitude)) {
|
|
493
|
+
logger.log(
|
|
494
|
+
`Skipping location with invalid coordinates for payee ${actualPayeeId}: lat=${location.latitude}, lng=${location.longitude}`,
|
|
495
|
+
);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
// Create the payee location in Actual
|
|
501
|
+
await send('payee-location-create', {
|
|
502
|
+
payeeId: actualPayeeId,
|
|
503
|
+
latitude,
|
|
504
|
+
longitude,
|
|
505
|
+
});
|
|
506
|
+
} catch (error) {
|
|
507
|
+
const errorMessage =
|
|
508
|
+
error instanceof Error
|
|
509
|
+
? error.message
|
|
510
|
+
: String(error ?? 'Unknown error');
|
|
511
|
+
logger.error(
|
|
512
|
+
`Failed to import location for payee ${actualPayeeId} at (${latitude}, ${longitude}): ${errorMessage}`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
async function importFlagsAsTags(
|
|
519
|
+
data: Budget,
|
|
520
|
+
flagNameConflicts: Set<string>,
|
|
521
|
+
): Promise<void> {
|
|
522
|
+
const tagsToCreate = new Map<string, string | null>();
|
|
523
|
+
const flaggedTransactions = getFlaggedTransactions(data);
|
|
524
|
+
|
|
525
|
+
for (const transaction of flaggedTransactions) {
|
|
526
|
+
if (transaction.deleted) {
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const tagName = transaction.flag_name?.trim() ?? '';
|
|
531
|
+
const colorKey = transaction.flag_color?.trim() ?? '';
|
|
532
|
+
const tagColor = flagColorMap[colorKey] ?? null;
|
|
533
|
+
|
|
534
|
+
if (!tagColor) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (tagName.length === 0) {
|
|
539
|
+
if (!tagsToCreate.has(colorKey)) {
|
|
540
|
+
tagsToCreate.set(colorKey, tagColor);
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const mappedName = flagNameConflicts.has(tagName)
|
|
546
|
+
? `${tagName}-${colorKey}`
|
|
547
|
+
: tagName;
|
|
548
|
+
|
|
549
|
+
if (!tagsToCreate.has(mappedName)) {
|
|
550
|
+
tagsToCreate.set(mappedName, tagColor);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (tagsToCreate.size === 0) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
await Promise.all(
|
|
559
|
+
[...tagsToCreate.entries()].map(async ([tag, color]) => {
|
|
560
|
+
await send('tags-create', {
|
|
561
|
+
tag,
|
|
562
|
+
color,
|
|
563
|
+
description: 'Imported from YNAB',
|
|
564
|
+
});
|
|
565
|
+
}),
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function importTransactions(
|
|
570
|
+
data: Budget,
|
|
571
|
+
entityIdMap: Map<string, string>,
|
|
572
|
+
flagNameConflicts: Set<string>,
|
|
573
|
+
) {
|
|
574
|
+
const payees = await send('api/payees-get');
|
|
575
|
+
const categories = await send('api/categories-get', {
|
|
576
|
+
grouped: false,
|
|
577
|
+
});
|
|
578
|
+
const incomeCatId = findIdByName(categories, 'Income');
|
|
579
|
+
const startingBalanceCatId = findIdByName(categories, 'Starting Balances'); //better way to do it?
|
|
580
|
+
|
|
581
|
+
const startingPayeeYNAB = findIdByName(data.payees, 'Starting Balance');
|
|
582
|
+
|
|
583
|
+
const transactionsGrouped = groupBy(data.transactions, 'account_id');
|
|
584
|
+
const subtransactionsGrouped = groupBy(
|
|
585
|
+
data.subtransactions,
|
|
586
|
+
'transaction_id',
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
const payeesByTransferAcct = payees
|
|
590
|
+
.filter(payee => payee?.transfer_acct)
|
|
591
|
+
.map(payee => [payee.transfer_acct, payee] as [string, Payee]);
|
|
592
|
+
const payeeTransferAcctHashMap = new Map<string, Payee>(payeesByTransferAcct);
|
|
593
|
+
const orphanTransferMap = new Map<string, Transaction[]>();
|
|
594
|
+
const orphanSubtransfer = [] as Subtransaction[];
|
|
595
|
+
const orphanSubtransferTrxId = [] as string[];
|
|
596
|
+
const orphanSubtransferAcctIdByTrxIdMap = new Map<string, string>();
|
|
597
|
+
const orphanSubtransferDateByTrxIdMap = new Map<string, string>();
|
|
598
|
+
|
|
599
|
+
// Go ahead and generate ids for all of the transactions so we can
|
|
600
|
+
// reliably resolve transfers
|
|
601
|
+
// Also identify orphan transfer transactions and subtransactions.
|
|
602
|
+
for (const transaction of data.subtransactions) {
|
|
603
|
+
entityIdMap.set(transaction.id, uuidv4());
|
|
604
|
+
|
|
605
|
+
if (transaction.transfer_account_id) {
|
|
606
|
+
orphanSubtransfer.push(transaction);
|
|
607
|
+
orphanSubtransferTrxId.push(transaction.transaction_id);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for (const transaction of data.transactions) {
|
|
612
|
+
entityIdMap.set(transaction.id, uuidv4());
|
|
613
|
+
|
|
614
|
+
if (
|
|
615
|
+
transaction.transfer_account_id &&
|
|
616
|
+
!transaction.transfer_transaction_id
|
|
617
|
+
) {
|
|
618
|
+
const key =
|
|
619
|
+
transaction.account_id + '#' + transaction.transfer_account_id;
|
|
620
|
+
if (!orphanTransferMap.has(key)) {
|
|
621
|
+
orphanTransferMap.set(key, [transaction]);
|
|
622
|
+
} else {
|
|
623
|
+
orphanTransferMap.get(key).push(transaction);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (orphanSubtransferTrxId.includes(transaction.id)) {
|
|
628
|
+
orphanSubtransferAcctIdByTrxIdMap.set(
|
|
629
|
+
transaction.id,
|
|
630
|
+
transaction.account_id,
|
|
631
|
+
);
|
|
632
|
+
orphanSubtransferDateByTrxIdMap.set(transaction.id, transaction.date);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// Compute link between subtransaction transfers and orphaned transaction
|
|
637
|
+
// transfers. The goal is to match each transfer subtransaction to the related
|
|
638
|
+
// transfer transaction according to the accounts, date, amount and memo.
|
|
639
|
+
const orphanSubtransferMap = orphanSubtransfer.reduce(
|
|
640
|
+
(map, subtransaction) => {
|
|
641
|
+
const key =
|
|
642
|
+
subtransaction.transfer_account_id +
|
|
643
|
+
'#' +
|
|
644
|
+
orphanSubtransferAcctIdByTrxIdMap.get(subtransaction.transaction_id);
|
|
645
|
+
if (!map.has(key)) {
|
|
646
|
+
map.set(key, [subtransaction]);
|
|
647
|
+
} else {
|
|
648
|
+
map.get(key).push(subtransaction);
|
|
649
|
+
}
|
|
650
|
+
return map;
|
|
651
|
+
},
|
|
652
|
+
new Map<string, Subtransaction[]>(),
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
// The comparator will be used to order transfer transactions and their
|
|
656
|
+
// corresponding tranfer subtransaction in two aligned list. Hopefully
|
|
657
|
+
// for every list index in the transactions list, the related subtransaction
|
|
658
|
+
// will be at the same index.
|
|
659
|
+
function orphanTransferComparator(
|
|
660
|
+
a: Transaction | Subtransaction,
|
|
661
|
+
b: Transaction | Subtransaction,
|
|
662
|
+
) {
|
|
663
|
+
// a and b can be a Transaction (having a date attribute) or a
|
|
664
|
+
// Subtransaction (missing that date attribute)
|
|
665
|
+
|
|
666
|
+
const date_a =
|
|
667
|
+
'date' in a
|
|
668
|
+
? a.date
|
|
669
|
+
: orphanSubtransferDateByTrxIdMap.get(a.transaction_id);
|
|
670
|
+
const date_b =
|
|
671
|
+
'date' in b
|
|
672
|
+
? b.date
|
|
673
|
+
: orphanSubtransferDateByTrxIdMap.get(b.transaction_id);
|
|
674
|
+
// A transaction and the related subtransaction have inverted amounts.
|
|
675
|
+
// To have those in the same order, the subtransaction has to be reversed
|
|
676
|
+
// to have the same amount.
|
|
677
|
+
const amount_a = 'date' in a ? a.amount : -a.amount;
|
|
678
|
+
const amount_b = 'date' in b ? b.amount : -b.amount;
|
|
679
|
+
|
|
680
|
+
// Transaction are ordered first by date, then by amount, and lastly by memo
|
|
681
|
+
if (date_a > date_b) return 1;
|
|
682
|
+
if (date_a < date_b) return -1;
|
|
683
|
+
if (amount_a > amount_b) return 1;
|
|
684
|
+
if (amount_a < amount_b) return -1;
|
|
685
|
+
if (a.memo > b.memo) return 1;
|
|
686
|
+
if (a.memo < b.memo) return -1;
|
|
687
|
+
return 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const orphanTrxIdSubtrxIdMap = new Map<string, string>();
|
|
691
|
+
orphanTransferMap.forEach((transactions, key) => {
|
|
692
|
+
const subtransactions = orphanSubtransferMap.get(key);
|
|
693
|
+
if (subtransactions) {
|
|
694
|
+
transactions.sort(orphanTransferComparator);
|
|
695
|
+
subtransactions.sort(orphanTransferComparator);
|
|
696
|
+
|
|
697
|
+
// Iterate on the two sorted lists transactions and subtransactions and
|
|
698
|
+
// find matching data to identify the related transaction ids.
|
|
699
|
+
let transactionIdx = 0;
|
|
700
|
+
let subtransactionIdx = 0;
|
|
701
|
+
do {
|
|
702
|
+
switch (
|
|
703
|
+
orphanTransferComparator(
|
|
704
|
+
transactions[transactionIdx],
|
|
705
|
+
subtransactions[subtransactionIdx],
|
|
706
|
+
)
|
|
707
|
+
) {
|
|
708
|
+
case 0:
|
|
709
|
+
// The current list indexes are matching: the transaction and
|
|
710
|
+
// subtransaction are related (same date, amount and memo)
|
|
711
|
+
orphanTrxIdSubtrxIdMap.set(
|
|
712
|
+
transactions[transactionIdx].id,
|
|
713
|
+
entityIdMap.get(subtransactions[subtransactionIdx].id),
|
|
714
|
+
);
|
|
715
|
+
orphanTrxIdSubtrxIdMap.set(
|
|
716
|
+
subtransactions[subtransactionIdx].id,
|
|
717
|
+
entityIdMap.get(transactions[transactionIdx].id),
|
|
718
|
+
);
|
|
719
|
+
transactionIdx++;
|
|
720
|
+
subtransactionIdx++;
|
|
721
|
+
break;
|
|
722
|
+
case -1:
|
|
723
|
+
// The current list indexes are not matching:
|
|
724
|
+
// The current transaction is "smaller" than the current subtransaction
|
|
725
|
+
// (earlier date, smaller amount, memo value sorted before)
|
|
726
|
+
// So we advance to the next transaction and see if it match with
|
|
727
|
+
// the current subtransaction
|
|
728
|
+
transactionIdx++;
|
|
729
|
+
break;
|
|
730
|
+
case 1:
|
|
731
|
+
// Inverse of the previous case:
|
|
732
|
+
// The current subtransaction is "smaller" than the current transaction
|
|
733
|
+
// So we advance to the next subtransaction
|
|
734
|
+
subtransactionIdx++;
|
|
735
|
+
break;
|
|
736
|
+
default:
|
|
737
|
+
throw new Error(`Unrecognized orphan transfer comparator result`);
|
|
738
|
+
}
|
|
739
|
+
} while (
|
|
740
|
+
transactionIdx < transactions.length &&
|
|
741
|
+
subtransactionIdx < subtransactions.length
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
await Promise.all(
|
|
747
|
+
[...transactionsGrouped.keys()].map(async accountId => {
|
|
748
|
+
const transactions = transactionsGrouped.get(accountId);
|
|
749
|
+
|
|
750
|
+
const toImport = transactions
|
|
751
|
+
.map(transaction => {
|
|
752
|
+
if (transaction.deleted) {
|
|
753
|
+
return null;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const subtransactions = subtransactionsGrouped.get(transaction.id);
|
|
757
|
+
|
|
758
|
+
// Add transaction
|
|
759
|
+
const newTransaction = {
|
|
760
|
+
id: entityIdMap.get(transaction.id),
|
|
761
|
+
account: entityIdMap.get(transaction.account_id),
|
|
762
|
+
date: transaction.date,
|
|
763
|
+
amount: amountFromYnab(transaction.amount),
|
|
764
|
+
category: entityIdMap.get(transaction.category_id) || null,
|
|
765
|
+
cleared: ['cleared', 'reconciled'].includes(transaction.cleared),
|
|
766
|
+
reconciled: transaction.cleared === 'reconciled',
|
|
767
|
+
notes: buildTransactionNotes(transaction, flagNameConflicts),
|
|
768
|
+
imported_id: transaction.import_id || null,
|
|
769
|
+
transfer_id:
|
|
770
|
+
entityIdMap.get(transaction.transfer_transaction_id) ||
|
|
771
|
+
orphanTrxIdSubtrxIdMap.get(transaction.id) ||
|
|
772
|
+
null,
|
|
773
|
+
subtransactions: subtransactions
|
|
774
|
+
? subtransactions.map(subtrans => {
|
|
775
|
+
return {
|
|
776
|
+
id: entityIdMap.get(subtrans.id),
|
|
777
|
+
amount: amountFromYnab(subtrans.amount),
|
|
778
|
+
category: entityIdMap.get(subtrans.category_id) || null,
|
|
779
|
+
notes: subtrans.memo,
|
|
780
|
+
transfer_id:
|
|
781
|
+
orphanTrxIdSubtrxIdMap.get(subtrans.id) || null,
|
|
782
|
+
payee: null,
|
|
783
|
+
imported_payee: null,
|
|
784
|
+
};
|
|
785
|
+
})
|
|
786
|
+
: null,
|
|
787
|
+
payee: null,
|
|
788
|
+
imported_payee: null,
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
// Handle transactions and subtransactions payee
|
|
792
|
+
function transactionPayeeUpdate(
|
|
793
|
+
trx: Transaction | Subtransaction,
|
|
794
|
+
newTrx,
|
|
795
|
+
fallbackPayeeId?: string | null,
|
|
796
|
+
) {
|
|
797
|
+
if (trx.transfer_account_id) {
|
|
798
|
+
const mappedTransferAccountId = entityIdMap.get(
|
|
799
|
+
trx.transfer_account_id,
|
|
800
|
+
);
|
|
801
|
+
newTrx.payee = payeeTransferAcctHashMap.get(
|
|
802
|
+
mappedTransferAccountId,
|
|
803
|
+
)?.id;
|
|
804
|
+
} else if (trx.payee_id) {
|
|
805
|
+
newTrx.payee = entityIdMap.get(trx.payee_id);
|
|
806
|
+
newTrx.imported_payee = data.payees.find(
|
|
807
|
+
p => !p.deleted && p.id === trx.payee_id,
|
|
808
|
+
)?.name;
|
|
809
|
+
} else if (fallbackPayeeId) {
|
|
810
|
+
newTrx.payee = fallbackPayeeId;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
transactionPayeeUpdate(transaction, newTransaction);
|
|
815
|
+
if (newTransaction.subtransactions) {
|
|
816
|
+
subtransactions.forEach(subtrans => {
|
|
817
|
+
const newSubtransaction = newTransaction.subtransactions.find(
|
|
818
|
+
newSubtrans => newSubtrans.id === entityIdMap.get(subtrans.id),
|
|
819
|
+
);
|
|
820
|
+
transactionPayeeUpdate(
|
|
821
|
+
subtrans,
|
|
822
|
+
newSubtransaction,
|
|
823
|
+
newTransaction.payee,
|
|
824
|
+
);
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Handle starting balances
|
|
829
|
+
if (
|
|
830
|
+
transaction.payee_id === startingPayeeYNAB &&
|
|
831
|
+
entityIdMap.get(transaction.category_id) === incomeCatId
|
|
832
|
+
) {
|
|
833
|
+
newTransaction.category = startingBalanceCatId;
|
|
834
|
+
newTransaction.payee = null;
|
|
835
|
+
}
|
|
836
|
+
return newTransaction;
|
|
837
|
+
})
|
|
838
|
+
.filter(x => x);
|
|
839
|
+
|
|
840
|
+
await send('api/transactions-add', {
|
|
841
|
+
accountId: entityIdMap.get(accountId),
|
|
842
|
+
transactions: toImport,
|
|
843
|
+
learnCategories: true,
|
|
844
|
+
runTransfers: false,
|
|
845
|
+
});
|
|
846
|
+
}),
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function importScheduledTransactions(
|
|
851
|
+
data: Budget,
|
|
852
|
+
entityIdMap: Map<string, string>,
|
|
853
|
+
flagNameConflicts: Set<string>,
|
|
854
|
+
) {
|
|
855
|
+
const scheduledTransactions = data.scheduled_transactions;
|
|
856
|
+
const scheduledSubtransactionsGrouped = groupBy(
|
|
857
|
+
data.scheduled_subtransactions,
|
|
858
|
+
'scheduled_transaction_id',
|
|
859
|
+
);
|
|
860
|
+
if (scheduledTransactions.length === 0) {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const payees = await send('api/payees-get');
|
|
865
|
+
const payeesByTransferAcct = payees
|
|
866
|
+
.filter(payee => payee?.transfer_acct)
|
|
867
|
+
.map(payee => [payee.transfer_acct, payee] as [string, Payee]);
|
|
868
|
+
const payeeTransferAcctHashMap = new Map<string, Payee>(payeesByTransferAcct);
|
|
869
|
+
const scheduleCategoryMap = new Map<string, string>();
|
|
870
|
+
const scheduleSplitsMap = new Map<string, ScheduledSubtransaction[]>();
|
|
871
|
+
const schedulePayeeMap = new Map<string, string>();
|
|
872
|
+
|
|
873
|
+
async function createScheduleWithUniqueName(params: {
|
|
874
|
+
name: string;
|
|
875
|
+
posts_transaction: boolean;
|
|
876
|
+
payee: string;
|
|
877
|
+
account: string;
|
|
878
|
+
amount: number;
|
|
879
|
+
amountOp: 'is';
|
|
880
|
+
date: RecurConfig | string;
|
|
881
|
+
}) {
|
|
882
|
+
const baseName = params.name;
|
|
883
|
+
let count = 1;
|
|
884
|
+
|
|
885
|
+
while (true) {
|
|
886
|
+
try {
|
|
887
|
+
return await send('api/schedule-create', {
|
|
888
|
+
...params,
|
|
889
|
+
name: params.name,
|
|
890
|
+
});
|
|
891
|
+
} catch (e) {
|
|
892
|
+
if (count >= MAX_RETRY) {
|
|
893
|
+
const errorMsg = normalizeError(e);
|
|
894
|
+
throw Error(errorMsg);
|
|
895
|
+
}
|
|
896
|
+
params.name = `${baseName} (${count})`;
|
|
897
|
+
count += 1;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
async function getRuleForSchedule(
|
|
903
|
+
scheduleId: string,
|
|
904
|
+
): Promise<RuleEntity | null> {
|
|
905
|
+
const { data: ruleId } = (await send('api/query', {
|
|
906
|
+
query: q('schedules')
|
|
907
|
+
.filter({ id: scheduleId })
|
|
908
|
+
.calculate('rule')
|
|
909
|
+
.serialize(),
|
|
910
|
+
})) as { data: string | null };
|
|
911
|
+
if (!ruleId) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const { data: ruleData } = (await send('api/query', {
|
|
916
|
+
query: q('rules').filter({ id: ruleId }).select('*').serialize(),
|
|
917
|
+
})) as { data: Array<Record<string, unknown>> };
|
|
918
|
+
const ruleRow = ruleData?.[0];
|
|
919
|
+
if (!ruleRow) {
|
|
920
|
+
return null;
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
return ruleModel.toJS(ruleRow);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
for (const scheduled of scheduledTransactions) {
|
|
927
|
+
if (scheduled.deleted) {
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const mappedAccountId = entityIdMap.get(scheduled.account_id);
|
|
932
|
+
if (!mappedAccountId) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const scheduleDate = getScheduleDateValue(scheduled);
|
|
937
|
+
|
|
938
|
+
let mappedPayeeId: string | undefined;
|
|
939
|
+
if (scheduled.transfer_account_id) {
|
|
940
|
+
const mappedTransferAccountId = entityIdMap.get(
|
|
941
|
+
scheduled.transfer_account_id,
|
|
942
|
+
);
|
|
943
|
+
mappedPayeeId = mappedTransferAccountId
|
|
944
|
+
? payeeTransferAcctHashMap.get(mappedTransferAccountId)?.id
|
|
945
|
+
: undefined;
|
|
946
|
+
} else if (scheduled.payee_id) {
|
|
947
|
+
mappedPayeeId = entityIdMap.get(scheduled.payee_id);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
if (!mappedPayeeId) {
|
|
951
|
+
continue;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const scheduleId = await createScheduleWithUniqueName({
|
|
955
|
+
name: scheduled.memo,
|
|
956
|
+
posts_transaction: false,
|
|
957
|
+
payee: mappedPayeeId,
|
|
958
|
+
account: mappedAccountId,
|
|
959
|
+
amount: amountFromYnab(scheduled.amount),
|
|
960
|
+
amountOp: 'is',
|
|
961
|
+
date: scheduleDate,
|
|
962
|
+
});
|
|
963
|
+
schedulePayeeMap.set(scheduleId, mappedPayeeId);
|
|
964
|
+
|
|
965
|
+
const scheduleNotes = buildTransactionNotes(scheduled, flagNameConflicts);
|
|
966
|
+
if (scheduleNotes) {
|
|
967
|
+
const rule = await getRuleForSchedule(scheduleId);
|
|
968
|
+
if (rule) {
|
|
969
|
+
const actions = rule.actions ? [...rule.actions] : [];
|
|
970
|
+
actions.push({
|
|
971
|
+
op: 'set',
|
|
972
|
+
field: 'notes',
|
|
973
|
+
value: scheduleNotes,
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
await send('api/rule-update', {
|
|
977
|
+
rule: buildRuleUpdate(rule, actions),
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const scheduledSubtransactions =
|
|
983
|
+
scheduledSubtransactionsGrouped
|
|
984
|
+
.get(scheduled.id)
|
|
985
|
+
?.filter(subtransaction => !subtransaction.deleted) || [];
|
|
986
|
+
|
|
987
|
+
if (scheduledSubtransactions.length > 0) {
|
|
988
|
+
scheduleSplitsMap.set(scheduleId, scheduledSubtransactions);
|
|
989
|
+
} else if (!scheduled.transfer_account_id && scheduled.category_id) {
|
|
990
|
+
const mappedCategoryId = entityIdMap.get(scheduled.category_id);
|
|
991
|
+
if (mappedCategoryId) {
|
|
992
|
+
scheduleCategoryMap.set(scheduleId, mappedCategoryId);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
if (scheduleCategoryMap.size > 0 || scheduleSplitsMap.size > 0) {
|
|
998
|
+
for (const [scheduleId, categoryId] of scheduleCategoryMap.entries()) {
|
|
999
|
+
const rule = await getRuleForSchedule(scheduleId);
|
|
1000
|
+
if (!rule) {
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const actions = rule.actions ? [...rule.actions] : [];
|
|
1005
|
+
actions.push({
|
|
1006
|
+
op: 'set',
|
|
1007
|
+
field: 'category',
|
|
1008
|
+
value: categoryId,
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
await send('api/rule-update', {
|
|
1012
|
+
rule: buildRuleUpdate(rule, actions),
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
for (const [scheduleId, subtransactions] of scheduleSplitsMap.entries()) {
|
|
1017
|
+
const rule = await getRuleForSchedule(scheduleId);
|
|
1018
|
+
if (!rule) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const actions = rule.actions ? [...rule.actions] : [];
|
|
1023
|
+
const parentPayeeId = schedulePayeeMap.get(scheduleId);
|
|
1024
|
+
|
|
1025
|
+
subtransactions.forEach((subtransaction, index) => {
|
|
1026
|
+
const splitIndex = index + 1;
|
|
1027
|
+
|
|
1028
|
+
actions.push({
|
|
1029
|
+
op: 'set-split-amount',
|
|
1030
|
+
value: amountFromYnab(subtransaction.amount),
|
|
1031
|
+
options: { splitIndex, method: 'fixed-amount' },
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
if (subtransaction.memo) {
|
|
1035
|
+
actions.push({
|
|
1036
|
+
op: 'set',
|
|
1037
|
+
field: 'notes',
|
|
1038
|
+
value: subtransaction.memo,
|
|
1039
|
+
options: { splitIndex },
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (subtransaction.transfer_account_id) {
|
|
1044
|
+
const mappedTransferAccountId = entityIdMap.get(
|
|
1045
|
+
subtransaction.transfer_account_id,
|
|
1046
|
+
);
|
|
1047
|
+
const transferPayeeId = mappedTransferAccountId
|
|
1048
|
+
? payeeTransferAcctHashMap.get(mappedTransferAccountId)?.id
|
|
1049
|
+
: undefined;
|
|
1050
|
+
if (transferPayeeId) {
|
|
1051
|
+
actions.push({
|
|
1052
|
+
op: 'set',
|
|
1053
|
+
field: 'payee',
|
|
1054
|
+
value: transferPayeeId,
|
|
1055
|
+
options: { splitIndex },
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
} else if (subtransaction.payee_id) {
|
|
1059
|
+
const mappedPayeeId = entityIdMap.get(subtransaction.payee_id);
|
|
1060
|
+
if (mappedPayeeId) {
|
|
1061
|
+
actions.push({
|
|
1062
|
+
op: 'set',
|
|
1063
|
+
field: 'payee',
|
|
1064
|
+
value: mappedPayeeId,
|
|
1065
|
+
options: { splitIndex },
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
} else if (parentPayeeId) {
|
|
1069
|
+
actions.push({
|
|
1070
|
+
op: 'set',
|
|
1071
|
+
field: 'payee',
|
|
1072
|
+
value: parentPayeeId,
|
|
1073
|
+
options: { splitIndex },
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (!subtransaction.transfer_account_id && subtransaction.category_id) {
|
|
1078
|
+
const mappedCategoryId = entityIdMap.get(subtransaction.category_id);
|
|
1079
|
+
if (mappedCategoryId) {
|
|
1080
|
+
actions.push({
|
|
1081
|
+
op: 'set',
|
|
1082
|
+
field: 'category',
|
|
1083
|
+
value: mappedCategoryId,
|
|
1084
|
+
options: { splitIndex },
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
await send('api/rule-update', {
|
|
1091
|
+
rule: buildRuleUpdate(rule, actions),
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async function importBudgets(data: Budget, entityIdMap: Map<string, string>) {
|
|
1098
|
+
// There should be info in the docs to deal with
|
|
1099
|
+
// no credit card category and how YNAB and Actual
|
|
1100
|
+
// handle differently the amount To be Budgeted
|
|
1101
|
+
// i.e. Actual considers the cc debt while YNAB doesn't
|
|
1102
|
+
//
|
|
1103
|
+
// Also, there could be a way to set rollover using
|
|
1104
|
+
// Deferred Income Subcat and Immediate Income Subcat
|
|
1105
|
+
|
|
1106
|
+
const budgets = sortByKey(data.months, 'month');
|
|
1107
|
+
|
|
1108
|
+
const internalCatIdYnab = findIdByName(
|
|
1109
|
+
data.category_groups,
|
|
1110
|
+
'Internal Master Category',
|
|
1111
|
+
);
|
|
1112
|
+
const creditcardCatIdYnab = findIdByName(
|
|
1113
|
+
data.category_groups,
|
|
1114
|
+
'Credit Card Payments',
|
|
1115
|
+
);
|
|
1116
|
+
|
|
1117
|
+
await send('api/batch-budget-start');
|
|
1118
|
+
try {
|
|
1119
|
+
for (const budget of budgets) {
|
|
1120
|
+
const month = monthUtils.monthFromDate(budget.month);
|
|
1121
|
+
|
|
1122
|
+
await Promise.all(
|
|
1123
|
+
budget.categories.map(async catBudget => {
|
|
1124
|
+
const catId = entityIdMap.get(catBudget.id);
|
|
1125
|
+
const amount = Math.round(catBudget.budgeted / 10);
|
|
1126
|
+
|
|
1127
|
+
if (
|
|
1128
|
+
!catId ||
|
|
1129
|
+
catBudget.category_group_id === internalCatIdYnab ||
|
|
1130
|
+
catBudget.category_group_id === creditcardCatIdYnab
|
|
1131
|
+
) {
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
await send('api/budget-set-amount', {
|
|
1136
|
+
month,
|
|
1137
|
+
categoryId: catId,
|
|
1138
|
+
amount,
|
|
1139
|
+
});
|
|
1140
|
+
}),
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
} finally {
|
|
1144
|
+
await send('api/batch-budget-end');
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
export function parseFile(buffer: Buffer): Budget {
|
|
1149
|
+
let data = JSON.parse(buffer.toString());
|
|
1150
|
+
if (data.data) {
|
|
1151
|
+
data = data.data;
|
|
1152
|
+
}
|
|
1153
|
+
if (data.budget) {
|
|
1154
|
+
data = data.budget;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return data;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export function getBudgetName(_filepath: string, data: Budget) {
|
|
1161
|
+
return data.budget_name || data.name;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export async function doImport(data: Budget) {
|
|
1165
|
+
const entityIdMap = new Map<string, string>();
|
|
1166
|
+
const flagNameConflicts = getFlagNameConflicts(data);
|
|
1167
|
+
|
|
1168
|
+
logger.log('Importing Accounts...');
|
|
1169
|
+
await importAccounts(data, entityIdMap);
|
|
1170
|
+
|
|
1171
|
+
logger.log('Importing Categories...');
|
|
1172
|
+
await importCategories(data, entityIdMap);
|
|
1173
|
+
|
|
1174
|
+
logger.log('Importing Payees...');
|
|
1175
|
+
await importPayees(data, entityIdMap);
|
|
1176
|
+
|
|
1177
|
+
logger.log('Importing Payee Locations...');
|
|
1178
|
+
await importPayeeLocations(data, entityIdMap);
|
|
1179
|
+
|
|
1180
|
+
logger.log('Importing Tags...');
|
|
1181
|
+
await importFlagsAsTags(data, flagNameConflicts);
|
|
1182
|
+
|
|
1183
|
+
logger.log('Importing Transactions...');
|
|
1184
|
+
await importTransactions(data, entityIdMap, flagNameConflicts);
|
|
1185
|
+
|
|
1186
|
+
logger.log('Importing Scheduled Transactions...');
|
|
1187
|
+
await importScheduledTransactions(data, entityIdMap, flagNameConflicts);
|
|
1188
|
+
|
|
1189
|
+
logger.log('Importing Budgets...');
|
|
1190
|
+
await importBudgets(data, entityIdMap);
|
|
1191
|
+
|
|
1192
|
+
logger.log('Setting up...');
|
|
1193
|
+
}
|