@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,436 @@
|
|
|
1
|
+
// @ts-strict-ignore
|
|
2
|
+
import * as dateFns from 'date-fns';
|
|
3
|
+
|
|
4
|
+
import { logger } from '../../platform/server/log';
|
|
5
|
+
import {
|
|
6
|
+
addDays,
|
|
7
|
+
isAfter,
|
|
8
|
+
isBefore,
|
|
9
|
+
monthFromDate,
|
|
10
|
+
parseDate,
|
|
11
|
+
subDays,
|
|
12
|
+
yearFromDate,
|
|
13
|
+
} from '../../shared/months';
|
|
14
|
+
import {
|
|
15
|
+
FIELD_TYPES,
|
|
16
|
+
getApproxNumberThreshold,
|
|
17
|
+
isValidOp,
|
|
18
|
+
sortNumbers,
|
|
19
|
+
} from '../../shared/rules';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
assert,
|
|
23
|
+
parseBetweenAmount,
|
|
24
|
+
parseDateString,
|
|
25
|
+
parseRecurDate,
|
|
26
|
+
} from './rule-utils';
|
|
27
|
+
|
|
28
|
+
export const CONDITION_TYPES = {
|
|
29
|
+
date: {
|
|
30
|
+
ops: ['is', 'isapprox', 'gt', 'gte', 'lt', 'lte'],
|
|
31
|
+
nullable: false,
|
|
32
|
+
parse(op, value, fieldName) {
|
|
33
|
+
const parsed =
|
|
34
|
+
typeof value === 'string'
|
|
35
|
+
? parseDateString(value)
|
|
36
|
+
: value.frequency != null
|
|
37
|
+
? parseRecurDate(value)
|
|
38
|
+
: null;
|
|
39
|
+
assert(
|
|
40
|
+
parsed,
|
|
41
|
+
'date-format',
|
|
42
|
+
`Invalid date format (field: ${fieldName})`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
// Approximate only works with exact & recurring dates
|
|
46
|
+
if (op === 'isapprox') {
|
|
47
|
+
assert(
|
|
48
|
+
parsed.type === 'date' || parsed.type === 'recur',
|
|
49
|
+
'date-format',
|
|
50
|
+
`Invalid date value for "isapprox" (field: ${fieldName})`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
// These only work with exact dates
|
|
54
|
+
else if (op === 'gt' || op === 'gte' || op === 'lt' || op === 'lte') {
|
|
55
|
+
assert(
|
|
56
|
+
parsed.type === 'date',
|
|
57
|
+
'date-format',
|
|
58
|
+
`Invalid date value for "${op}" (field: ${fieldName})`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parsed;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
id: {
|
|
66
|
+
ops: [
|
|
67
|
+
'is',
|
|
68
|
+
'contains',
|
|
69
|
+
'matches',
|
|
70
|
+
'oneOf',
|
|
71
|
+
'isNot',
|
|
72
|
+
'doesNotContain',
|
|
73
|
+
'notOneOf',
|
|
74
|
+
'and',
|
|
75
|
+
'onBudget',
|
|
76
|
+
'offBudget',
|
|
77
|
+
],
|
|
78
|
+
nullable: true,
|
|
79
|
+
parse(op, value, fieldName) {
|
|
80
|
+
if (op === 'oneOf' || op === 'notOneOf' || op === 'and') {
|
|
81
|
+
assert(
|
|
82
|
+
Array.isArray(value),
|
|
83
|
+
'no-empty-array',
|
|
84
|
+
`oneOf must have an array value (field: ${fieldName})`,
|
|
85
|
+
);
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
string: {
|
|
92
|
+
ops: [
|
|
93
|
+
'is',
|
|
94
|
+
'contains',
|
|
95
|
+
'matches',
|
|
96
|
+
'oneOf',
|
|
97
|
+
'isNot',
|
|
98
|
+
'doesNotContain',
|
|
99
|
+
'notOneOf',
|
|
100
|
+
'hasTags',
|
|
101
|
+
],
|
|
102
|
+
nullable: true,
|
|
103
|
+
parse(op, value, fieldName) {
|
|
104
|
+
if (op === 'oneOf' || op === 'notOneOf') {
|
|
105
|
+
assert(
|
|
106
|
+
Array.isArray(value),
|
|
107
|
+
'no-empty-array',
|
|
108
|
+
`oneOf must have an array value (field: ${fieldName}): ${JSON.stringify(
|
|
109
|
+
value,
|
|
110
|
+
)}`,
|
|
111
|
+
);
|
|
112
|
+
return value.filter(Boolean).map(val => val.toLowerCase());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
assert(
|
|
116
|
+
typeof value === 'string',
|
|
117
|
+
'not-string',
|
|
118
|
+
`Invalid string value (field: ${fieldName})`,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
op === 'contains' ||
|
|
123
|
+
op === 'matches' ||
|
|
124
|
+
op === 'doesNotContain' ||
|
|
125
|
+
op === 'hasTags'
|
|
126
|
+
) {
|
|
127
|
+
assert(
|
|
128
|
+
value.length > 0,
|
|
129
|
+
'no-empty-string',
|
|
130
|
+
`${op} must have non-empty string (field: ${fieldName})`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (op === 'hasTags') {
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return value.toLowerCase();
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
number: {
|
|
142
|
+
ops: ['is', 'isapprox', 'isbetween', 'gt', 'gte', 'lt', 'lte'],
|
|
143
|
+
nullable: false,
|
|
144
|
+
parse(op, value, fieldName) {
|
|
145
|
+
const parsed =
|
|
146
|
+
typeof value === 'number'
|
|
147
|
+
? { type: 'literal', value }
|
|
148
|
+
: parseBetweenAmount(value);
|
|
149
|
+
|
|
150
|
+
assert(
|
|
151
|
+
parsed != null,
|
|
152
|
+
'not-number',
|
|
153
|
+
`Value must be a number or between amount: ${JSON.stringify(
|
|
154
|
+
value,
|
|
155
|
+
)} (field: ${fieldName})`,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
if (op === 'isbetween') {
|
|
159
|
+
assert(
|
|
160
|
+
parsed.type === 'between',
|
|
161
|
+
'number-format',
|
|
162
|
+
`Invalid between value for "${op}" (field: ${fieldName})`,
|
|
163
|
+
);
|
|
164
|
+
} else {
|
|
165
|
+
assert(
|
|
166
|
+
parsed.type === 'literal',
|
|
167
|
+
'number-format',
|
|
168
|
+
`Invalid number value for "${op}" (field: ${fieldName})`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return parsed;
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
boolean: {
|
|
176
|
+
ops: ['is'],
|
|
177
|
+
nullable: false,
|
|
178
|
+
parse(op, value, fieldName) {
|
|
179
|
+
assert(
|
|
180
|
+
typeof value === 'boolean',
|
|
181
|
+
'not-boolean',
|
|
182
|
+
`Value must be a boolean: ${value} (field: ${fieldName})`,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return value;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export class Condition {
|
|
191
|
+
field;
|
|
192
|
+
op;
|
|
193
|
+
options;
|
|
194
|
+
rawValue;
|
|
195
|
+
type;
|
|
196
|
+
unparsedValue;
|
|
197
|
+
value;
|
|
198
|
+
|
|
199
|
+
constructor(op, field, value, options) {
|
|
200
|
+
const typeName = FIELD_TYPES.get(field);
|
|
201
|
+
assert(typeName, 'internal', 'Invalid condition field: ' + field);
|
|
202
|
+
|
|
203
|
+
const type = CONDITION_TYPES[typeName];
|
|
204
|
+
|
|
205
|
+
// It's important to validate rules because a faulty rule might mess
|
|
206
|
+
// up the user's transaction (and be very confusing)
|
|
207
|
+
assert(
|
|
208
|
+
type,
|
|
209
|
+
'internal',
|
|
210
|
+
`Invalid condition type: ${typeName} (field: ${field})`,
|
|
211
|
+
);
|
|
212
|
+
assert(
|
|
213
|
+
isValidOp(field, op),
|
|
214
|
+
'internal',
|
|
215
|
+
`Invalid condition operator: ${op} (type: ${typeName}, field: ${field})`,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (type.nullable !== true) {
|
|
219
|
+
assert(value != null, 'no-null', `Field cannot be empty: ${field}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// For strings, an empty string is equal to null
|
|
223
|
+
if (typeName === 'string' && type.nullable !== true) {
|
|
224
|
+
assert(value !== '', 'no-null', `Field cannot be empty: ${field}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
this.rawValue = value;
|
|
228
|
+
this.unparsedValue = value;
|
|
229
|
+
this.op = op;
|
|
230
|
+
this.field = field;
|
|
231
|
+
this.value = type.parse ? type.parse(op, value, field) : value;
|
|
232
|
+
this.options = options;
|
|
233
|
+
this.type = typeName;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
eval(object) {
|
|
237
|
+
let fieldValue = object[this.field];
|
|
238
|
+
const type = this.type;
|
|
239
|
+
|
|
240
|
+
if (type === 'string') {
|
|
241
|
+
fieldValue ??= '';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (fieldValue === undefined) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (typeof fieldValue === 'string') {
|
|
249
|
+
fieldValue = fieldValue.toLowerCase();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (type === 'number' && this.options) {
|
|
253
|
+
if (this.options.outflow) {
|
|
254
|
+
if (fieldValue > 0) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fieldValue = -fieldValue;
|
|
259
|
+
} else if (this.options.inflow) {
|
|
260
|
+
if (fieldValue < 0) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const extractValue = v => (type === 'number' ? v.value : v);
|
|
267
|
+
|
|
268
|
+
switch (this.op) {
|
|
269
|
+
case 'isapprox':
|
|
270
|
+
case 'is':
|
|
271
|
+
if (type === 'date') {
|
|
272
|
+
if (fieldValue == null) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (this.value.type === 'recur') {
|
|
277
|
+
const { schedule } = this.value;
|
|
278
|
+
if (this.op === 'isapprox') {
|
|
279
|
+
const fieldDate = parseDate(fieldValue);
|
|
280
|
+
return schedule.occursBetween(
|
|
281
|
+
dateFns.subDays(fieldDate, 2),
|
|
282
|
+
dateFns.addDays(fieldDate, 2),
|
|
283
|
+
);
|
|
284
|
+
} else {
|
|
285
|
+
return schedule.occursOn({ date: parseDate(fieldValue) });
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
const { date } = this.value;
|
|
289
|
+
|
|
290
|
+
if (this.op === 'isapprox') {
|
|
291
|
+
const fullDate = parseDate(date);
|
|
292
|
+
const high = addDays(fullDate, 2);
|
|
293
|
+
const low = subDays(fullDate, 2);
|
|
294
|
+
|
|
295
|
+
return fieldValue >= low && fieldValue <= high;
|
|
296
|
+
} else {
|
|
297
|
+
switch (this.value.type) {
|
|
298
|
+
case 'date':
|
|
299
|
+
return fieldValue === date;
|
|
300
|
+
case 'month':
|
|
301
|
+
return monthFromDate(fieldValue) === date;
|
|
302
|
+
case 'year':
|
|
303
|
+
return yearFromDate(fieldValue) === date;
|
|
304
|
+
default:
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} else if (type === 'number') {
|
|
309
|
+
const number = this.value.value;
|
|
310
|
+
if (this.op === 'isapprox') {
|
|
311
|
+
const threshold = getApproxNumberThreshold(number);
|
|
312
|
+
return (
|
|
313
|
+
fieldValue >= number - threshold &&
|
|
314
|
+
fieldValue <= number + threshold
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return fieldValue === number;
|
|
318
|
+
}
|
|
319
|
+
return fieldValue === this.value;
|
|
320
|
+
|
|
321
|
+
case 'isNot':
|
|
322
|
+
return fieldValue !== this.value;
|
|
323
|
+
case 'isbetween': {
|
|
324
|
+
// The parsing logic already checks that the value is of the
|
|
325
|
+
// right type (only numbers with high and low)
|
|
326
|
+
const [low, high] = sortNumbers(this.value.num1, this.value.num2);
|
|
327
|
+
return fieldValue >= low && fieldValue <= high;
|
|
328
|
+
}
|
|
329
|
+
case 'contains':
|
|
330
|
+
if (fieldValue === null) {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return String(fieldValue).indexOf(this.value) !== -1;
|
|
334
|
+
case 'doesNotContain':
|
|
335
|
+
if (fieldValue === null) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return String(fieldValue).indexOf(this.value) === -1;
|
|
339
|
+
case 'oneOf':
|
|
340
|
+
if (fieldValue === null) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
return this.value.indexOf(fieldValue) !== -1;
|
|
344
|
+
|
|
345
|
+
case 'hasTags':
|
|
346
|
+
if (fieldValue === null) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
return String(fieldValue).indexOf(this.value) !== -1;
|
|
350
|
+
|
|
351
|
+
case 'notOneOf':
|
|
352
|
+
if (fieldValue === null) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
return this.value.indexOf(fieldValue) === -1;
|
|
356
|
+
case 'gt':
|
|
357
|
+
if (fieldValue === null) {
|
|
358
|
+
return false;
|
|
359
|
+
} else if (type === 'date') {
|
|
360
|
+
return isAfter(fieldValue, this.value.date);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return fieldValue > extractValue(this.value);
|
|
364
|
+
case 'gte':
|
|
365
|
+
if (fieldValue === null) {
|
|
366
|
+
return false;
|
|
367
|
+
} else if (type === 'date') {
|
|
368
|
+
return (
|
|
369
|
+
fieldValue === this.value.date ||
|
|
370
|
+
isAfter(fieldValue, this.value.date)
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return fieldValue >= extractValue(this.value);
|
|
375
|
+
case 'lt':
|
|
376
|
+
if (fieldValue === null) {
|
|
377
|
+
return false;
|
|
378
|
+
} else if (type === 'date') {
|
|
379
|
+
return isBefore(fieldValue, this.value.date);
|
|
380
|
+
}
|
|
381
|
+
return fieldValue < extractValue(this.value);
|
|
382
|
+
case 'lte':
|
|
383
|
+
if (fieldValue === null) {
|
|
384
|
+
return false;
|
|
385
|
+
} else if (type === 'date') {
|
|
386
|
+
return (
|
|
387
|
+
fieldValue === this.value.date ||
|
|
388
|
+
isBefore(fieldValue, this.value.date)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
return fieldValue <= extractValue(this.value);
|
|
392
|
+
case 'matches':
|
|
393
|
+
if (fieldValue === null) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
return new RegExp(this.value).test(fieldValue);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
logger.log('invalid regexp in matches condition', e);
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
case 'onBudget':
|
|
404
|
+
if (!object._account) {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return object._account.offbudget === 0;
|
|
409
|
+
|
|
410
|
+
case 'offBudget':
|
|
411
|
+
if (!object._account) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return object._account.offbudget === 1;
|
|
416
|
+
|
|
417
|
+
default:
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
getValue() {
|
|
424
|
+
return this.value;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
serialize() {
|
|
428
|
+
return {
|
|
429
|
+
op: this.op,
|
|
430
|
+
field: this.field,
|
|
431
|
+
value: this.unparsedValue,
|
|
432
|
+
type: this.type,
|
|
433
|
+
...(this.options ? { options: this.options } : null),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { FunctionArgumentType, FunctionPlugin } from 'hyperformula';
|
|
2
|
+
import type { InterpreterState } from 'hyperformula/typings/interpreter/InterpreterState';
|
|
3
|
+
import type { ProcedureAst } from 'hyperformula/typings/parser';
|
|
4
|
+
|
|
5
|
+
import { integerToAmount } from '../../shared/util';
|
|
6
|
+
|
|
7
|
+
export class CustomFunctionsPlugin extends FunctionPlugin {
|
|
8
|
+
integerToAmount(ast: ProcedureAst, state: InterpreterState) {
|
|
9
|
+
return this.runFunction(
|
|
10
|
+
ast.args,
|
|
11
|
+
state,
|
|
12
|
+
this.metadata('INTEGER_TO_AMOUNT'),
|
|
13
|
+
(integerAmount: number, decimalPlaces: number = 2) => {
|
|
14
|
+
return integerToAmount(integerAmount, decimalPlaces);
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
fixed(ast: ProcedureAst, state: InterpreterState) {
|
|
20
|
+
return this.runFunction(
|
|
21
|
+
ast.args,
|
|
22
|
+
state,
|
|
23
|
+
this.metadata('FIXED'),
|
|
24
|
+
(number: number, decimals: number = 0) => {
|
|
25
|
+
return Number(number).toFixed(decimals);
|
|
26
|
+
},
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
CustomFunctionsPlugin.implementedFunctions = {
|
|
32
|
+
INTEGER_TO_AMOUNT: {
|
|
33
|
+
method: 'integerToAmount',
|
|
34
|
+
parameters: [
|
|
35
|
+
{ argumentType: FunctionArgumentType.NUMBER },
|
|
36
|
+
{
|
|
37
|
+
argumentType: FunctionArgumentType.NUMBER,
|
|
38
|
+
optionalArg: true,
|
|
39
|
+
defaultValue: 2,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
FIXED: {
|
|
44
|
+
method: 'fixed',
|
|
45
|
+
parameters: [
|
|
46
|
+
{ argumentType: FunctionArgumentType.NUMBER },
|
|
47
|
+
{
|
|
48
|
+
argumentType: FunctionArgumentType.NUMBER,
|
|
49
|
+
optionalArg: true,
|
|
50
|
+
defaultValue: 0,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const customFunctionsTranslations = {
|
|
57
|
+
enUS: {
|
|
58
|
+
INTEGER_TO_AMOUNT: 'INTEGER_TO_AMOUNT',
|
|
59
|
+
FIXED: 'FIXED',
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import type { TransactionForRules } from '../transactions/transaction-rules';
|
|
4
|
+
|
|
5
|
+
import { Action } from './action';
|
|
6
|
+
|
|
7
|
+
describe('Formula-based rule actions', () => {
|
|
8
|
+
it('should execute a simple math formula', () => {
|
|
9
|
+
const action = new Action('set', 'amount', null, {});
|
|
10
|
+
const transaction: Partial<TransactionForRules> = { amount: 500 };
|
|
11
|
+
const result = action.executeFormulaSync('=100 + 200', transaction);
|
|
12
|
+
|
|
13
|
+
expect(result).toBe(30000);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should use transaction field variables', () => {
|
|
17
|
+
const action = new Action('set', 'notes', null, {});
|
|
18
|
+
const transaction = { amount: 5000 };
|
|
19
|
+
const result = action.executeFormulaSync('=amount / 100', transaction);
|
|
20
|
+
|
|
21
|
+
expect(result).toBe(5000);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should support IF function with transaction fields', () => {
|
|
25
|
+
const action = new Action('set', 'notes', null, {});
|
|
26
|
+
|
|
27
|
+
const transaction1 = { amount: 1000 };
|
|
28
|
+
const result1 = action.executeFormulaSync(
|
|
29
|
+
'=IF(amount > 0, "Income", "Expense")',
|
|
30
|
+
transaction1,
|
|
31
|
+
);
|
|
32
|
+
expect(result1).toBe('Income');
|
|
33
|
+
|
|
34
|
+
const transaction2 = { amount: -1000 };
|
|
35
|
+
const result2 = action.executeFormulaSync(
|
|
36
|
+
'=IF(amount > 0, "Income", "Expense")',
|
|
37
|
+
transaction2,
|
|
38
|
+
);
|
|
39
|
+
expect(result2).toBe('Expense');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should support UPPER string function', () => {
|
|
43
|
+
const action = new Action('set', 'notes', null, {});
|
|
44
|
+
const transaction = { notes: 'hello world' };
|
|
45
|
+
const result = action.executeFormulaSync('=UPPER(notes)', transaction);
|
|
46
|
+
|
|
47
|
+
expect(result).toBe('HELLO WORLD');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should support CONCATENATE function', () => {
|
|
51
|
+
const action = new Action('set', 'notes', null, {});
|
|
52
|
+
const transaction = {
|
|
53
|
+
imported_payee: 'Store Name',
|
|
54
|
+
notes: 'Purchase',
|
|
55
|
+
};
|
|
56
|
+
const result = action.executeFormulaSync(
|
|
57
|
+
'=CONCATENATE(imported_payee, " - ", notes)',
|
|
58
|
+
transaction,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(result).toBe('Store Name - Purchase');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should support LEFT function', () => {
|
|
65
|
+
const action = new Action('set', 'notes', null, {});
|
|
66
|
+
const transaction = { imported_payee: 'Store Name' };
|
|
67
|
+
const result = action.executeFormulaSync(
|
|
68
|
+
'=LEFT(imported_payee, 5)',
|
|
69
|
+
transaction,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
expect(result).toBe('Store');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should provide today variable', () => {
|
|
76
|
+
const action = new Action('set', 'date', null, {});
|
|
77
|
+
const transaction = { date: '2024-01-01' };
|
|
78
|
+
const result = action.executeFormulaSync('=today', transaction);
|
|
79
|
+
|
|
80
|
+
// Should be a date string in YYYY-MM-DD format
|
|
81
|
+
expect(typeof result).toBe('string');
|
|
82
|
+
expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw error for invalid formula', () => {
|
|
86
|
+
const action = new Action('set', 'notes', null, {});
|
|
87
|
+
const transaction = { amount: 100 };
|
|
88
|
+
|
|
89
|
+
expect(() => {
|
|
90
|
+
action.executeFormulaSync('=INVALID_FUNCTION()', transaction);
|
|
91
|
+
}).toThrow();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should throw error for formula without = prefix', () => {
|
|
95
|
+
const action = new Action('set', 'notes', null, {});
|
|
96
|
+
const transaction = { amount: 100 };
|
|
97
|
+
|
|
98
|
+
expect(() => {
|
|
99
|
+
action.executeFormulaSync('amount + 100', transaction);
|
|
100
|
+
}).toThrow('Formula must start with =');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should support balance field variable', () => {
|
|
104
|
+
const action = new Action('set', 'notes', null, {});
|
|
105
|
+
const transaction: Partial<TransactionForRules> = {
|
|
106
|
+
balance: 1500,
|
|
107
|
+
notes: 'original',
|
|
108
|
+
};
|
|
109
|
+
const result = action.executeFormulaSync('=balance * 2', transaction);
|
|
110
|
+
|
|
111
|
+
expect(result).toBe(300000);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should execute formula and convert to number type', () => {
|
|
115
|
+
const action = new Action('set', 'amount', null, {
|
|
116
|
+
formula: '=500 + 250',
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const transaction = { amount: 100 };
|
|
120
|
+
action.exec(transaction);
|
|
121
|
+
|
|
122
|
+
expect(transaction.amount).toBe(75000);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should execute formula and convert to string type', () => {
|
|
126
|
+
const action = new Action('set', 'notes', null, {
|
|
127
|
+
formula: '=UPPER("hello")',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const transaction = { notes: 'original' };
|
|
131
|
+
action.exec(transaction);
|
|
132
|
+
|
|
133
|
+
expect(transaction.notes).toBe('HELLO');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should handle formula errors gracefully', () => {
|
|
137
|
+
const action = new Action('set', 'notes', null, {
|
|
138
|
+
formula: '=1/0', // Division by zero
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const transaction = { notes: 'original' };
|
|
142
|
+
|
|
143
|
+
// Should not throw, but keep original value on error
|
|
144
|
+
expect(() => {
|
|
145
|
+
action.exec(transaction);
|
|
146
|
+
}).not.toThrow();
|
|
147
|
+
|
|
148
|
+
// Original value should be preserved
|
|
149
|
+
expect(transaction.notes).toBe('original');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('should validate numeric field output', () => {
|
|
153
|
+
const action = new Action('set', 'amount', null, {
|
|
154
|
+
formula: '=UPPER("test")', // Returns string for numeric field
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const transaction = { amount: 100 };
|
|
158
|
+
action.exec(transaction);
|
|
159
|
+
|
|
160
|
+
// Should keep original value when formula produces non-numeric result
|
|
161
|
+
expect(transaction.amount).toBe(100);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should convert non-string results to string for text fields', () => {
|
|
165
|
+
const action = new Action('set', 'notes', null, {
|
|
166
|
+
formula: '=500 + 250', // Returns number
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const transaction = { notes: 'original' };
|
|
170
|
+
action.exec(transaction);
|
|
171
|
+
|
|
172
|
+
// Should convert number to string
|
|
173
|
+
expect(transaction.notes).toBe('75000');
|
|
174
|
+
});
|
|
175
|
+
});
|