@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,58 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import { logger } from '../../platform/server/log';
|
|
3
|
+
import { handlers } from '../main';
|
|
4
|
+
|
|
5
|
+
import { importActual } from './actual';
|
|
6
|
+
import * as YNAB4 from './ynab4';
|
|
7
|
+
import * as YNAB5 from './ynab5';
|
|
8
|
+
|
|
9
|
+
export type ImportableBudgetType = 'ynab4' | 'ynab5' | 'actual';
|
|
10
|
+
|
|
11
|
+
type Importer = {
|
|
12
|
+
parseFile(buffer: Buffer): unknown;
|
|
13
|
+
getBudgetName(filepath: string, data: unknown): string | null;
|
|
14
|
+
doImport(data: unknown): Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const importers: Record<Exclude<ImportableBudgetType, 'actual'>, Importer> = {
|
|
18
|
+
ynab4: YNAB4,
|
|
19
|
+
ynab5: YNAB5,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function handleBudgetImport(
|
|
23
|
+
type: ImportableBudgetType,
|
|
24
|
+
filepath: string,
|
|
25
|
+
buffer: Buffer,
|
|
26
|
+
) {
|
|
27
|
+
if (type === 'actual') {
|
|
28
|
+
return importActual(filepath, buffer);
|
|
29
|
+
}
|
|
30
|
+
const importer = importers[type];
|
|
31
|
+
try {
|
|
32
|
+
let data;
|
|
33
|
+
let budgetName: string;
|
|
34
|
+
try {
|
|
35
|
+
data = importer.parseFile(buffer);
|
|
36
|
+
budgetName = importer.getBudgetName(filepath, data);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
logger.error('failed to parse file', e);
|
|
39
|
+
}
|
|
40
|
+
if (!budgetName) {
|
|
41
|
+
return { error: 'not-' + type };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await handlers['api/start-import']({ budgetName });
|
|
46
|
+
} catch (e) {
|
|
47
|
+
logger.error('failed to start import', e);
|
|
48
|
+
return { error: 'unknown' };
|
|
49
|
+
}
|
|
50
|
+
await importer.doImport(data);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
await handlers['api/abort-import']();
|
|
53
|
+
logger.error('failed to run import', e);
|
|
54
|
+
return { error: 'unknown' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await handlers['api/finish-import']();
|
|
58
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
export type YFull = {
|
|
2
|
+
masterCategories: MasterCategory[];
|
|
3
|
+
payees: Payee[];
|
|
4
|
+
monthlyBudgets: MonthlyBudget[];
|
|
5
|
+
fileMetaData: FileMetaData;
|
|
6
|
+
transactions: Transaction[];
|
|
7
|
+
scheduledTransactions: ScheduledTransaction[];
|
|
8
|
+
// accountMappings: [];
|
|
9
|
+
budgetMetaData: BudgetMetaData;
|
|
10
|
+
accounts: Account[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type MasterCategory = {
|
|
14
|
+
entityType: string;
|
|
15
|
+
expanded: boolean;
|
|
16
|
+
note?: string;
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
deleteable: boolean;
|
|
20
|
+
subCategories?: SubCategory[];
|
|
21
|
+
entityVersion: string;
|
|
22
|
+
entityId: string;
|
|
23
|
+
sortableIndex: number;
|
|
24
|
+
|
|
25
|
+
// speculative
|
|
26
|
+
isTombstone?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type SubCategory = {
|
|
30
|
+
entityType: string;
|
|
31
|
+
name: string;
|
|
32
|
+
note?: string;
|
|
33
|
+
type: string;
|
|
34
|
+
// cachedBalance: null;
|
|
35
|
+
masterCategoryId: string;
|
|
36
|
+
entityVersion: string;
|
|
37
|
+
entityId: string;
|
|
38
|
+
sortableIndex: number;
|
|
39
|
+
|
|
40
|
+
// speculative
|
|
41
|
+
isTombstone?: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type Payee = {
|
|
45
|
+
entityType: string;
|
|
46
|
+
autoFillCategoryId?: string;
|
|
47
|
+
autoFillAmount: number;
|
|
48
|
+
name: string;
|
|
49
|
+
renameConditions?: RenameCondition[];
|
|
50
|
+
autoFillMemo?: string;
|
|
51
|
+
targetAccountId?: string;
|
|
52
|
+
// locations: null;
|
|
53
|
+
enabled: boolean;
|
|
54
|
+
entityVersion: string;
|
|
55
|
+
entityId: string;
|
|
56
|
+
|
|
57
|
+
// speculative
|
|
58
|
+
isTombstone?: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type RenameCondition = {
|
|
62
|
+
entityType: string;
|
|
63
|
+
parentPayeeId: string;
|
|
64
|
+
operator: string;
|
|
65
|
+
operand: string;
|
|
66
|
+
entityVersion: string;
|
|
67
|
+
entityId: string;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type MonthlyBudget = {
|
|
71
|
+
entityType: string;
|
|
72
|
+
monthlySubCategoryBudgets: MonthlySubCategoryBudget[];
|
|
73
|
+
month: string;
|
|
74
|
+
entityVersion: string;
|
|
75
|
+
entityId: string;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type MonthlySubCategoryBudget = {
|
|
79
|
+
entityType: string;
|
|
80
|
+
categoryId: string;
|
|
81
|
+
budgeted: number;
|
|
82
|
+
overspendingHandling?: string | undefined | null;
|
|
83
|
+
entityVersion: string;
|
|
84
|
+
entityId: string;
|
|
85
|
+
parentMonthlyBudgetId: string;
|
|
86
|
+
|
|
87
|
+
// speculative
|
|
88
|
+
isTombstone?: boolean;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export type FileMetaData = {
|
|
92
|
+
entityType: string;
|
|
93
|
+
budgetDataVersion: string;
|
|
94
|
+
currentKnowledge: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type Transaction = {
|
|
98
|
+
entityType: string;
|
|
99
|
+
entityId: string;
|
|
100
|
+
categoryId: string;
|
|
101
|
+
payeeId: string;
|
|
102
|
+
amount: number;
|
|
103
|
+
date: string;
|
|
104
|
+
accountId: string;
|
|
105
|
+
entityVersion: string;
|
|
106
|
+
cleared: string;
|
|
107
|
+
accepted: boolean;
|
|
108
|
+
isTombstone?: boolean;
|
|
109
|
+
memo?: string;
|
|
110
|
+
dateEnteredFromSchedule?: string;
|
|
111
|
+
// speculative:
|
|
112
|
+
subTransactions?: SubTransaction[];
|
|
113
|
+
transferTransactionId?: string;
|
|
114
|
+
targetAccountId?: string;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// speculative, not in the test data
|
|
118
|
+
export type SubTransaction = Omit<Transaction, 'subTransactions'>;
|
|
119
|
+
|
|
120
|
+
export type ScheduledTransaction = {
|
|
121
|
+
entityType: string;
|
|
122
|
+
entityId: string;
|
|
123
|
+
categoryId: string;
|
|
124
|
+
payeeId: string;
|
|
125
|
+
amount: number;
|
|
126
|
+
date: string;
|
|
127
|
+
isTombstone?: boolean;
|
|
128
|
+
accountId: string;
|
|
129
|
+
entityVersion: string;
|
|
130
|
+
memo: string;
|
|
131
|
+
twiceAMonthStartDay: number;
|
|
132
|
+
cleared: string;
|
|
133
|
+
frequency: string;
|
|
134
|
+
accepted: boolean;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type BudgetMetaData = {
|
|
138
|
+
entityType: string;
|
|
139
|
+
strictBudget: string;
|
|
140
|
+
currencyISOSymbol?: string;
|
|
141
|
+
entityVersion: string;
|
|
142
|
+
currencyLocale: string;
|
|
143
|
+
budgetType: string;
|
|
144
|
+
dateLocale: string;
|
|
145
|
+
entityId: string;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type Account = {
|
|
149
|
+
entityType: string;
|
|
150
|
+
// lastReconciledDate: null;
|
|
151
|
+
lastEnteredCheckNumber: number;
|
|
152
|
+
lastReconciledBalance: number;
|
|
153
|
+
accountType: string;
|
|
154
|
+
hidden: boolean;
|
|
155
|
+
sortableIndex: number;
|
|
156
|
+
onBudget: boolean;
|
|
157
|
+
accountName: string;
|
|
158
|
+
entityVersion: string;
|
|
159
|
+
entityId: string;
|
|
160
|
+
|
|
161
|
+
// speculative
|
|
162
|
+
isTombstone?: boolean;
|
|
163
|
+
};
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import AdmZip from 'adm-zip';
|
|
3
|
+
import normalizePathSep from 'slash';
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
|
|
6
|
+
import { logger } from '../../platform/server/log';
|
|
7
|
+
import * as monthUtils from '../../shared/months';
|
|
8
|
+
import { amountToInteger, groupBy, sortByKey } from '../../shared/util';
|
|
9
|
+
import { send } from '../main-app';
|
|
10
|
+
|
|
11
|
+
import type * as YNAB4 from './ynab4-types';
|
|
12
|
+
|
|
13
|
+
// Importer
|
|
14
|
+
|
|
15
|
+
async function importAccounts(
|
|
16
|
+
data: YNAB4.YFull,
|
|
17
|
+
entityIdMap: Map<string, string>,
|
|
18
|
+
) {
|
|
19
|
+
const accounts = sortByKey(data.accounts, 'sortableIndex');
|
|
20
|
+
|
|
21
|
+
return Promise.all(
|
|
22
|
+
accounts.map(async account => {
|
|
23
|
+
if (!account.isTombstone) {
|
|
24
|
+
const id = await send('api/account-create', {
|
|
25
|
+
account: {
|
|
26
|
+
name: account.accountName,
|
|
27
|
+
offbudget: account.onBudget ? false : true,
|
|
28
|
+
closed: account.hidden ? true : false,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
entityIdMap.set(account.entityId, id);
|
|
32
|
+
}
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function importCategories(
|
|
38
|
+
data: YNAB4.YFull,
|
|
39
|
+
entityIdMap: Map<string, string>,
|
|
40
|
+
) {
|
|
41
|
+
const masterCategories = sortByKey(data.masterCategories, 'sortableIndex');
|
|
42
|
+
|
|
43
|
+
await Promise.all(
|
|
44
|
+
masterCategories.map(async masterCategory => {
|
|
45
|
+
if (
|
|
46
|
+
masterCategory.type === 'OUTFLOW' &&
|
|
47
|
+
!masterCategory.isTombstone &&
|
|
48
|
+
masterCategory.subCategories &&
|
|
49
|
+
masterCategory.subCategories.some(cat => !cat.isTombstone)
|
|
50
|
+
) {
|
|
51
|
+
const id = await send('api/category-group-create', {
|
|
52
|
+
group: {
|
|
53
|
+
name: masterCategory.name,
|
|
54
|
+
is_income: false,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
entityIdMap.set(masterCategory.entityId, id);
|
|
58
|
+
if (masterCategory.note) {
|
|
59
|
+
void send('notes-save', {
|
|
60
|
+
id,
|
|
61
|
+
note: masterCategory.note,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (masterCategory.subCategories) {
|
|
66
|
+
const subCategories = sortByKey(
|
|
67
|
+
masterCategory.subCategories,
|
|
68
|
+
'sortableIndex',
|
|
69
|
+
);
|
|
70
|
+
subCategories.reverse();
|
|
71
|
+
|
|
72
|
+
// This can't be done in parallel because sort order depends
|
|
73
|
+
// on insertion order
|
|
74
|
+
for (const category of subCategories) {
|
|
75
|
+
if (!category.isTombstone) {
|
|
76
|
+
let categoryName = category.name;
|
|
77
|
+
|
|
78
|
+
// Hidden categories have the parent category entity id
|
|
79
|
+
// appended to the end of the sub category name.
|
|
80
|
+
// The format is 'MasterCategory ` SubCategory ` entityId'.
|
|
81
|
+
// Remove the id to shorten the name.
|
|
82
|
+
if (masterCategory.name === 'Hidden Categories') {
|
|
83
|
+
const categoryNameParts = categoryName.split(' ` ');
|
|
84
|
+
|
|
85
|
+
// Remove the last part, which is the entityId.
|
|
86
|
+
categoryNameParts.pop();
|
|
87
|
+
|
|
88
|
+
// Join the remaining parts with a slash between them.
|
|
89
|
+
categoryName = categoryNameParts.join('/').trim();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const id = await send('api/category-create', {
|
|
93
|
+
category: {
|
|
94
|
+
name: categoryName,
|
|
95
|
+
group_id: entityIdMap.get(category.masterCategoryId),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
entityIdMap.set(category.entityId, id);
|
|
99
|
+
if (category.note) {
|
|
100
|
+
void send('notes-save', {
|
|
101
|
+
id,
|
|
102
|
+
note: category.note,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function importPayees(
|
|
114
|
+
data: YNAB4.YFull,
|
|
115
|
+
entityIdMap: Map<string, string>,
|
|
116
|
+
) {
|
|
117
|
+
for (const payee of data.payees) {
|
|
118
|
+
if (!payee.isTombstone) {
|
|
119
|
+
const id = await send('api/payee-create', {
|
|
120
|
+
payee: {
|
|
121
|
+
name: payee.name,
|
|
122
|
+
transfer_acct: entityIdMap.get(payee.targetAccountId) || null,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// TODO: import payee rules
|
|
127
|
+
|
|
128
|
+
entityIdMap.set(payee.entityId, id);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function importTransactions(
|
|
134
|
+
data: YNAB4.YFull,
|
|
135
|
+
entityIdMap: Map<string, string>,
|
|
136
|
+
) {
|
|
137
|
+
const categories = await send('api/categories-get', {
|
|
138
|
+
grouped: false,
|
|
139
|
+
});
|
|
140
|
+
const incomeCategoryId: string = categories.find(
|
|
141
|
+
cat => cat.name === 'Income',
|
|
142
|
+
).id;
|
|
143
|
+
const accounts = await send('api/accounts-get');
|
|
144
|
+
const payees = await send('api/payees-get');
|
|
145
|
+
|
|
146
|
+
function getCategory(id: string) {
|
|
147
|
+
if (id == null || id === 'Category/__Split__') {
|
|
148
|
+
return null;
|
|
149
|
+
} else if (
|
|
150
|
+
id === 'Category/__ImmediateIncome__' ||
|
|
151
|
+
id === 'Category/__DeferredIncome__'
|
|
152
|
+
) {
|
|
153
|
+
return incomeCategoryId;
|
|
154
|
+
}
|
|
155
|
+
return entityIdMap.get(id);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isOffBudget(acctId: string) {
|
|
159
|
+
const acct = accounts.find(acct => acct.id === acctId);
|
|
160
|
+
if (!acct) {
|
|
161
|
+
throw new Error('Could not find account for transaction when importing');
|
|
162
|
+
}
|
|
163
|
+
return acct.offbudget;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Go ahead and generate ids for all of the transactions so we can
|
|
167
|
+
// reliably resolve transfers
|
|
168
|
+
for (const transaction of data.transactions) {
|
|
169
|
+
entityIdMap.set(transaction.entityId, uuidv4());
|
|
170
|
+
|
|
171
|
+
if (transaction.subTransactions) {
|
|
172
|
+
for (const subTransaction of transaction.subTransactions) {
|
|
173
|
+
entityIdMap.set(subTransaction.entityId, uuidv4());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const transactionsGrouped = groupBy(data.transactions, 'accountId');
|
|
179
|
+
|
|
180
|
+
await Promise.all(
|
|
181
|
+
[...transactionsGrouped.keys()].map(async accountId => {
|
|
182
|
+
const transactions = transactionsGrouped.get(accountId);
|
|
183
|
+
|
|
184
|
+
const toImport = transactions
|
|
185
|
+
.map(transaction => {
|
|
186
|
+
if (transaction.isTombstone) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const id = entityIdMap.get(transaction.entityId);
|
|
191
|
+
|
|
192
|
+
function transferProperties(t: YNAB4.SubTransaction) {
|
|
193
|
+
const transferId = entityIdMap.get(t.transferTransactionId) || null;
|
|
194
|
+
|
|
195
|
+
let payee = null;
|
|
196
|
+
let imported_payee = null;
|
|
197
|
+
if (transferId) {
|
|
198
|
+
payee = payees.find(
|
|
199
|
+
p => p.transfer_acct === entityIdMap.get(t.targetAccountId),
|
|
200
|
+
).id;
|
|
201
|
+
} else {
|
|
202
|
+
payee = entityIdMap.get(t.payeeId);
|
|
203
|
+
imported_payee = data.payees.find(
|
|
204
|
+
p => p.entityId === t.payeeId,
|
|
205
|
+
)?.name;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
transfer_id: transferId,
|
|
210
|
+
payee,
|
|
211
|
+
imported_payee,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const newTransaction = {
|
|
216
|
+
id,
|
|
217
|
+
amount: amountToInteger(transaction.amount),
|
|
218
|
+
category: isOffBudget(entityIdMap.get(accountId))
|
|
219
|
+
? null
|
|
220
|
+
: getCategory(transaction.categoryId),
|
|
221
|
+
date: transaction.date,
|
|
222
|
+
notes: transaction.memo || null,
|
|
223
|
+
cleared:
|
|
224
|
+
transaction.cleared === 'Cleared' ||
|
|
225
|
+
transaction.cleared === 'Reconciled',
|
|
226
|
+
reconciled: transaction.cleared === 'Reconciled',
|
|
227
|
+
...transferProperties(transaction),
|
|
228
|
+
|
|
229
|
+
subtransactions:
|
|
230
|
+
transaction.subTransactions &&
|
|
231
|
+
transaction.subTransactions
|
|
232
|
+
.filter(st => !st.isTombstone)
|
|
233
|
+
.map(t => {
|
|
234
|
+
return {
|
|
235
|
+
id: entityIdMap.get(t.entityId),
|
|
236
|
+
amount: amountToInteger(t.amount),
|
|
237
|
+
category: getCategory(t.categoryId),
|
|
238
|
+
notes: t.memo || null,
|
|
239
|
+
...transferProperties(t),
|
|
240
|
+
};
|
|
241
|
+
}),
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return newTransaction;
|
|
245
|
+
})
|
|
246
|
+
.filter(x => x);
|
|
247
|
+
|
|
248
|
+
await send('api/transactions-add', {
|
|
249
|
+
accountId: entityIdMap.get(accountId),
|
|
250
|
+
transactions: toImport,
|
|
251
|
+
learnCategories: true,
|
|
252
|
+
runTransfers: false,
|
|
253
|
+
});
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function fillInBudgets(
|
|
259
|
+
data: YNAB4.YFull,
|
|
260
|
+
categoryBudgets: YNAB4.MonthlySubCategoryBudget[],
|
|
261
|
+
) {
|
|
262
|
+
// YNAB only contains entries for categories that have been actually
|
|
263
|
+
// budgeted. That would be fine except that we need to set the
|
|
264
|
+
// "carryover" flag on each month when carrying debt across months.
|
|
265
|
+
// To make sure our system has a chance to set this flag on each
|
|
266
|
+
// category, make sure a budget exists for every category of every
|
|
267
|
+
// month.
|
|
268
|
+
const budgets: {
|
|
269
|
+
budgeted: number;
|
|
270
|
+
categoryId: string;
|
|
271
|
+
overspendingHandling?: string;
|
|
272
|
+
}[] = [...categoryBudgets];
|
|
273
|
+
data.masterCategories.forEach(masterCategory => {
|
|
274
|
+
if (masterCategory.subCategories) {
|
|
275
|
+
masterCategory.subCategories.forEach(category => {
|
|
276
|
+
if (!budgets.find(b => b.categoryId === category.entityId)) {
|
|
277
|
+
budgets.push({
|
|
278
|
+
budgeted: 0,
|
|
279
|
+
categoryId: category.entityId,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
return budgets;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function importBudgets(
|
|
289
|
+
data: YNAB4.YFull,
|
|
290
|
+
entityIdMap: Map<string, string>,
|
|
291
|
+
) {
|
|
292
|
+
const budgets = sortByKey(data.monthlyBudgets, 'month');
|
|
293
|
+
|
|
294
|
+
await send('api/batch-budget-start');
|
|
295
|
+
try {
|
|
296
|
+
for (const budget of budgets) {
|
|
297
|
+
const filled = fillInBudgets(
|
|
298
|
+
data,
|
|
299
|
+
budget.monthlySubCategoryBudgets.filter(b => !b.isTombstone),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
await Promise.all(
|
|
303
|
+
filled.map(async catBudget => {
|
|
304
|
+
const amount = amountToInteger(catBudget.budgeted);
|
|
305
|
+
const catId = entityIdMap.get(catBudget.categoryId);
|
|
306
|
+
const month = monthUtils.monthFromDate(budget.month);
|
|
307
|
+
if (!catId) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await send('api/budget-set-amount', {
|
|
312
|
+
month,
|
|
313
|
+
categoryId: catId,
|
|
314
|
+
amount,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (catBudget.overspendingHandling === 'AffectsBuffer') {
|
|
318
|
+
await send('api/budget-set-carryover', {
|
|
319
|
+
month,
|
|
320
|
+
categoryId: catId,
|
|
321
|
+
flag: false,
|
|
322
|
+
});
|
|
323
|
+
} else if (catBudget.overspendingHandling === 'Confined') {
|
|
324
|
+
await send('api/budget-set-carryover', {
|
|
325
|
+
month,
|
|
326
|
+
categoryId: catId,
|
|
327
|
+
flag: true,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
} finally {
|
|
334
|
+
await send('api/batch-budget-end');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function estimateRecentness(str: string) {
|
|
339
|
+
// The "recentness" is the total amount of changes that this device
|
|
340
|
+
// is aware of, which is estimated by summing up all of the version
|
|
341
|
+
// numbers that its aware of. This works because version numbers are
|
|
342
|
+
// increasing integers.
|
|
343
|
+
return str.split(',').reduce((total, version) => {
|
|
344
|
+
const [_, number] = version.split('-');
|
|
345
|
+
return total + parseInt(number);
|
|
346
|
+
}, 0);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function findLatestDevice(zipped: AdmZip, entries: AdmZip.IZipEntry[]): string {
|
|
350
|
+
let devices = entries
|
|
351
|
+
.map(entry => {
|
|
352
|
+
const contents = zipped.readFile(entry).toString('utf8');
|
|
353
|
+
|
|
354
|
+
let data;
|
|
355
|
+
try {
|
|
356
|
+
data = JSON.parse(contents);
|
|
357
|
+
} catch {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (data.hasFullKnowledge) {
|
|
362
|
+
return {
|
|
363
|
+
deviceGUID: data.deviceGUID,
|
|
364
|
+
shortName: data.shortDeviceId,
|
|
365
|
+
recentness: estimateRecentness(data.knowledge),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return null;
|
|
370
|
+
})
|
|
371
|
+
.filter(x => x);
|
|
372
|
+
|
|
373
|
+
devices = sortByKey(devices, 'recentness');
|
|
374
|
+
return devices[devices.length - 1].deviceGUID;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export async function doImport(data: YNAB4.YFull) {
|
|
378
|
+
const entityIdMap = new Map<string, string>();
|
|
379
|
+
|
|
380
|
+
logger.log('Importing Accounts...');
|
|
381
|
+
await importAccounts(data, entityIdMap);
|
|
382
|
+
|
|
383
|
+
logger.log('Importing Categories...');
|
|
384
|
+
await importCategories(data, entityIdMap);
|
|
385
|
+
|
|
386
|
+
logger.log('Importing Payees...');
|
|
387
|
+
await importPayees(data, entityIdMap);
|
|
388
|
+
|
|
389
|
+
logger.log('Importing Transactions...');
|
|
390
|
+
await importTransactions(data, entityIdMap);
|
|
391
|
+
|
|
392
|
+
logger.log('Importing Budgets...');
|
|
393
|
+
await importBudgets(data, entityIdMap);
|
|
394
|
+
|
|
395
|
+
logger.log('Setting up...');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export function getBudgetName(filepath) {
|
|
399
|
+
let unixFilepath = normalizePathSep(filepath);
|
|
400
|
+
|
|
401
|
+
if (!/\.zip/.test(unixFilepath)) {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
unixFilepath = unixFilepath.replace(/\.zip$/, '').replace(/.ynab4$/, '');
|
|
406
|
+
|
|
407
|
+
// Most budgets are named like "Budget~51938D82.ynab4" but sometimes
|
|
408
|
+
// they are only "Budget.ynab4". We only want to grab the name
|
|
409
|
+
// before the ~ if it exists.
|
|
410
|
+
const m = unixFilepath.match(/([^/~]+)[^/]*$/);
|
|
411
|
+
if (!m) {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
return m[1];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getFile(entries: AdmZip.IZipEntry[], path: string) {
|
|
418
|
+
const files = entries.filter(e => e.entryName === path);
|
|
419
|
+
if (files.length === 0) {
|
|
420
|
+
throw new Error('Could not find file: ' + path);
|
|
421
|
+
}
|
|
422
|
+
if (files.length >= 2) {
|
|
423
|
+
throw new Error('File name matches multiple files: ' + path);
|
|
424
|
+
}
|
|
425
|
+
return files[0];
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function join(...paths: string[]): string {
|
|
429
|
+
return paths.slice(1).reduce(
|
|
430
|
+
(full, path) => {
|
|
431
|
+
return full + '/' + path.replace(/^\//, '');
|
|
432
|
+
},
|
|
433
|
+
paths[0].replace(/\/$/, ''),
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function parseFile(buffer: Buffer): YNAB4.YFull {
|
|
438
|
+
const zipped = new AdmZip(buffer);
|
|
439
|
+
const entries = zipped.getEntries();
|
|
440
|
+
|
|
441
|
+
let root = '';
|
|
442
|
+
const dirMatch = entries[0].entryName.match(/([^/]*\.ynab4)/);
|
|
443
|
+
if (dirMatch) {
|
|
444
|
+
root = dirMatch[1] + '/';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const metaStr = zipped.readFile(getFile(entries, root + 'Budget.ymeta'));
|
|
448
|
+
const meta = JSON.parse(metaStr.toString('utf8'));
|
|
449
|
+
const budgetPath = join(root, meta.relativeDataFolderName);
|
|
450
|
+
|
|
451
|
+
const deviceFiles = entries.filter(e =>
|
|
452
|
+
e.entryName.startsWith(join(budgetPath, 'devices')),
|
|
453
|
+
);
|
|
454
|
+
const deviceGUID = findLatestDevice(zipped, deviceFiles);
|
|
455
|
+
|
|
456
|
+
const yfullPath = join(budgetPath, deviceGUID, 'Budget.yfull');
|
|
457
|
+
let contents;
|
|
458
|
+
try {
|
|
459
|
+
contents = zipped.readFile(getFile(entries, yfullPath)).toString('utf8');
|
|
460
|
+
} catch (e) {
|
|
461
|
+
logger.log(e);
|
|
462
|
+
throw new Error('Error reading Budget.yfull file');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(contents);
|
|
467
|
+
} catch {
|
|
468
|
+
throw new Error('Error parsing Budget.yfull file');
|
|
469
|
+
}
|
|
470
|
+
}
|