@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,1030 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import { getClock } from '@actual-app/crdt';
|
|
3
|
+
|
|
4
|
+
import * as connection from '../platform/server/connection';
|
|
5
|
+
import { logger } from '../platform/server/log';
|
|
6
|
+
import {
|
|
7
|
+
getBankSyncError,
|
|
8
|
+
getDownloadError,
|
|
9
|
+
getSyncError,
|
|
10
|
+
getTestKeyError,
|
|
11
|
+
} from '../shared/errors';
|
|
12
|
+
import * as monthUtils from '../shared/months';
|
|
13
|
+
import { q } from '../shared/query';
|
|
14
|
+
import {
|
|
15
|
+
deleteTransaction,
|
|
16
|
+
ungroupTransactions,
|
|
17
|
+
updateTransaction,
|
|
18
|
+
} from '../shared/transactions';
|
|
19
|
+
import { integerToAmount } from '../shared/util';
|
|
20
|
+
import type { Handlers } from '../types/handlers';
|
|
21
|
+
import type {
|
|
22
|
+
AccountEntity,
|
|
23
|
+
CategoryGroupEntity,
|
|
24
|
+
ScheduleEntity,
|
|
25
|
+
} from '../types/models';
|
|
26
|
+
import type { ServerHandlers } from '../types/server-handlers';
|
|
27
|
+
|
|
28
|
+
import { addTransactions } from './accounts/sync';
|
|
29
|
+
import {
|
|
30
|
+
accountModel,
|
|
31
|
+
budgetModel,
|
|
32
|
+
categoryGroupModel,
|
|
33
|
+
categoryModel,
|
|
34
|
+
payeeModel,
|
|
35
|
+
remoteFileModel,
|
|
36
|
+
scheduleModel,
|
|
37
|
+
tagModel,
|
|
38
|
+
} from './api-models';
|
|
39
|
+
import type { AmountOPType, APIScheduleEntity } from './api-models';
|
|
40
|
+
import { aqlQuery } from './aql';
|
|
41
|
+
import * as cloudStorage from './cloud-storage';
|
|
42
|
+
import type { RemoteFile } from './cloud-storage';
|
|
43
|
+
import * as db from './db';
|
|
44
|
+
import { APIError } from './errors';
|
|
45
|
+
import { runMutator } from './mutators';
|
|
46
|
+
import * as prefs from './prefs';
|
|
47
|
+
import * as sheet from './sheet';
|
|
48
|
+
import { batchMessages, setSyncingMode } from './sync';
|
|
49
|
+
|
|
50
|
+
let IMPORT_MODE = false;
|
|
51
|
+
|
|
52
|
+
// The API is different in two ways: we never want undo enabled, and
|
|
53
|
+
// we also need to notify the UI manually if stuff has changed (if
|
|
54
|
+
// they are connecting to an already running instance, the UI should
|
|
55
|
+
// update). The wrapper handles that.
|
|
56
|
+
function withMutation<Params extends Array<unknown>, ReturnType>(
|
|
57
|
+
handler: (...args: Params) => Promise<ReturnType>,
|
|
58
|
+
) {
|
|
59
|
+
return (...args: Params) => {
|
|
60
|
+
return runMutator(
|
|
61
|
+
async () => {
|
|
62
|
+
const latestTimestamp = getClock().timestamp.toString();
|
|
63
|
+
const result = await handler(...args);
|
|
64
|
+
|
|
65
|
+
const rows = await db.all<Pick<db.DbCrdtMessage, 'dataset'>>(
|
|
66
|
+
'SELECT DISTINCT dataset FROM messages_crdt WHERE timestamp > ?',
|
|
67
|
+
[latestTimestamp],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Only send the sync event if anybody else is connected
|
|
71
|
+
if (connection.getNumClients() > 1) {
|
|
72
|
+
connection.send('sync-event', {
|
|
73
|
+
type: 'success',
|
|
74
|
+
tables: rows.map(row => row.dataset),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
},
|
|
80
|
+
{ undoDisabled: true },
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let handlers = {} as unknown as Handlers;
|
|
86
|
+
|
|
87
|
+
async function validateMonth(month) {
|
|
88
|
+
if (!month.match(/^\d{4}-\d{2}$/)) {
|
|
89
|
+
throw APIError('Invalid month format, use YYYY-MM: ' + month);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!IMPORT_MODE) {
|
|
93
|
+
const { start, end } = await handlers['get-budget-bounds']();
|
|
94
|
+
const range = monthUtils.range(start, end);
|
|
95
|
+
if (!range.includes(month)) {
|
|
96
|
+
throw APIError('No budget exists for month: ' + month);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function validateExpenseCategory(debug, id) {
|
|
102
|
+
if (id == null) {
|
|
103
|
+
throw APIError(`${debug}: category id is required`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const row = await db.first<Pick<db.DbCategory, 'is_income'>>(
|
|
107
|
+
'SELECT is_income FROM categories WHERE id = ?',
|
|
108
|
+
[id],
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (!row) {
|
|
112
|
+
throw APIError(`${debug}: category "${id}" does not exist`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (row.is_income !== 0) {
|
|
116
|
+
throw APIError(`${debug}: category "${id}" is not an expense category`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function checkFileOpen() {
|
|
121
|
+
if (!(prefs.getPrefs() || {}).id) {
|
|
122
|
+
throw APIError('No budget file is open');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let batchPromise = null;
|
|
127
|
+
|
|
128
|
+
handlers['api/batch-budget-start'] = async function () {
|
|
129
|
+
if (batchPromise) {
|
|
130
|
+
throw APIError('Cannot start a batch process: batch already started');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// If we are importing, all we need to do is start a raw database
|
|
134
|
+
// transaction. Updating spreadsheet cells doesn't go through the
|
|
135
|
+
// syncing layer in that case.
|
|
136
|
+
if (IMPORT_MODE) {
|
|
137
|
+
void db.asyncTransaction(() => {
|
|
138
|
+
return new Promise((resolve, reject) => {
|
|
139
|
+
batchPromise = { resolve, reject };
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
} else {
|
|
143
|
+
void batchMessages(() => {
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
batchPromise = { resolve, reject };
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
handlers['api/batch-budget-end'] = async function () {
|
|
152
|
+
if (!batchPromise) {
|
|
153
|
+
throw APIError('Cannot end a batch process: no batch started');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
batchPromise.resolve();
|
|
157
|
+
batchPromise = null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
handlers['api/load-budget'] = async function ({ id }) {
|
|
161
|
+
const { id: currentId } = prefs.getPrefs() || {};
|
|
162
|
+
|
|
163
|
+
if (currentId !== id) {
|
|
164
|
+
connection.send('start-load');
|
|
165
|
+
const { error } = await handlers['load-budget']({ id });
|
|
166
|
+
|
|
167
|
+
if (!error) {
|
|
168
|
+
connection.send('finish-load');
|
|
169
|
+
} else {
|
|
170
|
+
connection.send('show-budgets');
|
|
171
|
+
|
|
172
|
+
throw new Error(getSyncError(error, id));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
handlers['api/download-budget'] = async function ({ syncId, password }) {
|
|
178
|
+
const { id: currentId } = prefs.getPrefs() || {};
|
|
179
|
+
if (currentId) {
|
|
180
|
+
await handlers['close-budget']();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const budgets = await handlers['get-budgets']();
|
|
184
|
+
const localBudget = budgets.find(b => b.groupId === syncId);
|
|
185
|
+
let remoteBudget: RemoteFile;
|
|
186
|
+
|
|
187
|
+
// Load a remote file if we could not find the file locally
|
|
188
|
+
if (!localBudget) {
|
|
189
|
+
const files = await handlers['get-remote-files']();
|
|
190
|
+
if (!files) {
|
|
191
|
+
throw new Error('Could not get remote files');
|
|
192
|
+
}
|
|
193
|
+
const file = files.find(f => f.groupId === syncId);
|
|
194
|
+
if (!file) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Budget "${syncId}" not found. Check the sync id of your budget in the Advanced section of the settings page.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
remoteBudget = file;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const activeFile = remoteBudget ? remoteBudget : localBudget;
|
|
204
|
+
|
|
205
|
+
// Set the e2e encryption keys
|
|
206
|
+
if (activeFile.encryptKeyId) {
|
|
207
|
+
if (!password) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`File ${activeFile.name} is encrypted. Please provide a password.`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const result = await handlers['key-test']({
|
|
214
|
+
cloudFileId: remoteBudget ? remoteBudget.fileId : localBudget.cloudFileId,
|
|
215
|
+
password,
|
|
216
|
+
});
|
|
217
|
+
if (result.error) {
|
|
218
|
+
throw new Error(getTestKeyError(result.error));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Sync the local budget file
|
|
223
|
+
if (localBudget) {
|
|
224
|
+
await handlers['load-budget']({ id: localBudget.id });
|
|
225
|
+
const result = await handlers['sync-budget']();
|
|
226
|
+
if (result.error) {
|
|
227
|
+
throw new Error(getSyncError(result.error.reason, localBudget.id));
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Download the remote file (no need to perform a sync as the file will already be up-to-date)
|
|
233
|
+
const result = await handlers['download-budget']({
|
|
234
|
+
cloudFileId: remoteBudget.fileId,
|
|
235
|
+
});
|
|
236
|
+
if (result.error) {
|
|
237
|
+
logger.log('Full error details', result.error);
|
|
238
|
+
throw new Error(getDownloadError(result.error));
|
|
239
|
+
}
|
|
240
|
+
await handlers['load-budget']({ id: result.id });
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
handlers['api/get-budgets'] = async function () {
|
|
244
|
+
const budgets = await handlers['get-budgets']();
|
|
245
|
+
const files = (await handlers['get-remote-files']()) || [];
|
|
246
|
+
return [
|
|
247
|
+
...budgets.map(file => budgetModel.toExternal(file)),
|
|
248
|
+
...files.map(file => remoteFileModel.toExternal(file)).filter(file => file),
|
|
249
|
+
];
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
handlers['api/sync'] = async function () {
|
|
253
|
+
const { id } = prefs.getPrefs();
|
|
254
|
+
const result = await handlers['sync-budget']();
|
|
255
|
+
if (result.error) {
|
|
256
|
+
throw new Error(getSyncError(result.error.reason, id));
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
handlers['api/bank-sync'] = async function (args) {
|
|
261
|
+
const batchSync = args?.accountId == null;
|
|
262
|
+
const allErrors = [];
|
|
263
|
+
|
|
264
|
+
if (!batchSync) {
|
|
265
|
+
const { errors } = await handlers['accounts-bank-sync']({
|
|
266
|
+
ids: [args.accountId],
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
allErrors.push(...errors);
|
|
270
|
+
} else {
|
|
271
|
+
const accountsData = await handlers['accounts-get']();
|
|
272
|
+
const accountIdsToSync = accountsData.map(a => a.id);
|
|
273
|
+
const simpleFinAccounts = accountsData.filter(
|
|
274
|
+
a => a.account_sync_source === 'simpleFin',
|
|
275
|
+
);
|
|
276
|
+
const simpleFinAccountIds = simpleFinAccounts.map(a => a.id);
|
|
277
|
+
|
|
278
|
+
if (simpleFinAccounts.length > 1) {
|
|
279
|
+
const res = await handlers['simplefin-batch-sync']({
|
|
280
|
+
ids: simpleFinAccountIds,
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
res.forEach(a => allErrors.push(...a.res.errors));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { errors } = await handlers['accounts-bank-sync']({
|
|
287
|
+
ids: accountIdsToSync.filter(a => !simpleFinAccountIds.includes(a)),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
allErrors.push(...errors);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const errors = allErrors.filter(e => e != null);
|
|
294
|
+
if (errors.length > 0) {
|
|
295
|
+
throw new Error(getBankSyncError(errors[0]));
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
handlers['api/start-import'] = async function ({ budgetName }) {
|
|
300
|
+
// Notify UI to close budget
|
|
301
|
+
await handlers['close-budget']();
|
|
302
|
+
|
|
303
|
+
// Create the budget
|
|
304
|
+
await handlers['create-budget']({ budgetName, avoidUpload: true });
|
|
305
|
+
|
|
306
|
+
// Clear out the default expense categories
|
|
307
|
+
db.runQuery('DELETE FROM categories WHERE is_income = 0');
|
|
308
|
+
db.runQuery('DELETE FROM category_groups WHERE is_income = 0');
|
|
309
|
+
|
|
310
|
+
// Turn syncing off
|
|
311
|
+
setSyncingMode('import');
|
|
312
|
+
|
|
313
|
+
connection.send('start-import');
|
|
314
|
+
IMPORT_MODE = true;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
handlers['api/finish-import'] = async function () {
|
|
318
|
+
checkFileOpen();
|
|
319
|
+
|
|
320
|
+
sheet.get().markCacheDirty();
|
|
321
|
+
|
|
322
|
+
// We always need to fully reload the app. Importing doesn't touch
|
|
323
|
+
// the spreadsheet, but we can't just recreate the spreadsheet
|
|
324
|
+
// either; there is other internal state that isn't created
|
|
325
|
+
const { id } = prefs.getPrefs();
|
|
326
|
+
await handlers['close-budget']();
|
|
327
|
+
await handlers['load-budget']({ id });
|
|
328
|
+
|
|
329
|
+
await handlers['get-budget-bounds']();
|
|
330
|
+
await sheet.waitOnSpreadsheet();
|
|
331
|
+
|
|
332
|
+
await cloudStorage.upload().catch(() => {
|
|
333
|
+
// Ignore errors
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
connection.send('finish-import');
|
|
337
|
+
IMPORT_MODE = false;
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
handlers['api/abort-import'] = async function () {
|
|
341
|
+
if (IMPORT_MODE) {
|
|
342
|
+
checkFileOpen();
|
|
343
|
+
|
|
344
|
+
const { id } = prefs.getPrefs();
|
|
345
|
+
|
|
346
|
+
await handlers['close-budget']();
|
|
347
|
+
await handlers['delete-budget']({ id });
|
|
348
|
+
connection.send('show-budgets');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
IMPORT_MODE = false;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
handlers['api/query'] = async function ({ query }) {
|
|
355
|
+
checkFileOpen();
|
|
356
|
+
return aqlQuery(query);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
handlers['api/budget-months'] = async function () {
|
|
360
|
+
checkFileOpen();
|
|
361
|
+
const { start, end } = await handlers['get-budget-bounds']();
|
|
362
|
+
return monthUtils.range(start, end);
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
handlers['api/budget-month'] = async function ({ month }) {
|
|
366
|
+
checkFileOpen();
|
|
367
|
+
await validateMonth(month);
|
|
368
|
+
|
|
369
|
+
const { data: groups }: { data: CategoryGroupEntity[] } = await aqlQuery(
|
|
370
|
+
q('category_groups').select('*'),
|
|
371
|
+
);
|
|
372
|
+
const sheetName = monthUtils.sheetForMonth(month);
|
|
373
|
+
|
|
374
|
+
function value(name) {
|
|
375
|
+
const v = sheet.get().getCellValue(sheetName, name);
|
|
376
|
+
return v === '' ? 0 : v;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// This is duplicated from main.js because the return format is
|
|
380
|
+
// different (for now)
|
|
381
|
+
return {
|
|
382
|
+
month,
|
|
383
|
+
incomeAvailable: value('available-funds') as number,
|
|
384
|
+
lastMonthOverspent: value('last-month-overspent') as number,
|
|
385
|
+
forNextMonth: value('buffered') as number,
|
|
386
|
+
totalBudgeted: value('total-budgeted') as number,
|
|
387
|
+
toBudget: value('to-budget') as number,
|
|
388
|
+
|
|
389
|
+
fromLastMonth: value('from-last-month') as number,
|
|
390
|
+
totalIncome: value('total-income') as number,
|
|
391
|
+
totalSpent: value('total-spent') as number,
|
|
392
|
+
totalBalance: value('total-leftover') as number,
|
|
393
|
+
|
|
394
|
+
categoryGroups: groups.map(group => {
|
|
395
|
+
if (group.is_income) {
|
|
396
|
+
return {
|
|
397
|
+
...categoryGroupModel.toExternal(group),
|
|
398
|
+
received: value('total-income'),
|
|
399
|
+
|
|
400
|
+
categories: group.categories.map(cat => ({
|
|
401
|
+
...categoryModel.toExternal(cat),
|
|
402
|
+
received: value(`sum-amount-${cat.id}`),
|
|
403
|
+
})),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
...categoryGroupModel.toExternal(group),
|
|
409
|
+
budgeted: value(`group-budget-${group.id}`),
|
|
410
|
+
spent: value(`group-sum-amount-${group.id}`),
|
|
411
|
+
balance: value(`group-leftover-${group.id}`),
|
|
412
|
+
|
|
413
|
+
categories: group.categories.map(cat => ({
|
|
414
|
+
...categoryModel.toExternal(cat),
|
|
415
|
+
budgeted: value(`budget-${cat.id}`),
|
|
416
|
+
spent: value(`sum-amount-${cat.id}`),
|
|
417
|
+
balance: value(`leftover-${cat.id}`),
|
|
418
|
+
carryover: value(`carryover-${cat.id}`),
|
|
419
|
+
})),
|
|
420
|
+
};
|
|
421
|
+
}),
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
handlers['api/budget-set-amount'] = withMutation(async function ({
|
|
426
|
+
month,
|
|
427
|
+
categoryId,
|
|
428
|
+
amount,
|
|
429
|
+
}) {
|
|
430
|
+
checkFileOpen();
|
|
431
|
+
return handlers['budget/budget-amount']({
|
|
432
|
+
month,
|
|
433
|
+
category: categoryId,
|
|
434
|
+
amount,
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
handlers['api/budget-set-carryover'] = withMutation(async function ({
|
|
439
|
+
month,
|
|
440
|
+
categoryId,
|
|
441
|
+
flag,
|
|
442
|
+
}) {
|
|
443
|
+
checkFileOpen();
|
|
444
|
+
await validateMonth(month);
|
|
445
|
+
await validateExpenseCategory('budget-set-carryover', categoryId);
|
|
446
|
+
return handlers['budget/set-carryover']({
|
|
447
|
+
startMonth: month,
|
|
448
|
+
category: categoryId,
|
|
449
|
+
flag,
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
handlers['api/budget-hold-for-next-month'] = withMutation(async function ({
|
|
454
|
+
month,
|
|
455
|
+
amount,
|
|
456
|
+
}) {
|
|
457
|
+
checkFileOpen();
|
|
458
|
+
await validateMonth(month);
|
|
459
|
+
if (amount <= 0) {
|
|
460
|
+
throw APIError('Amount to hold needs to be greater than 0');
|
|
461
|
+
}
|
|
462
|
+
return handlers['budget/hold-for-next-month']({
|
|
463
|
+
month,
|
|
464
|
+
amount,
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
handlers['api/budget-reset-hold'] = withMutation(async function ({ month }) {
|
|
469
|
+
checkFileOpen();
|
|
470
|
+
await validateMonth(month);
|
|
471
|
+
return handlers['budget/reset-hold']({ month });
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
handlers['api/transactions-export'] = async function ({
|
|
475
|
+
transactions,
|
|
476
|
+
categoryGroups,
|
|
477
|
+
payees,
|
|
478
|
+
accounts,
|
|
479
|
+
}) {
|
|
480
|
+
checkFileOpen();
|
|
481
|
+
return handlers['transactions-export']({
|
|
482
|
+
transactions,
|
|
483
|
+
categoryGroups,
|
|
484
|
+
payees,
|
|
485
|
+
accounts,
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
handlers['api/transactions-import'] = withMutation(async function ({
|
|
490
|
+
accountId,
|
|
491
|
+
transactions,
|
|
492
|
+
isPreview = false,
|
|
493
|
+
opts,
|
|
494
|
+
}) {
|
|
495
|
+
checkFileOpen();
|
|
496
|
+
return handlers['transactions-import']({
|
|
497
|
+
accountId,
|
|
498
|
+
transactions,
|
|
499
|
+
isPreview,
|
|
500
|
+
opts,
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
handlers['api/transactions-add'] = withMutation(async function ({
|
|
505
|
+
accountId,
|
|
506
|
+
transactions,
|
|
507
|
+
runTransfers = false,
|
|
508
|
+
learnCategories = false,
|
|
509
|
+
}) {
|
|
510
|
+
checkFileOpen();
|
|
511
|
+
await addTransactions(accountId, transactions, {
|
|
512
|
+
runTransfers,
|
|
513
|
+
learnCategories,
|
|
514
|
+
});
|
|
515
|
+
return 'ok' as const;
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
handlers['api/transactions-get'] = async function ({
|
|
519
|
+
accountId,
|
|
520
|
+
startDate,
|
|
521
|
+
endDate,
|
|
522
|
+
}) {
|
|
523
|
+
checkFileOpen();
|
|
524
|
+
const { data } = await aqlQuery(
|
|
525
|
+
q('transactions')
|
|
526
|
+
.filter({
|
|
527
|
+
$and: [
|
|
528
|
+
accountId && { account: accountId },
|
|
529
|
+
startDate && { date: { $gte: startDate } },
|
|
530
|
+
endDate && { date: { $lte: endDate } },
|
|
531
|
+
].filter(Boolean),
|
|
532
|
+
})
|
|
533
|
+
.select('*')
|
|
534
|
+
.options({ splits: 'grouped' }),
|
|
535
|
+
);
|
|
536
|
+
return data;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
handlers['api/transaction-update'] = withMutation(async function ({
|
|
540
|
+
id,
|
|
541
|
+
fields,
|
|
542
|
+
}) {
|
|
543
|
+
checkFileOpen();
|
|
544
|
+
const { data } = await aqlQuery(
|
|
545
|
+
q('transactions').filter({ id }).select('*').options({ splits: 'grouped' }),
|
|
546
|
+
);
|
|
547
|
+
const transactions = ungroupTransactions(data);
|
|
548
|
+
|
|
549
|
+
if (transactions.length === 0) {
|
|
550
|
+
return [];
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const { diff } = updateTransaction(transactions, { id, ...fields });
|
|
554
|
+
return handlers['transactions-batch-update'](diff)['updated'];
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
handlers['api/transaction-delete'] = withMutation(async function ({ id }) {
|
|
558
|
+
checkFileOpen();
|
|
559
|
+
const { data } = await aqlQuery(
|
|
560
|
+
q('transactions').filter({ id }).select('*').options({ splits: 'grouped' }),
|
|
561
|
+
);
|
|
562
|
+
const transactions = ungroupTransactions(data);
|
|
563
|
+
|
|
564
|
+
if (transactions.length === 0) {
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const { diff } = deleteTransaction(transactions, id);
|
|
569
|
+
return handlers['transactions-batch-update'](diff)['deleted'];
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
handlers['api/accounts-get'] = async function () {
|
|
573
|
+
checkFileOpen();
|
|
574
|
+
const accounts: AccountEntity[] = await handlers['accounts-get']();
|
|
575
|
+
return accounts.map(account => accountModel.toExternal(account));
|
|
576
|
+
};
|
|
577
|
+
|
|
578
|
+
handlers['api/account-create'] = withMutation(async function ({
|
|
579
|
+
account,
|
|
580
|
+
initialBalance = null,
|
|
581
|
+
}) {
|
|
582
|
+
checkFileOpen();
|
|
583
|
+
return handlers['account-create']({
|
|
584
|
+
name: account.name,
|
|
585
|
+
offBudget: account.offbudget,
|
|
586
|
+
closed: account.closed,
|
|
587
|
+
// Current the API expects an amount but it really should expect
|
|
588
|
+
// an integer
|
|
589
|
+
balance: initialBalance != null ? integerToAmount(initialBalance) : null,
|
|
590
|
+
});
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
handlers['api/account-update'] = withMutation(async function ({ id, fields }) {
|
|
594
|
+
checkFileOpen();
|
|
595
|
+
return db.updateAccount({ id, ...accountModel.fromExternal(fields) });
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
handlers['api/account-close'] = withMutation(async function ({
|
|
599
|
+
id,
|
|
600
|
+
transferAccountId,
|
|
601
|
+
transferCategoryId,
|
|
602
|
+
}) {
|
|
603
|
+
checkFileOpen();
|
|
604
|
+
return handlers['account-close']({
|
|
605
|
+
id,
|
|
606
|
+
transferAccountId,
|
|
607
|
+
categoryId: transferCategoryId,
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
handlers['api/account-reopen'] = withMutation(async function ({ id }) {
|
|
612
|
+
checkFileOpen();
|
|
613
|
+
return handlers['account-reopen']({ id });
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
handlers['api/account-delete'] = withMutation(async function ({ id }) {
|
|
617
|
+
checkFileOpen();
|
|
618
|
+
return handlers['account-close']({ id, forced: true });
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
handlers['api/account-balance'] = withMutation(async function ({
|
|
622
|
+
id,
|
|
623
|
+
cutoff = new Date(),
|
|
624
|
+
}) {
|
|
625
|
+
checkFileOpen();
|
|
626
|
+
return handlers['account-balance']({ id, cutoff });
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
handlers['api/categories-get'] = async function ({
|
|
630
|
+
grouped,
|
|
631
|
+
}: { grouped? } = {}) {
|
|
632
|
+
checkFileOpen();
|
|
633
|
+
const result = await handlers['get-categories']();
|
|
634
|
+
return grouped
|
|
635
|
+
? result.grouped.map(group => categoryGroupModel.toExternal(group))
|
|
636
|
+
: result.list.map(category => categoryModel.toExternal(category));
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
handlers['api/category-groups-get'] = async function () {
|
|
640
|
+
checkFileOpen();
|
|
641
|
+
const groups = await handlers['get-category-groups']();
|
|
642
|
+
return groups.map(group => categoryGroupModel.toExternal(group));
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
handlers['api/category-group-create'] = withMutation(async function ({
|
|
646
|
+
group,
|
|
647
|
+
}) {
|
|
648
|
+
checkFileOpen();
|
|
649
|
+
return handlers['category-group-create']({
|
|
650
|
+
name: group.name,
|
|
651
|
+
hidden: group.hidden,
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
handlers['api/category-group-update'] = withMutation(async function ({
|
|
656
|
+
id,
|
|
657
|
+
fields,
|
|
658
|
+
}) {
|
|
659
|
+
checkFileOpen();
|
|
660
|
+
return handlers['category-group-update']({
|
|
661
|
+
id,
|
|
662
|
+
...categoryGroupModel.fromExternal(fields),
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
handlers['api/category-group-delete'] = withMutation(async function ({
|
|
667
|
+
id,
|
|
668
|
+
transferCategoryId,
|
|
669
|
+
}) {
|
|
670
|
+
checkFileOpen();
|
|
671
|
+
return handlers['category-group-delete']({
|
|
672
|
+
id,
|
|
673
|
+
transferId: transferCategoryId,
|
|
674
|
+
});
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
handlers['api/category-create'] = withMutation(async function ({ category }) {
|
|
678
|
+
checkFileOpen();
|
|
679
|
+
return handlers['category-create']({
|
|
680
|
+
name: category.name,
|
|
681
|
+
groupId: category.group_id,
|
|
682
|
+
isIncome: category.is_income,
|
|
683
|
+
hidden: category.hidden,
|
|
684
|
+
});
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
handlers['api/category-update'] = withMutation(async function ({ id, fields }) {
|
|
688
|
+
checkFileOpen();
|
|
689
|
+
return handlers['category-update']({
|
|
690
|
+
id,
|
|
691
|
+
...categoryModel.fromExternal(fields),
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
handlers['api/category-delete'] = withMutation(async function ({
|
|
696
|
+
id,
|
|
697
|
+
transferCategoryId,
|
|
698
|
+
}) {
|
|
699
|
+
checkFileOpen();
|
|
700
|
+
return handlers['category-delete']({
|
|
701
|
+
id,
|
|
702
|
+
transferId: transferCategoryId,
|
|
703
|
+
});
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
handlers['api/common-payees-get'] = async function () {
|
|
707
|
+
checkFileOpen();
|
|
708
|
+
const payees = await handlers['common-payees-get']();
|
|
709
|
+
return payees.map(payee => payeeModel.toExternal(payee));
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
handlers['api/payees-get'] = async function () {
|
|
713
|
+
checkFileOpen();
|
|
714
|
+
const payees = await handlers['payees-get']();
|
|
715
|
+
return payees.map(payee => payeeModel.toExternal(payee));
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
handlers['api/payee-create'] = withMutation(async function ({ payee }) {
|
|
719
|
+
checkFileOpen();
|
|
720
|
+
return handlers['payee-create']({ name: payee.name });
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
handlers['api/payee-update'] = withMutation(async function ({ id, fields }) {
|
|
724
|
+
checkFileOpen();
|
|
725
|
+
return handlers['payees-batch-change']({
|
|
726
|
+
updated: [{ id, ...payeeModel.fromExternal(fields) }],
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
handlers['api/payee-delete'] = withMutation(async function ({ id }) {
|
|
731
|
+
checkFileOpen();
|
|
732
|
+
return handlers['payees-batch-change']({ deleted: [{ id }] });
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
handlers['api/payees-merge'] = withMutation(async function ({
|
|
736
|
+
targetId,
|
|
737
|
+
mergeIds,
|
|
738
|
+
}) {
|
|
739
|
+
checkFileOpen();
|
|
740
|
+
return handlers['payees-merge']({ targetId, mergeIds });
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
handlers['api/tags-get'] = async function () {
|
|
744
|
+
checkFileOpen();
|
|
745
|
+
const tags = await handlers['tags-get']();
|
|
746
|
+
return tags.map(tag => tagModel.toExternal(tag));
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
handlers['api/tag-create'] = withMutation(async function ({ tag }) {
|
|
750
|
+
checkFileOpen();
|
|
751
|
+
const result = await handlers['tags-create']({
|
|
752
|
+
tag: tag.tag,
|
|
753
|
+
color: tag.color,
|
|
754
|
+
description: tag.description,
|
|
755
|
+
});
|
|
756
|
+
return result.id;
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
handlers['api/tag-update'] = withMutation(async function ({ id, fields }) {
|
|
760
|
+
checkFileOpen();
|
|
761
|
+
await handlers['tags-update']({ id, ...tagModel.fromExternal(fields) });
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
handlers['api/tag-delete'] = withMutation(async function ({ id }) {
|
|
765
|
+
checkFileOpen();
|
|
766
|
+
await handlers['tags-delete']({ id });
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
handlers['api/payee-location-create'] = withMutation(async function ({
|
|
770
|
+
payeeId,
|
|
771
|
+
latitude,
|
|
772
|
+
longitude,
|
|
773
|
+
}) {
|
|
774
|
+
checkFileOpen();
|
|
775
|
+
return handlers['payee-location-create']({ payeeId, latitude, longitude });
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
handlers['api/payee-locations-get'] = async function ({ payeeId }) {
|
|
779
|
+
checkFileOpen();
|
|
780
|
+
return handlers['payee-locations-get']({ payeeId });
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
handlers['api/payee-location-delete'] = withMutation(async function ({ id }) {
|
|
784
|
+
checkFileOpen();
|
|
785
|
+
return handlers['payee-location-delete']({ id });
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
handlers['api/payees-get-nearby'] = async function ({
|
|
789
|
+
latitude,
|
|
790
|
+
longitude,
|
|
791
|
+
maxDistance,
|
|
792
|
+
}) {
|
|
793
|
+
checkFileOpen();
|
|
794
|
+
return handlers['payees-get-nearby']({ latitude, longitude, maxDistance });
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
handlers['api/rules-get'] = async function () {
|
|
798
|
+
checkFileOpen();
|
|
799
|
+
return handlers['rules-get']();
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
handlers['api/payee-rules-get'] = async function ({ id }) {
|
|
803
|
+
checkFileOpen();
|
|
804
|
+
return handlers['payees-get-rules']({ id });
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
handlers['api/rule-create'] = withMutation(async function ({ rule }) {
|
|
808
|
+
checkFileOpen();
|
|
809
|
+
const addedRule = await handlers['rule-add'](rule);
|
|
810
|
+
|
|
811
|
+
if ('error' in addedRule) {
|
|
812
|
+
throw APIError('Failed creating a new rule', addedRule.error);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return addedRule;
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
handlers['api/rule-update'] = withMutation(async function ({ rule }) {
|
|
819
|
+
checkFileOpen();
|
|
820
|
+
const updatedRule = await handlers['rule-update'](rule);
|
|
821
|
+
|
|
822
|
+
if ('error' in updatedRule) {
|
|
823
|
+
throw APIError('Failed updating the rule', updatedRule.error);
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
return updatedRule;
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
handlers['api/rule-delete'] = withMutation(async function (id) {
|
|
830
|
+
checkFileOpen();
|
|
831
|
+
return handlers['rule-delete'](id);
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
handlers['api/schedules-get'] = async function () {
|
|
835
|
+
checkFileOpen();
|
|
836
|
+
const { data } = await aqlQuery(q('schedules').select('*'));
|
|
837
|
+
const schedules = data as ScheduleEntity[];
|
|
838
|
+
return schedules.map(schedule => scheduleModel.toExternal(schedule));
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
handlers['api/schedule-create'] = withMutation(async function (
|
|
842
|
+
schedule: Omit<APIScheduleEntity, 'id'>,
|
|
843
|
+
) {
|
|
844
|
+
checkFileOpen();
|
|
845
|
+
const internalSchedule = scheduleModel.fromExternal({ ...schedule, id: '' });
|
|
846
|
+
const partialSchedule = {
|
|
847
|
+
name: internalSchedule.name,
|
|
848
|
+
posts_transaction: internalSchedule.posts_transaction,
|
|
849
|
+
};
|
|
850
|
+
return handlers['schedule/create']({
|
|
851
|
+
schedule: partialSchedule,
|
|
852
|
+
conditions: internalSchedule._conditions,
|
|
853
|
+
});
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
handlers['api/schedule-update'] = withMutation(async function ({
|
|
857
|
+
id,
|
|
858
|
+
fields,
|
|
859
|
+
resetNextDate,
|
|
860
|
+
}) {
|
|
861
|
+
checkFileOpen();
|
|
862
|
+
const { data } = await aqlQuery(q('schedules').filter({ id }).select('*'));
|
|
863
|
+
if (!data || data.length === 0) {
|
|
864
|
+
throw APIError(`Schedule ${id} not found`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
const sched = data[0] as ScheduleEntity;
|
|
868
|
+
let conditionsUpdated = false;
|
|
869
|
+
// Find all indices to avoid direct assignment
|
|
870
|
+
const payeeIndex = sched._conditions.findIndex(c => c.field === 'payee');
|
|
871
|
+
const accountIndex = sched._conditions.findIndex(c => c.field === 'account');
|
|
872
|
+
const dateIndex = sched._conditions.findIndex(c => c.field === 'date');
|
|
873
|
+
const amountIndex = sched._conditions.findIndex(c => c.field === 'amount');
|
|
874
|
+
|
|
875
|
+
for (const key in fields) {
|
|
876
|
+
const typedKey = key as keyof APIScheduleEntity;
|
|
877
|
+
const value = fields[typedKey];
|
|
878
|
+
|
|
879
|
+
switch (typedKey) {
|
|
880
|
+
case 'name': {
|
|
881
|
+
const newName = String(value);
|
|
882
|
+
const { data: existing } = await aqlQuery(
|
|
883
|
+
q('schedules').filter({ name: newName }).select('*'),
|
|
884
|
+
);
|
|
885
|
+
if (!existing || existing.length === 0 || existing[0].id === sched.id) {
|
|
886
|
+
sched.name = newName;
|
|
887
|
+
conditionsUpdated = true;
|
|
888
|
+
} else {
|
|
889
|
+
throw APIError(`There is already a schedule named: ${newName}`);
|
|
890
|
+
}
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
case 'next_date':
|
|
894
|
+
case 'completed': {
|
|
895
|
+
throw APIError(
|
|
896
|
+
`Field ${typedKey} is system-managed and not user-editable.`,
|
|
897
|
+
);
|
|
898
|
+
}
|
|
899
|
+
case 'posts_transaction': {
|
|
900
|
+
sched.posts_transaction = Boolean(value);
|
|
901
|
+
conditionsUpdated = true;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
case 'payee': {
|
|
905
|
+
if (payeeIndex !== -1) {
|
|
906
|
+
sched._conditions[payeeIndex].value = value;
|
|
907
|
+
conditionsUpdated = true;
|
|
908
|
+
} else {
|
|
909
|
+
sched._conditions.push({
|
|
910
|
+
field: 'payee',
|
|
911
|
+
op: 'is',
|
|
912
|
+
value: String(value),
|
|
913
|
+
});
|
|
914
|
+
conditionsUpdated = true;
|
|
915
|
+
}
|
|
916
|
+
break;
|
|
917
|
+
}
|
|
918
|
+
case 'account': {
|
|
919
|
+
if (accountIndex !== -1) {
|
|
920
|
+
sched._conditions[accountIndex].value = value;
|
|
921
|
+
conditionsUpdated = true;
|
|
922
|
+
} else {
|
|
923
|
+
sched._conditions.push({
|
|
924
|
+
field: 'account',
|
|
925
|
+
op: 'is',
|
|
926
|
+
value: String(value),
|
|
927
|
+
});
|
|
928
|
+
conditionsUpdated = true;
|
|
929
|
+
}
|
|
930
|
+
break;
|
|
931
|
+
}
|
|
932
|
+
case 'amountOp': {
|
|
933
|
+
if (amountIndex !== -1) {
|
|
934
|
+
let convertedOp: AmountOPType;
|
|
935
|
+
switch (value) {
|
|
936
|
+
case 'is':
|
|
937
|
+
convertedOp = 'is';
|
|
938
|
+
break;
|
|
939
|
+
case 'isapprox':
|
|
940
|
+
convertedOp = 'isapprox';
|
|
941
|
+
break;
|
|
942
|
+
case 'isbetween':
|
|
943
|
+
convertedOp = 'isbetween';
|
|
944
|
+
break;
|
|
945
|
+
default:
|
|
946
|
+
throw APIError(
|
|
947
|
+
`Invalid amount operator: ${String(value)}. Expected: is, isapprox, or isbetween`,
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
sched._conditions[amountIndex].op = convertedOp;
|
|
951
|
+
conditionsUpdated = true;
|
|
952
|
+
} else {
|
|
953
|
+
throw APIError(`Ammount can not be found. There is a bug here`);
|
|
954
|
+
}
|
|
955
|
+
break;
|
|
956
|
+
}
|
|
957
|
+
case 'amount': {
|
|
958
|
+
if (amountIndex !== -1) {
|
|
959
|
+
sched._conditions[amountIndex].value = value;
|
|
960
|
+
conditionsUpdated = true;
|
|
961
|
+
} else {
|
|
962
|
+
throw APIError(`Ammount can not be found. There is a bug here`);
|
|
963
|
+
}
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
case 'date': {
|
|
967
|
+
if (dateIndex !== -1) {
|
|
968
|
+
sched._conditions[dateIndex].value = value;
|
|
969
|
+
conditionsUpdated = true;
|
|
970
|
+
} else {
|
|
971
|
+
throw APIError(
|
|
972
|
+
`Date can not be found. Schedules can not be created without a date there is a bug here`,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
default: {
|
|
978
|
+
throw APIError(`Unhandled field: ${typedKey}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
if (conditionsUpdated) {
|
|
984
|
+
return handlers['schedule/update']({
|
|
985
|
+
schedule: {
|
|
986
|
+
id: sched.id,
|
|
987
|
+
posts_transaction: sched.posts_transaction,
|
|
988
|
+
name: sched.name,
|
|
989
|
+
},
|
|
990
|
+
conditions: sched._conditions,
|
|
991
|
+
resetNextDate,
|
|
992
|
+
});
|
|
993
|
+
} else {
|
|
994
|
+
return sched.id;
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
handlers['api/schedule-delete'] = withMutation(async function (id: string) {
|
|
999
|
+
checkFileOpen();
|
|
1000
|
+
return handlers['schedule/delete']({ id });
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
handlers['api/get-id-by-name'] = async function ({ type, name }) {
|
|
1004
|
+
checkFileOpen();
|
|
1005
|
+
|
|
1006
|
+
const allowedTypes = ['payees', 'categories', 'schedules', 'accounts'];
|
|
1007
|
+
|
|
1008
|
+
if (!allowedTypes.includes(type)) {
|
|
1009
|
+
throw APIError('Provide a valid type');
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const { data } = await aqlQuery(q(type).filter({ name }).select('*'));
|
|
1013
|
+
|
|
1014
|
+
if (!data || data.length === 0) {
|
|
1015
|
+
throw APIError(`Not found: ${type} with name ${name}`);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
return data[0].id;
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
handlers['api/get-server-version'] = async function () {
|
|
1022
|
+
checkFileOpen();
|
|
1023
|
+
return handlers['get-server-version']();
|
|
1024
|
+
};
|
|
1025
|
+
|
|
1026
|
+
export function installAPI(serverHandlers: ServerHandlers) {
|
|
1027
|
+
const merged = Object.assign({}, serverHandlers, handlers);
|
|
1028
|
+
handlers = merged as Handlers;
|
|
1029
|
+
return merged;
|
|
1030
|
+
}
|