@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,391 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import * as d from 'date-fns';
|
|
3
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
|
|
5
|
+
import { dayFromDate, parseDate } from '../../shared/months';
|
|
6
|
+
import { q } from '../../shared/query';
|
|
7
|
+
import { getApproxNumberThreshold } from '../../shared/rules';
|
|
8
|
+
import { recurConfigToRSchedule } from '../../shared/schedules';
|
|
9
|
+
import { groupBy } from '../../shared/util';
|
|
10
|
+
import { aqlQuery } from '../aql';
|
|
11
|
+
import * as db from '../db';
|
|
12
|
+
import { fromDateRepr } from '../models';
|
|
13
|
+
import { conditionsToAQL } from '../transactions/transaction-rules';
|
|
14
|
+
import { RSchedule } from '../util/rschedule';
|
|
15
|
+
|
|
16
|
+
function takeDates(config) {
|
|
17
|
+
const schedule = new RSchedule({ rrules: recurConfigToRSchedule(config) });
|
|
18
|
+
return schedule
|
|
19
|
+
.occurrences({ take: 3 })
|
|
20
|
+
.toArray()
|
|
21
|
+
.map(d => d.date);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function getTransactions(date, account) {
|
|
25
|
+
const { data } = await aqlQuery(
|
|
26
|
+
q('transactions')
|
|
27
|
+
.filter({
|
|
28
|
+
account,
|
|
29
|
+
schedule: null,
|
|
30
|
+
// Don't match transfers
|
|
31
|
+
'payee.transfer_acct': null,
|
|
32
|
+
$and: [
|
|
33
|
+
{ date: { $gte: d.subDays(date, 2) } },
|
|
34
|
+
{ date: { $lte: d.addDays(date, 2) } },
|
|
35
|
+
],
|
|
36
|
+
})
|
|
37
|
+
.select('*')
|
|
38
|
+
.options({ splits: 'none' }),
|
|
39
|
+
);
|
|
40
|
+
return data;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getRank(day1, day2) {
|
|
44
|
+
const dayDiff = Math.abs(
|
|
45
|
+
d.differenceInDays(parseDate(day1), parseDate(day2)),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// The amount of days off determines the rank: exact same day
|
|
49
|
+
// is highest rank 1, 1 day off is .5, etc. This will find the
|
|
50
|
+
// best start date that matches all the dates the closest
|
|
51
|
+
return 1 / (dayDiff + 1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function matchSchedules(allOccurs, config) {
|
|
55
|
+
allOccurs = [...allOccurs].reverse();
|
|
56
|
+
const baseOccur = allOccurs[0];
|
|
57
|
+
const occurs = allOccurs.slice(1);
|
|
58
|
+
const schedules = [];
|
|
59
|
+
|
|
60
|
+
for (const trans of baseOccur.transactions) {
|
|
61
|
+
const threshold = getApproxNumberThreshold(trans.amount);
|
|
62
|
+
const payee = trans.payee;
|
|
63
|
+
|
|
64
|
+
const found = occurs.map(occur => {
|
|
65
|
+
let matched = occur.transactions.find(
|
|
66
|
+
t =>
|
|
67
|
+
t.amount >= trans.amount - threshold &&
|
|
68
|
+
t.amount <= trans.amount + threshold,
|
|
69
|
+
);
|
|
70
|
+
matched = matched && matched.payee === payee ? matched : null;
|
|
71
|
+
|
|
72
|
+
if (matched) {
|
|
73
|
+
return { trans: matched, rank: getRank(occur.date, matched.date) };
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (found.indexOf(null) !== -1) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const rank = found.reduce(
|
|
83
|
+
(total, match) => total + match.rank,
|
|
84
|
+
getRank(baseOccur.date, trans.date),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const exactAmount = found.reduce(
|
|
88
|
+
(exact, match) => exact && match.trans.amount === trans.amount,
|
|
89
|
+
true,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
schedules.push({
|
|
93
|
+
rank,
|
|
94
|
+
amount: trans.amount,
|
|
95
|
+
account: trans.account,
|
|
96
|
+
payee: trans.payee,
|
|
97
|
+
date: config,
|
|
98
|
+
// Exact dates rank as 1, so all of them matches exactly it
|
|
99
|
+
// would equal the number of `allOccurs`
|
|
100
|
+
exactDate: rank === allOccurs.length,
|
|
101
|
+
exactAmount,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return schedules;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function schedulesForPattern(baseStart, numDays, baseConfig, accountId) {
|
|
109
|
+
let schedules = [];
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < numDays; i++) {
|
|
112
|
+
const start = d.addDays(baseStart, i);
|
|
113
|
+
let config;
|
|
114
|
+
if (typeof baseConfig === 'function') {
|
|
115
|
+
config = baseConfig(start);
|
|
116
|
+
|
|
117
|
+
if (config === false) {
|
|
118
|
+
// Skip this one
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
config = { ...baseConfig, start };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Our recur config expects a day string, not a native date format
|
|
126
|
+
config.start = dayFromDate(config.start);
|
|
127
|
+
|
|
128
|
+
const data = [];
|
|
129
|
+
const dates = takeDates(config);
|
|
130
|
+
for (const date of dates) {
|
|
131
|
+
data.push({
|
|
132
|
+
date: dayFromDate(date),
|
|
133
|
+
transactions: await getTransactions(date, accountId),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
schedules = schedules.concat(matchSchedules(data, config));
|
|
138
|
+
}
|
|
139
|
+
return schedules;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function weekly(startDate, accountId) {
|
|
143
|
+
return schedulesForPattern(
|
|
144
|
+
d.subWeeks(parseDate(startDate), 4),
|
|
145
|
+
7 * 2,
|
|
146
|
+
{ frequency: 'weekly' },
|
|
147
|
+
accountId,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function every2weeks(startDate, accountId) {
|
|
152
|
+
return schedulesForPattern(
|
|
153
|
+
// 6 weeks would cover 3 instances, but we also scan an addition
|
|
154
|
+
// week back
|
|
155
|
+
d.subWeeks(parseDate(startDate), 7),
|
|
156
|
+
7 * 2,
|
|
157
|
+
{ frequency: 'weekly', interval: 2 },
|
|
158
|
+
accountId,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function monthly(startDate, accountId) {
|
|
163
|
+
return schedulesForPattern(
|
|
164
|
+
d.subMonths(parseDate(startDate), 4),
|
|
165
|
+
31 * 2,
|
|
166
|
+
start => {
|
|
167
|
+
// 28 is the max number of days that all months are guaranteed
|
|
168
|
+
// to have. We don't want to go any higher than that because
|
|
169
|
+
// we'll end up skipping months that don't have that day.
|
|
170
|
+
// The use cases of end of month days will be covered with the
|
|
171
|
+
// `monthlyLastDay` pattern;
|
|
172
|
+
if (d.getDate(start) > 28) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
return { start, frequency: 'monthly' };
|
|
176
|
+
},
|
|
177
|
+
accountId,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function monthlyLastDay(startDate, accountId) {
|
|
182
|
+
// We do two separate calls because this pattern doesn't fit into
|
|
183
|
+
// how `schedulesForPattern` works
|
|
184
|
+
const s1 = await schedulesForPattern(
|
|
185
|
+
d.subMonths(parseDate(startDate), 3),
|
|
186
|
+
1,
|
|
187
|
+
{ frequency: 'monthly', patterns: [{ type: 'day', value: -1 }] },
|
|
188
|
+
accountId,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const s2 = await schedulesForPattern(
|
|
192
|
+
d.subMonths(parseDate(startDate), 4),
|
|
193
|
+
1,
|
|
194
|
+
{ frequency: 'monthly', patterns: [{ type: 'day', value: -1 }] },
|
|
195
|
+
accountId,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return s1.concat(s2);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async function monthly1stor3rd(startDate, accountId) {
|
|
202
|
+
return schedulesForPattern(
|
|
203
|
+
d.subWeeks(parseDate(startDate), 8),
|
|
204
|
+
14,
|
|
205
|
+
start => {
|
|
206
|
+
const day = d.format(new Date(), 'iiii');
|
|
207
|
+
const dayValue = day.slice(0, 2).toUpperCase();
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
start,
|
|
211
|
+
frequency: 'monthly',
|
|
212
|
+
patterns: [
|
|
213
|
+
{ type: dayValue, value: 1 },
|
|
214
|
+
{ type: dayValue, value: 3 },
|
|
215
|
+
],
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
accountId,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function monthly2ndor4th(startDate, accountId) {
|
|
223
|
+
return schedulesForPattern(
|
|
224
|
+
d.subMonths(parseDate(startDate), 8),
|
|
225
|
+
14,
|
|
226
|
+
start => {
|
|
227
|
+
const day = d.format(new Date(), 'iiii');
|
|
228
|
+
const dayValue = day.slice(0, 2).toUpperCase();
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
start,
|
|
232
|
+
frequency: 'monthly',
|
|
233
|
+
patterns: [
|
|
234
|
+
{ type: dayValue, value: 2 },
|
|
235
|
+
{ type: dayValue, value: 4 },
|
|
236
|
+
],
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
accountId,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function findStartDate(schedule) {
|
|
244
|
+
const conditions = schedule._conditions;
|
|
245
|
+
const dateCond = conditions.find(c => c.field === 'date');
|
|
246
|
+
let currentConfig = dateCond.value;
|
|
247
|
+
|
|
248
|
+
while (true) {
|
|
249
|
+
const prevConfig = currentConfig;
|
|
250
|
+
currentConfig = { ...prevConfig };
|
|
251
|
+
|
|
252
|
+
switch (currentConfig.frequency) {
|
|
253
|
+
case 'weekly':
|
|
254
|
+
currentConfig.start = dayFromDate(
|
|
255
|
+
d.subWeeks(
|
|
256
|
+
parseDate(currentConfig.start),
|
|
257
|
+
currentConfig.interval || 1,
|
|
258
|
+
),
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
break;
|
|
262
|
+
case 'monthly':
|
|
263
|
+
currentConfig.start = dayFromDate(
|
|
264
|
+
d.subMonths(
|
|
265
|
+
parseDate(currentConfig.start),
|
|
266
|
+
currentConfig.interval || 1,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
break;
|
|
270
|
+
case 'yearly':
|
|
271
|
+
currentConfig.start = dayFromDate(
|
|
272
|
+
d.subYears(
|
|
273
|
+
parseDate(currentConfig.start),
|
|
274
|
+
currentConfig.interval || 1,
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
break;
|
|
278
|
+
default:
|
|
279
|
+
throw new Error('findStartDate: invalid frequency');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const newConditions = conditions.map(c =>
|
|
283
|
+
c.field === 'date' ? { ...c, value: currentConfig } : c,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const { filters, errors } = conditionsToAQL(newConditions, {
|
|
287
|
+
recurDateBounds: 1,
|
|
288
|
+
});
|
|
289
|
+
if (errors.length > 0) {
|
|
290
|
+
// Somehow we generated an invalid config. Abort the whole
|
|
291
|
+
// process and don't change the date at all
|
|
292
|
+
currentConfig = null;
|
|
293
|
+
break;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const { data } = await aqlQuery(
|
|
297
|
+
q('transactions').filter({ $and: filters }).select('*'),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (data.length === 0) {
|
|
301
|
+
// No data, revert back to the last valid value and stop
|
|
302
|
+
currentConfig = prevConfig;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (currentConfig) {
|
|
308
|
+
return {
|
|
309
|
+
...schedule,
|
|
310
|
+
date: currentConfig,
|
|
311
|
+
_conditions: conditions.map(c =>
|
|
312
|
+
c.field === 'date' ? { ...c, value: currentConfig } : c,
|
|
313
|
+
),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return schedule;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export async function findSchedules() {
|
|
320
|
+
// Patterns to look for:
|
|
321
|
+
// * Weekly
|
|
322
|
+
// * Every two weeks
|
|
323
|
+
// * Monthly on day X
|
|
324
|
+
// * Monthly on every 1st or 3rd day
|
|
325
|
+
// * Monthly on every 2nd or 4th day
|
|
326
|
+
//
|
|
327
|
+
// Search for them approx (+- 2 days) but track which transactions
|
|
328
|
+
// and find the best one...
|
|
329
|
+
|
|
330
|
+
const { data: accounts } = await aqlQuery(
|
|
331
|
+
q('accounts').filter({ closed: false }).select('*'),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
let allSchedules = [];
|
|
335
|
+
|
|
336
|
+
for (const account of accounts) {
|
|
337
|
+
// Find latest transaction-ish to start with
|
|
338
|
+
const latestTrans = await db.first<Pick<db.DbViewTransaction, 'date'>>(
|
|
339
|
+
'SELECT date FROM v_transactions WHERE account = ? AND parent_id IS NULL ORDER BY date DESC LIMIT 1',
|
|
340
|
+
[account.id],
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
if (latestTrans) {
|
|
344
|
+
const latestDate = fromDateRepr(latestTrans.date);
|
|
345
|
+
allSchedules = allSchedules.concat(
|
|
346
|
+
await weekly(latestDate, account.id),
|
|
347
|
+
await every2weeks(latestDate, account.id),
|
|
348
|
+
await monthly(latestDate, account.id),
|
|
349
|
+
await monthlyLastDay(latestDate, account.id),
|
|
350
|
+
await monthly1stor3rd(latestDate, account.id),
|
|
351
|
+
await monthly2ndor4th(latestDate, account.id),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const schedules = [...groupBy(allSchedules, 'payee').entries()].map(
|
|
357
|
+
([, schedules]) => {
|
|
358
|
+
schedules.sort((s1, s2) => s2.rank - s1.rank);
|
|
359
|
+
const winner = schedules[0];
|
|
360
|
+
|
|
361
|
+
// Convert to schedule and return it
|
|
362
|
+
return {
|
|
363
|
+
id: uuidv4(),
|
|
364
|
+
account: winner.account,
|
|
365
|
+
payee: winner.payee,
|
|
366
|
+
date: winner.date,
|
|
367
|
+
amount: winner.amount,
|
|
368
|
+
_conditions: [
|
|
369
|
+
{ op: 'is', field: 'account', value: winner.account },
|
|
370
|
+
{ op: 'is', field: 'payee', value: winner.payee },
|
|
371
|
+
{
|
|
372
|
+
op: winner.exactDate ? 'is' : 'isapprox',
|
|
373
|
+
field: 'date',
|
|
374
|
+
value: winner.date,
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
op: winner.exactAmount ? 'is' : 'isapprox',
|
|
378
|
+
field: 'amount',
|
|
379
|
+
value: winner.amount,
|
|
380
|
+
},
|
|
381
|
+
],
|
|
382
|
+
};
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
const finalized: Awaited<ReturnType<typeof findStartDate>> = [];
|
|
387
|
+
for (const schedule of schedules) {
|
|
388
|
+
finalized.push(await findStartDate(schedule));
|
|
389
|
+
}
|
|
390
|
+
return finalized;
|
|
391
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import * as fs from '../platform/server/fs';
|
|
2
|
+
import { logger } from '../platform/server/log';
|
|
3
|
+
|
|
4
|
+
type ServerConfig = {
|
|
5
|
+
BASE_SERVER: string;
|
|
6
|
+
SYNC_SERVER: string;
|
|
7
|
+
SIGNUP_SERVER: string;
|
|
8
|
+
GOCARDLESS_SERVER: string;
|
|
9
|
+
SIMPLEFIN_SERVER: string;
|
|
10
|
+
PLUGGYAI_SERVER: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
let config: ServerConfig | null = null;
|
|
14
|
+
|
|
15
|
+
function joinURL(base: string | URL, ...paths: string[]): string {
|
|
16
|
+
const url = new URL(base);
|
|
17
|
+
url.pathname = fs.join(url.pathname, ...paths);
|
|
18
|
+
return url.toString();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isValidBaseURL(base: string): boolean {
|
|
22
|
+
try {
|
|
23
|
+
return Boolean(new URL(base));
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setServer(url: string): void {
|
|
30
|
+
if (url == null) {
|
|
31
|
+
config = null;
|
|
32
|
+
} else {
|
|
33
|
+
config = getServer(url);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// `url` is optional; if not given it will provide the global config
|
|
38
|
+
export function getServer(url?: string): ServerConfig | null {
|
|
39
|
+
if (url) {
|
|
40
|
+
try {
|
|
41
|
+
return {
|
|
42
|
+
BASE_SERVER: url,
|
|
43
|
+
SYNC_SERVER: joinURL(url, '/sync'),
|
|
44
|
+
SIGNUP_SERVER: joinURL(url, '/account'),
|
|
45
|
+
GOCARDLESS_SERVER: joinURL(url, '/gocardless'),
|
|
46
|
+
SIMPLEFIN_SERVER: joinURL(url, '/simplefin'),
|
|
47
|
+
PLUGGYAI_SERVER: joinURL(url, '/pluggyai'),
|
|
48
|
+
};
|
|
49
|
+
} catch (error) {
|
|
50
|
+
logger.warn(
|
|
51
|
+
'Unable to parse server URL - using the global config.',
|
|
52
|
+
{ config },
|
|
53
|
+
error,
|
|
54
|
+
);
|
|
55
|
+
return config;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return config;
|
|
59
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import { generateTransaction } from '../mocks';
|
|
3
|
+
|
|
4
|
+
import * as db from './db';
|
|
5
|
+
import * as sheet from './sheet';
|
|
6
|
+
|
|
7
|
+
beforeEach(global.emptyDatabase());
|
|
8
|
+
|
|
9
|
+
async function insertTransactions() {
|
|
10
|
+
await db.insertCategoryGroup({ id: 'group1', name: 'group1' });
|
|
11
|
+
await db.insertCategory({ id: 'cat1', name: 'cat1', cat_group: 'group1' });
|
|
12
|
+
await db.insertCategory({ id: 'cat2', name: 'cat2', cat_group: 'group1' });
|
|
13
|
+
|
|
14
|
+
await db.insertTransaction(
|
|
15
|
+
generateTransaction({
|
|
16
|
+
id: 'trans1',
|
|
17
|
+
amount: -3200,
|
|
18
|
+
account: '1',
|
|
19
|
+
category: 'cat1',
|
|
20
|
+
date: '2017-01-08',
|
|
21
|
+
})[0],
|
|
22
|
+
);
|
|
23
|
+
await db.insertTransaction(
|
|
24
|
+
generateTransaction({
|
|
25
|
+
id: 'trans2',
|
|
26
|
+
amount: -2800,
|
|
27
|
+
account: '1',
|
|
28
|
+
category: 'cat2',
|
|
29
|
+
date: '2017-01-10',
|
|
30
|
+
})[0],
|
|
31
|
+
);
|
|
32
|
+
await db.insertTransaction(
|
|
33
|
+
generateTransaction({
|
|
34
|
+
id: 'trans3',
|
|
35
|
+
amount: -9832,
|
|
36
|
+
account: '1',
|
|
37
|
+
category: 'cat2',
|
|
38
|
+
date: '2017-01-15',
|
|
39
|
+
})[0],
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('Spreadsheet', () => {
|
|
44
|
+
test('transferring a category triggers an update', async () => {
|
|
45
|
+
const spreadsheet = await sheet.loadSpreadsheet(db);
|
|
46
|
+
await insertTransactions();
|
|
47
|
+
|
|
48
|
+
spreadsheet.startTransaction();
|
|
49
|
+
spreadsheet.set(
|
|
50
|
+
'g!foo',
|
|
51
|
+
`=from transactions where category = "cat2" calculate { sum(amount) }`,
|
|
52
|
+
);
|
|
53
|
+
spreadsheet.endTransaction();
|
|
54
|
+
|
|
55
|
+
await new Promise(resolve => {
|
|
56
|
+
spreadsheet.onFinish(() => {
|
|
57
|
+
expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
|
|
58
|
+
resolve(undefined);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await db.deleteCategory({ id: 'cat1' }, 'cat2');
|
|
63
|
+
|
|
64
|
+
return new Promise(resolve => {
|
|
65
|
+
spreadsheet.onFinish(() => {
|
|
66
|
+
expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
|
|
67
|
+
resolve(undefined);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('updating still works after transferring categories', async () => {
|
|
73
|
+
const spreadsheet = await sheet.loadSpreadsheet(db);
|
|
74
|
+
await insertTransactions();
|
|
75
|
+
|
|
76
|
+
await db.deleteCategory({ id: 'cat1' }, 'cat2');
|
|
77
|
+
|
|
78
|
+
spreadsheet.startTransaction();
|
|
79
|
+
spreadsheet.set(
|
|
80
|
+
'g!foo',
|
|
81
|
+
`=from transactions where category = "cat2" calculate { sum(amount) }`,
|
|
82
|
+
);
|
|
83
|
+
spreadsheet.endTransaction();
|
|
84
|
+
|
|
85
|
+
await new Promise(resolve => {
|
|
86
|
+
spreadsheet.onFinish(() => {
|
|
87
|
+
expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
|
|
88
|
+
resolve(undefined);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await db.updateTransaction({ id: 'trans1', amount: 50000 });
|
|
93
|
+
|
|
94
|
+
await new Promise(resolve => {
|
|
95
|
+
spreadsheet.onFinish(() => {
|
|
96
|
+
expect(spreadsheet.getValue('g!foo')).toMatchSnapshot();
|
|
97
|
+
resolve(undefined);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|