@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,1658 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { amountToInteger } from '../../shared/util';
|
|
4
|
+
import type { CategoryEntity } from '../../types/models';
|
|
5
|
+
import type { Template } from '../../types/models/templates';
|
|
6
|
+
import * as aql from '../aql';
|
|
7
|
+
import * as db from '../db';
|
|
8
|
+
|
|
9
|
+
import * as actions from './actions';
|
|
10
|
+
import { CategoryTemplateContext } from './category-template-context';
|
|
11
|
+
|
|
12
|
+
// Mock getSheetValue and getCategories
|
|
13
|
+
vi.mock('./actions', () => ({
|
|
14
|
+
getSheetValue: vi.fn(),
|
|
15
|
+
getSheetBoolean: vi.fn(),
|
|
16
|
+
isReflectBudget: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('../db', () => ({
|
|
20
|
+
getCategories: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('../aql', () => ({
|
|
24
|
+
aqlQuery: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// Helper function to mock preferences (hideFraction and defaultCurrencyCode)
|
|
28
|
+
function mockPreferences(
|
|
29
|
+
hideFraction: boolean = false,
|
|
30
|
+
currencyCode: string = 'USD',
|
|
31
|
+
) {
|
|
32
|
+
vi.mocked(aql.aqlQuery).mockImplementation(async (query: unknown) => {
|
|
33
|
+
const queryStr = JSON.stringify(query);
|
|
34
|
+
if (queryStr.includes('hideFraction')) {
|
|
35
|
+
return {
|
|
36
|
+
data: [{ value: hideFraction ? 'true' : 'false' }],
|
|
37
|
+
dependencies: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (queryStr.includes('defaultCurrencyCode')) {
|
|
41
|
+
return {
|
|
42
|
+
data: currencyCode ? [{ value: currencyCode }] : [],
|
|
43
|
+
dependencies: [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { data: [], dependencies: [] };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Test helper class to access constructor and methods
|
|
51
|
+
class TestCategoryTemplateContext extends CategoryTemplateContext {
|
|
52
|
+
public constructor(
|
|
53
|
+
templates: Template[],
|
|
54
|
+
category: CategoryEntity,
|
|
55
|
+
month: string,
|
|
56
|
+
fromLastMonth: number,
|
|
57
|
+
budgeted: number,
|
|
58
|
+
currencyCode: string = 'USD',
|
|
59
|
+
) {
|
|
60
|
+
super(templates, category, month, fromLastMonth, budgeted, currencyCode);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe('CategoryTemplateContext', () => {
|
|
65
|
+
describe('runSimple', () => {
|
|
66
|
+
it('should return monthly amount when provided', () => {
|
|
67
|
+
const category: CategoryEntity = {
|
|
68
|
+
id: 'test',
|
|
69
|
+
name: 'Test Category',
|
|
70
|
+
group: 'test-group',
|
|
71
|
+
is_income: false,
|
|
72
|
+
};
|
|
73
|
+
const template: Template = {
|
|
74
|
+
type: 'simple',
|
|
75
|
+
monthly: 100,
|
|
76
|
+
directive: 'template',
|
|
77
|
+
priority: 1,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const instance = new TestCategoryTemplateContext(
|
|
81
|
+
[],
|
|
82
|
+
category,
|
|
83
|
+
'2024-01',
|
|
84
|
+
0,
|
|
85
|
+
0,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const result = CategoryTemplateContext.runSimple(template, instance);
|
|
89
|
+
expect(result).toBe(amountToInteger(100));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return limit when monthly is not provided', () => {
|
|
93
|
+
const category: CategoryEntity = {
|
|
94
|
+
id: 'test',
|
|
95
|
+
name: 'Test Category',
|
|
96
|
+
group: 'test-group',
|
|
97
|
+
is_income: false,
|
|
98
|
+
};
|
|
99
|
+
const template: Template = {
|
|
100
|
+
type: 'simple',
|
|
101
|
+
limit: { amount: 500, hold: false, period: 'monthly' },
|
|
102
|
+
directive: 'template',
|
|
103
|
+
priority: 1,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const instance = new TestCategoryTemplateContext(
|
|
107
|
+
[template],
|
|
108
|
+
category,
|
|
109
|
+
'2024-01',
|
|
110
|
+
0,
|
|
111
|
+
0,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const result = CategoryTemplateContext.runSimple(template, instance);
|
|
115
|
+
expect(result).toBe(amountToInteger(500));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('should handle weekly limit', async () => {
|
|
119
|
+
const category: CategoryEntity = {
|
|
120
|
+
id: 'test',
|
|
121
|
+
name: 'Test Category',
|
|
122
|
+
group: 'test-group',
|
|
123
|
+
is_income: false,
|
|
124
|
+
};
|
|
125
|
+
const template: Template = {
|
|
126
|
+
type: 'simple',
|
|
127
|
+
limit: {
|
|
128
|
+
amount: 100,
|
|
129
|
+
hold: false,
|
|
130
|
+
period: 'weekly',
|
|
131
|
+
start: '2024-01-01',
|
|
132
|
+
},
|
|
133
|
+
directive: 'template',
|
|
134
|
+
priority: 1,
|
|
135
|
+
};
|
|
136
|
+
const instance = new TestCategoryTemplateContext(
|
|
137
|
+
[template],
|
|
138
|
+
category,
|
|
139
|
+
'2024-01',
|
|
140
|
+
0,
|
|
141
|
+
0,
|
|
142
|
+
);
|
|
143
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
144
|
+
expect(result).toBe(50000); // 5 Mondays * 100
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should handle daily limit', async () => {
|
|
148
|
+
const category: CategoryEntity = {
|
|
149
|
+
id: 'test',
|
|
150
|
+
name: 'Test Category',
|
|
151
|
+
group: 'test-group',
|
|
152
|
+
is_income: false,
|
|
153
|
+
};
|
|
154
|
+
const template: Template = {
|
|
155
|
+
type: 'simple',
|
|
156
|
+
limit: { amount: 10, hold: false, period: 'daily' },
|
|
157
|
+
directive: 'template',
|
|
158
|
+
priority: 1,
|
|
159
|
+
};
|
|
160
|
+
const instance = new TestCategoryTemplateContext(
|
|
161
|
+
[template],
|
|
162
|
+
category,
|
|
163
|
+
'2024-01',
|
|
164
|
+
0,
|
|
165
|
+
0,
|
|
166
|
+
);
|
|
167
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
168
|
+
expect(result).toBe(31000); // 31 days * 10
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('runRefill', () => {
|
|
173
|
+
it('should refill up to the monthly limit', async () => {
|
|
174
|
+
const category: CategoryEntity = {
|
|
175
|
+
id: 'test',
|
|
176
|
+
name: 'Test Category',
|
|
177
|
+
group: 'test-group',
|
|
178
|
+
is_income: false,
|
|
179
|
+
};
|
|
180
|
+
const limitTemplate: Template = {
|
|
181
|
+
type: 'limit',
|
|
182
|
+
amount: 150,
|
|
183
|
+
hold: false,
|
|
184
|
+
period: 'monthly',
|
|
185
|
+
directive: 'template',
|
|
186
|
+
priority: null,
|
|
187
|
+
};
|
|
188
|
+
const refillTemplate: Template = {
|
|
189
|
+
type: 'refill',
|
|
190
|
+
directive: 'template',
|
|
191
|
+
priority: 1,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const instance = new TestCategoryTemplateContext(
|
|
195
|
+
[limitTemplate, refillTemplate],
|
|
196
|
+
category,
|
|
197
|
+
'2024-01',
|
|
198
|
+
9000,
|
|
199
|
+
0,
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const result = await instance.runTemplatesForPriority(1, 10000, 10000);
|
|
203
|
+
expect(result).toBe(6000); // 150 - 90
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should handle weekly limit refill', async () => {
|
|
207
|
+
const category: CategoryEntity = {
|
|
208
|
+
id: 'test',
|
|
209
|
+
name: 'Test Category',
|
|
210
|
+
group: 'test-group',
|
|
211
|
+
is_income: false,
|
|
212
|
+
};
|
|
213
|
+
const limitTemplate: Template = {
|
|
214
|
+
type: 'limit',
|
|
215
|
+
amount: 100,
|
|
216
|
+
hold: false,
|
|
217
|
+
period: 'weekly',
|
|
218
|
+
start: '2024-01-01',
|
|
219
|
+
directive: 'template',
|
|
220
|
+
priority: null,
|
|
221
|
+
};
|
|
222
|
+
const refillTemplate: Template = {
|
|
223
|
+
type: 'refill',
|
|
224
|
+
directive: 'template',
|
|
225
|
+
priority: 1,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const instance = new TestCategoryTemplateContext(
|
|
229
|
+
[limitTemplate, refillTemplate],
|
|
230
|
+
category,
|
|
231
|
+
'2024-01',
|
|
232
|
+
0,
|
|
233
|
+
0,
|
|
234
|
+
);
|
|
235
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
236
|
+
expect(result).toBe(50000); // 5 Mondays * 100
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should handle daily limit refill', async () => {
|
|
240
|
+
const category: CategoryEntity = {
|
|
241
|
+
id: 'test',
|
|
242
|
+
name: 'Test Category',
|
|
243
|
+
group: 'test-group',
|
|
244
|
+
is_income: false,
|
|
245
|
+
};
|
|
246
|
+
const limitTemplate: Template = {
|
|
247
|
+
type: 'limit',
|
|
248
|
+
amount: 10,
|
|
249
|
+
hold: false,
|
|
250
|
+
period: 'daily',
|
|
251
|
+
directive: 'template',
|
|
252
|
+
priority: null,
|
|
253
|
+
};
|
|
254
|
+
const refillTemplate: Template = {
|
|
255
|
+
type: 'refill',
|
|
256
|
+
directive: 'template',
|
|
257
|
+
priority: 1,
|
|
258
|
+
};
|
|
259
|
+
const instance = new TestCategoryTemplateContext(
|
|
260
|
+
[limitTemplate, refillTemplate],
|
|
261
|
+
category,
|
|
262
|
+
'2024-01',
|
|
263
|
+
0,
|
|
264
|
+
0,
|
|
265
|
+
);
|
|
266
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
267
|
+
expect(result).toBe(31000); // 31 days * 10
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('runCopy', () => {
|
|
272
|
+
let instance: TestCategoryTemplateContext;
|
|
273
|
+
|
|
274
|
+
beforeEach(() => {
|
|
275
|
+
const category: CategoryEntity = {
|
|
276
|
+
id: 'test',
|
|
277
|
+
name: 'Test Category',
|
|
278
|
+
group: 'test-group',
|
|
279
|
+
is_income: false,
|
|
280
|
+
};
|
|
281
|
+
instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
|
|
282
|
+
vi.clearAllMocks();
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should copy budget from previous month', async () => {
|
|
286
|
+
const template: Template = {
|
|
287
|
+
type: 'copy',
|
|
288
|
+
lookBack: 1,
|
|
289
|
+
directive: 'template',
|
|
290
|
+
priority: 1,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
vi.mocked(actions.getSheetValue).mockResolvedValue(100);
|
|
294
|
+
|
|
295
|
+
const result = await CategoryTemplateContext.runCopy(template, instance);
|
|
296
|
+
expect(result).toBe(100);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('should copy budget from multiple months back', async () => {
|
|
300
|
+
const template: Template = {
|
|
301
|
+
type: 'copy',
|
|
302
|
+
lookBack: 3,
|
|
303
|
+
directive: 'template',
|
|
304
|
+
priority: 1,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
vi.mocked(actions.getSheetValue).mockResolvedValue(200);
|
|
308
|
+
|
|
309
|
+
const result = await CategoryTemplateContext.runCopy(template, instance);
|
|
310
|
+
expect(result).toBe(200);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('should handle zero budget amount', async () => {
|
|
314
|
+
const template: Template = {
|
|
315
|
+
type: 'copy',
|
|
316
|
+
lookBack: 1,
|
|
317
|
+
directive: 'template',
|
|
318
|
+
priority: 1,
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
vi.mocked(actions.getSheetValue).mockResolvedValue(0);
|
|
322
|
+
|
|
323
|
+
const result = await CategoryTemplateContext.runCopy(template, instance);
|
|
324
|
+
expect(result).toBe(0);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe('runPeriodic', () => {
|
|
329
|
+
let instance: TestCategoryTemplateContext;
|
|
330
|
+
|
|
331
|
+
beforeEach(() => {
|
|
332
|
+
const category: CategoryEntity = {
|
|
333
|
+
id: 'test',
|
|
334
|
+
name: 'Test Category',
|
|
335
|
+
group: 'test-group',
|
|
336
|
+
is_income: false,
|
|
337
|
+
};
|
|
338
|
+
instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
//5 mondays in January 2024
|
|
342
|
+
it('should calculate weekly amount for single week', () => {
|
|
343
|
+
const template: Template = {
|
|
344
|
+
type: 'periodic',
|
|
345
|
+
amount: 100,
|
|
346
|
+
period: {
|
|
347
|
+
period: 'week',
|
|
348
|
+
amount: 1,
|
|
349
|
+
},
|
|
350
|
+
starting: '2024-01-01',
|
|
351
|
+
directive: 'template',
|
|
352
|
+
priority: 1,
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const result = CategoryTemplateContext.runPeriodic(template, instance);
|
|
356
|
+
expect(result).toBe(amountToInteger(500));
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should calculate weekly amount for multiple weeks', () => {
|
|
360
|
+
const template: Template = {
|
|
361
|
+
type: 'periodic',
|
|
362
|
+
amount: 100,
|
|
363
|
+
period: {
|
|
364
|
+
period: 'week',
|
|
365
|
+
amount: 2,
|
|
366
|
+
},
|
|
367
|
+
starting: '2024-01-01',
|
|
368
|
+
directive: 'template',
|
|
369
|
+
priority: 1,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const result = CategoryTemplateContext.runPeriodic(template, instance);
|
|
373
|
+
expect(result).toBe(amountToInteger(300));
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should handle weeks spanning multiple months', () => {
|
|
377
|
+
const template: Template = {
|
|
378
|
+
type: 'periodic',
|
|
379
|
+
amount: 100,
|
|
380
|
+
period: {
|
|
381
|
+
period: 'week',
|
|
382
|
+
amount: 7,
|
|
383
|
+
},
|
|
384
|
+
starting: '2023-12-04',
|
|
385
|
+
directive: 'template',
|
|
386
|
+
priority: 1,
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const result = CategoryTemplateContext.runPeriodic(template, instance);
|
|
390
|
+
expect(result).toBe(amountToInteger(100));
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('should handle periodic days', () => {
|
|
394
|
+
const template: Template = {
|
|
395
|
+
type: 'periodic',
|
|
396
|
+
amount: 100,
|
|
397
|
+
period: {
|
|
398
|
+
period: 'day',
|
|
399
|
+
amount: 10,
|
|
400
|
+
},
|
|
401
|
+
starting: '2024-01-01',
|
|
402
|
+
directive: 'template',
|
|
403
|
+
priority: 1,
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
const result = CategoryTemplateContext.runPeriodic(template, instance);
|
|
407
|
+
expect(result).toBe(amountToInteger(400)); // for the 1st, 11th, 21st, 31st
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should handle periodic years', () => {
|
|
411
|
+
const template: Template = {
|
|
412
|
+
type: 'periodic',
|
|
413
|
+
amount: 100,
|
|
414
|
+
period: {
|
|
415
|
+
period: 'year',
|
|
416
|
+
amount: 1,
|
|
417
|
+
},
|
|
418
|
+
starting: '2023-01-01',
|
|
419
|
+
directive: 'template',
|
|
420
|
+
priority: 1,
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const result = CategoryTemplateContext.runPeriodic(template, instance);
|
|
424
|
+
expect(result).toBe(amountToInteger(100));
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
it('should handle periodic months', () => {
|
|
428
|
+
const template: Template = {
|
|
429
|
+
type: 'periodic',
|
|
430
|
+
amount: 100,
|
|
431
|
+
period: {
|
|
432
|
+
period: 'month',
|
|
433
|
+
amount: 2,
|
|
434
|
+
},
|
|
435
|
+
starting: '2023-11-01',
|
|
436
|
+
directive: 'template',
|
|
437
|
+
priority: 1,
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
const result = CategoryTemplateContext.runPeriodic(template, instance);
|
|
441
|
+
expect(result).toBe(amountToInteger(100));
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
describe('runSpend', () => {
|
|
446
|
+
let instance: TestCategoryTemplateContext;
|
|
447
|
+
|
|
448
|
+
beforeEach(() => {
|
|
449
|
+
const category: CategoryEntity = {
|
|
450
|
+
id: 'test',
|
|
451
|
+
name: 'Test Category',
|
|
452
|
+
group: 'test-group',
|
|
453
|
+
is_income: false,
|
|
454
|
+
};
|
|
455
|
+
instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
|
|
456
|
+
vi.clearAllMocks();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should calculate monthly amount needed to reach target', async () => {
|
|
460
|
+
const template: Template = {
|
|
461
|
+
type: 'spend',
|
|
462
|
+
amount: 1000,
|
|
463
|
+
from: '2023-11',
|
|
464
|
+
month: '2024-01',
|
|
465
|
+
directive: 'template',
|
|
466
|
+
priority: 1,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
vi.mocked(actions.getSheetValue)
|
|
470
|
+
.mockResolvedValueOnce(-10000) // spent in Nov
|
|
471
|
+
.mockResolvedValueOnce(20000) // leftover in Nov
|
|
472
|
+
.mockResolvedValueOnce(10000); // budgeted in Dec
|
|
473
|
+
|
|
474
|
+
const result = await CategoryTemplateContext.runSpend(template, instance);
|
|
475
|
+
expect(result).toBe(60000);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should handle repeating spend template', async () => {
|
|
479
|
+
const template: Template = {
|
|
480
|
+
type: 'spend',
|
|
481
|
+
amount: 1000,
|
|
482
|
+
from: '2023-11',
|
|
483
|
+
month: '2023-12',
|
|
484
|
+
repeat: 3,
|
|
485
|
+
directive: 'template',
|
|
486
|
+
priority: 1,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
vi.mocked(actions.getSheetValue)
|
|
490
|
+
.mockResolvedValueOnce(-10000)
|
|
491
|
+
.mockResolvedValueOnce(20000)
|
|
492
|
+
.mockResolvedValueOnce(10000);
|
|
493
|
+
|
|
494
|
+
const result = await CategoryTemplateContext.runSpend(template, instance);
|
|
495
|
+
expect(result).toBe(33333);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('should return zero for past target date', async () => {
|
|
499
|
+
const template: Template = {
|
|
500
|
+
type: 'spend',
|
|
501
|
+
amount: 1000,
|
|
502
|
+
from: '2023-12',
|
|
503
|
+
month: '2023-12',
|
|
504
|
+
directive: 'template',
|
|
505
|
+
priority: 1,
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const result = await CategoryTemplateContext.runSpend(template, instance);
|
|
509
|
+
expect(result).toBe(0);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
describe('runPercentage', () => {
|
|
514
|
+
let instance: TestCategoryTemplateContext;
|
|
515
|
+
|
|
516
|
+
beforeEach(() => {
|
|
517
|
+
const category: CategoryEntity = {
|
|
518
|
+
id: 'test',
|
|
519
|
+
name: 'Test Category',
|
|
520
|
+
group: 'test-group',
|
|
521
|
+
is_income: false,
|
|
522
|
+
};
|
|
523
|
+
instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
|
|
524
|
+
vi.clearAllMocks();
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
it('should calculate percentage of all income', async () => {
|
|
528
|
+
const template: Template = {
|
|
529
|
+
type: 'percentage',
|
|
530
|
+
percent: 10,
|
|
531
|
+
category: 'all income',
|
|
532
|
+
previous: false,
|
|
533
|
+
directive: 'template',
|
|
534
|
+
priority: 1,
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
vi.mocked(actions.getSheetValue).mockResolvedValue(10000);
|
|
538
|
+
|
|
539
|
+
const result = await CategoryTemplateContext.runPercentage(
|
|
540
|
+
template,
|
|
541
|
+
0,
|
|
542
|
+
instance,
|
|
543
|
+
);
|
|
544
|
+
expect(result).toBe(1000); // 10% of 10000
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('should calculate percentage of available funds', async () => {
|
|
548
|
+
const template: Template = {
|
|
549
|
+
type: 'percentage',
|
|
550
|
+
percent: 20,
|
|
551
|
+
category: 'available funds',
|
|
552
|
+
previous: false,
|
|
553
|
+
directive: 'template',
|
|
554
|
+
priority: 1,
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const result = await CategoryTemplateContext.runPercentage(
|
|
558
|
+
template,
|
|
559
|
+
500,
|
|
560
|
+
instance,
|
|
561
|
+
);
|
|
562
|
+
expect(result).toBe(100); // 20% of 500
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
it('should calculate percentage of specific income category', async () => {
|
|
566
|
+
const template: Template = {
|
|
567
|
+
type: 'percentage',
|
|
568
|
+
percent: 15,
|
|
569
|
+
category: 'Salary',
|
|
570
|
+
previous: false,
|
|
571
|
+
directive: 'template',
|
|
572
|
+
priority: 1,
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
vi.mocked(db.getCategories).mockResolvedValue([
|
|
576
|
+
{
|
|
577
|
+
id: 'income1',
|
|
578
|
+
name: 'Salary',
|
|
579
|
+
is_income: 1,
|
|
580
|
+
cat_group: 'income',
|
|
581
|
+
sort_order: 1,
|
|
582
|
+
hidden: 0,
|
|
583
|
+
tombstone: 0,
|
|
584
|
+
},
|
|
585
|
+
]);
|
|
586
|
+
vi.mocked(actions.getSheetValue).mockResolvedValue(2000);
|
|
587
|
+
|
|
588
|
+
const result = await CategoryTemplateContext.runPercentage(
|
|
589
|
+
template,
|
|
590
|
+
0,
|
|
591
|
+
instance,
|
|
592
|
+
);
|
|
593
|
+
expect(result).toBe(300); // 15% of 2000
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('should calculate percentage of previous month income', async () => {
|
|
597
|
+
const template: Template = {
|
|
598
|
+
type: 'percentage',
|
|
599
|
+
percent: 10,
|
|
600
|
+
category: 'all income',
|
|
601
|
+
previous: true,
|
|
602
|
+
directive: 'template',
|
|
603
|
+
priority: 1,
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
vi.mocked(actions.getSheetValue).mockResolvedValue(10000);
|
|
607
|
+
|
|
608
|
+
const result = await CategoryTemplateContext.runPercentage(
|
|
609
|
+
template,
|
|
610
|
+
0,
|
|
611
|
+
instance,
|
|
612
|
+
);
|
|
613
|
+
expect(result).toBe(1000); // 10% of 10000
|
|
614
|
+
expect(actions.getSheetValue).toHaveBeenCalledWith(
|
|
615
|
+
'budget202312',
|
|
616
|
+
'total-income',
|
|
617
|
+
);
|
|
618
|
+
});
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
describe('runAverage', () => {
|
|
622
|
+
let instance: TestCategoryTemplateContext;
|
|
623
|
+
|
|
624
|
+
beforeEach(() => {
|
|
625
|
+
const category: CategoryEntity = {
|
|
626
|
+
id: 'test',
|
|
627
|
+
name: 'Test Category',
|
|
628
|
+
group: 'test-group',
|
|
629
|
+
is_income: false,
|
|
630
|
+
};
|
|
631
|
+
instance = new TestCategoryTemplateContext([], category, '2024-01', 0, 0);
|
|
632
|
+
vi.clearAllMocks();
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('should calculate average of 3 months', async () => {
|
|
636
|
+
const template: Template = {
|
|
637
|
+
type: 'average',
|
|
638
|
+
numMonths: 3,
|
|
639
|
+
directive: 'template',
|
|
640
|
+
priority: 1,
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
vi.mocked(actions.getSheetValue)
|
|
644
|
+
.mockResolvedValueOnce(-100) // Dec 2023
|
|
645
|
+
.mockResolvedValueOnce(-200) // Nov 2023
|
|
646
|
+
.mockResolvedValueOnce(-300); // Oct 2023
|
|
647
|
+
|
|
648
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
649
|
+
template,
|
|
650
|
+
instance,
|
|
651
|
+
);
|
|
652
|
+
expect(result).toBe(200); // Average of -100, -200, -300
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should handle zero amounts', async () => {
|
|
656
|
+
const template: Template = {
|
|
657
|
+
type: 'average',
|
|
658
|
+
numMonths: 3,
|
|
659
|
+
directive: 'template',
|
|
660
|
+
priority: 1,
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
vi.mocked(actions.getSheetValue)
|
|
664
|
+
.mockResolvedValueOnce(0)
|
|
665
|
+
.mockResolvedValueOnce(0)
|
|
666
|
+
.mockResolvedValueOnce(-300);
|
|
667
|
+
|
|
668
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
669
|
+
template,
|
|
670
|
+
instance,
|
|
671
|
+
);
|
|
672
|
+
expect(result).toBe(100);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
it('should handle mixed positive and negative amounts', async () => {
|
|
676
|
+
const template: Template = {
|
|
677
|
+
type: 'average',
|
|
678
|
+
numMonths: 3,
|
|
679
|
+
directive: 'template',
|
|
680
|
+
priority: 1,
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
vi.mocked(actions.getSheetValue)
|
|
684
|
+
.mockResolvedValueOnce(-100)
|
|
685
|
+
.mockResolvedValueOnce(200)
|
|
686
|
+
.mockResolvedValueOnce(-300);
|
|
687
|
+
|
|
688
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
689
|
+
template,
|
|
690
|
+
instance,
|
|
691
|
+
);
|
|
692
|
+
expect(result).toBe(67); // Average of -100, 200, -300
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it('should handle positive percent adjustments', async () => {
|
|
696
|
+
const template: Template = {
|
|
697
|
+
type: 'average',
|
|
698
|
+
numMonths: 3,
|
|
699
|
+
directive: 'template',
|
|
700
|
+
priority: 1,
|
|
701
|
+
adjustment: 10,
|
|
702
|
+
adjustmentType: 'percent',
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
vi.mocked(actions.getSheetValue)
|
|
706
|
+
.mockResolvedValueOnce(-100)
|
|
707
|
+
.mockResolvedValueOnce(-100)
|
|
708
|
+
.mockResolvedValueOnce(-100);
|
|
709
|
+
|
|
710
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
711
|
+
template,
|
|
712
|
+
instance,
|
|
713
|
+
);
|
|
714
|
+
expect(result).toBe(110);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
it('should handle negative percent adjustments', async () => {
|
|
718
|
+
const template: Template = {
|
|
719
|
+
type: 'average',
|
|
720
|
+
numMonths: 3,
|
|
721
|
+
directive: 'template',
|
|
722
|
+
priority: 1,
|
|
723
|
+
adjustment: -10,
|
|
724
|
+
adjustmentType: 'percent',
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
vi.mocked(actions.getSheetValue)
|
|
728
|
+
.mockResolvedValueOnce(-100)
|
|
729
|
+
.mockResolvedValueOnce(-100)
|
|
730
|
+
.mockResolvedValueOnce(-100);
|
|
731
|
+
|
|
732
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
733
|
+
template,
|
|
734
|
+
instance,
|
|
735
|
+
);
|
|
736
|
+
expect(result).toBe(90);
|
|
737
|
+
});
|
|
738
|
+
it('should handle zero percent adjustments', async () => {
|
|
739
|
+
const template: Template = {
|
|
740
|
+
type: 'average',
|
|
741
|
+
numMonths: 3,
|
|
742
|
+
directive: 'template',
|
|
743
|
+
priority: 1,
|
|
744
|
+
adjustment: 0,
|
|
745
|
+
adjustmentType: 'percent',
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
vi.mocked(actions.getSheetValue)
|
|
749
|
+
.mockResolvedValueOnce(-100)
|
|
750
|
+
.mockResolvedValueOnce(-100)
|
|
751
|
+
.mockResolvedValueOnce(-100);
|
|
752
|
+
|
|
753
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
754
|
+
template,
|
|
755
|
+
instance,
|
|
756
|
+
);
|
|
757
|
+
expect(result).toBe(100);
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
it('should handle zero amount adjustments', async () => {
|
|
761
|
+
const template: Template = {
|
|
762
|
+
type: 'average',
|
|
763
|
+
numMonths: 3,
|
|
764
|
+
directive: 'template',
|
|
765
|
+
priority: 1,
|
|
766
|
+
adjustment: 0,
|
|
767
|
+
adjustmentType: 'fixed',
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
vi.mocked(actions.getSheetValue)
|
|
771
|
+
.mockResolvedValueOnce(-100)
|
|
772
|
+
.mockResolvedValueOnce(-100)
|
|
773
|
+
.mockResolvedValueOnce(-100);
|
|
774
|
+
|
|
775
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
776
|
+
template,
|
|
777
|
+
instance,
|
|
778
|
+
);
|
|
779
|
+
expect(result).toBe(100);
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
it('should handle positive amount adjustments', async () => {
|
|
783
|
+
const template: Template = {
|
|
784
|
+
type: 'average',
|
|
785
|
+
numMonths: 3,
|
|
786
|
+
directive: 'template',
|
|
787
|
+
priority: 1,
|
|
788
|
+
adjustment: 11,
|
|
789
|
+
adjustmentType: 'fixed',
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
vi.mocked(actions.getSheetValue)
|
|
793
|
+
.mockResolvedValueOnce(-10000)
|
|
794
|
+
.mockResolvedValueOnce(-10000)
|
|
795
|
+
.mockResolvedValueOnce(-10000);
|
|
796
|
+
|
|
797
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
798
|
+
template,
|
|
799
|
+
instance,
|
|
800
|
+
);
|
|
801
|
+
expect(result).toBe(11100);
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
it('should handle negative amount adjustments', async () => {
|
|
805
|
+
const template: Template = {
|
|
806
|
+
type: 'average',
|
|
807
|
+
numMonths: 3,
|
|
808
|
+
directive: 'template',
|
|
809
|
+
priority: 1,
|
|
810
|
+
adjustment: -1,
|
|
811
|
+
adjustmentType: 'fixed',
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
vi.mocked(actions.getSheetValue)
|
|
815
|
+
.mockResolvedValueOnce(-10000)
|
|
816
|
+
.mockResolvedValueOnce(-10000)
|
|
817
|
+
.mockResolvedValueOnce(-10000);
|
|
818
|
+
|
|
819
|
+
const result = await CategoryTemplateContext.runAverage(
|
|
820
|
+
template,
|
|
821
|
+
instance,
|
|
822
|
+
);
|
|
823
|
+
expect(result).toBe(9900);
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
describe('runBy', () => {
|
|
828
|
+
let instance: TestCategoryTemplateContext;
|
|
829
|
+
|
|
830
|
+
beforeEach(() => {
|
|
831
|
+
const category: CategoryEntity = {
|
|
832
|
+
id: 'test',
|
|
833
|
+
name: 'Test Category',
|
|
834
|
+
group: 'test-group',
|
|
835
|
+
is_income: false,
|
|
836
|
+
};
|
|
837
|
+
instance = new TestCategoryTemplateContext(
|
|
838
|
+
[
|
|
839
|
+
{
|
|
840
|
+
type: 'by',
|
|
841
|
+
amount: 1000,
|
|
842
|
+
month: '2024-03',
|
|
843
|
+
directive: 'template',
|
|
844
|
+
priority: 1,
|
|
845
|
+
},
|
|
846
|
+
{
|
|
847
|
+
type: 'by',
|
|
848
|
+
amount: 2000,
|
|
849
|
+
month: '2024-06',
|
|
850
|
+
directive: 'template',
|
|
851
|
+
priority: 1,
|
|
852
|
+
},
|
|
853
|
+
],
|
|
854
|
+
category,
|
|
855
|
+
'2024-01',
|
|
856
|
+
0,
|
|
857
|
+
0,
|
|
858
|
+
);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('should calculate monthly amount needed for multiple targets', () => {
|
|
862
|
+
const result = CategoryTemplateContext.runBy(instance);
|
|
863
|
+
expect(result).toBe(66667);
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('should handle repeating targets', () => {
|
|
867
|
+
instance = new TestCategoryTemplateContext(
|
|
868
|
+
[
|
|
869
|
+
{
|
|
870
|
+
type: 'by',
|
|
871
|
+
amount: 1000,
|
|
872
|
+
month: '2023-03',
|
|
873
|
+
repeat: 12,
|
|
874
|
+
directive: 'template',
|
|
875
|
+
priority: 1,
|
|
876
|
+
},
|
|
877
|
+
{
|
|
878
|
+
type: 'by',
|
|
879
|
+
amount: 2000,
|
|
880
|
+
month: '2023-06',
|
|
881
|
+
repeat: 12,
|
|
882
|
+
directive: 'template',
|
|
883
|
+
priority: 1,
|
|
884
|
+
},
|
|
885
|
+
],
|
|
886
|
+
instance.category,
|
|
887
|
+
'2024-01',
|
|
888
|
+
0,
|
|
889
|
+
0,
|
|
890
|
+
);
|
|
891
|
+
|
|
892
|
+
const result = CategoryTemplateContext.runBy(instance);
|
|
893
|
+
expect(result).toBe(83333);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
it('should handle existing balance', () => {
|
|
897
|
+
instance = new TestCategoryTemplateContext(
|
|
898
|
+
[
|
|
899
|
+
{
|
|
900
|
+
type: 'by',
|
|
901
|
+
amount: 1000,
|
|
902
|
+
month: '2024-03',
|
|
903
|
+
directive: 'template',
|
|
904
|
+
priority: 1,
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
type: 'by',
|
|
908
|
+
amount: 2000,
|
|
909
|
+
month: '2024-06',
|
|
910
|
+
directive: 'template',
|
|
911
|
+
priority: 1,
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
instance.category,
|
|
915
|
+
'2024-01',
|
|
916
|
+
500,
|
|
917
|
+
0,
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
const result = CategoryTemplateContext.runBy(instance);
|
|
921
|
+
expect(result).toBe(66500); // (1000 + 2000 - 5) / 3
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
describe('template priorities', () => {
|
|
926
|
+
it('should handle multiple templates with priorities and insufficient funds', async () => {
|
|
927
|
+
const category: CategoryEntity = {
|
|
928
|
+
id: 'test',
|
|
929
|
+
name: 'Test Category',
|
|
930
|
+
group: 'test-group',
|
|
931
|
+
is_income: false,
|
|
932
|
+
};
|
|
933
|
+
const templates: Template[] = [
|
|
934
|
+
{
|
|
935
|
+
type: 'simple',
|
|
936
|
+
monthly: 100,
|
|
937
|
+
directive: 'template',
|
|
938
|
+
priority: 1,
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
type: 'simple',
|
|
942
|
+
monthly: 200,
|
|
943
|
+
directive: 'template',
|
|
944
|
+
priority: 1,
|
|
945
|
+
},
|
|
946
|
+
];
|
|
947
|
+
const instance = new TestCategoryTemplateContext(
|
|
948
|
+
templates,
|
|
949
|
+
category,
|
|
950
|
+
'2024-01',
|
|
951
|
+
0,
|
|
952
|
+
0,
|
|
953
|
+
);
|
|
954
|
+
const result = await instance.runTemplatesForPriority(1, 150, 150);
|
|
955
|
+
expect(result).toBe(150); // Max out at available funds
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
959
|
+
describe('category limits', () => {
|
|
960
|
+
it('should not budget over monthly limit', async () => {
|
|
961
|
+
const category: CategoryEntity = {
|
|
962
|
+
id: 'test',
|
|
963
|
+
name: 'Test Category',
|
|
964
|
+
group: 'test-group',
|
|
965
|
+
is_income: false,
|
|
966
|
+
};
|
|
967
|
+
const templates: Template[] = [
|
|
968
|
+
{
|
|
969
|
+
type: 'simple',
|
|
970
|
+
monthly: 100,
|
|
971
|
+
limit: { amount: 150, hold: false, period: 'monthly' },
|
|
972
|
+
directive: 'template',
|
|
973
|
+
priority: 1,
|
|
974
|
+
},
|
|
975
|
+
];
|
|
976
|
+
const instance = new TestCategoryTemplateContext(
|
|
977
|
+
templates,
|
|
978
|
+
category,
|
|
979
|
+
'2024-01',
|
|
980
|
+
9000,
|
|
981
|
+
0,
|
|
982
|
+
);
|
|
983
|
+
const result = await instance.runTemplatesForPriority(1, 10000, 10000);
|
|
984
|
+
expect(result).toBe(6000); //150 - 90
|
|
985
|
+
});
|
|
986
|
+
|
|
987
|
+
it('should handle hold flag when limit is reached', async () => {
|
|
988
|
+
const category: CategoryEntity = {
|
|
989
|
+
id: 'test',
|
|
990
|
+
name: 'Test Category',
|
|
991
|
+
group: 'test-group',
|
|
992
|
+
is_income: false,
|
|
993
|
+
};
|
|
994
|
+
const templates: Template[] = [
|
|
995
|
+
{
|
|
996
|
+
type: 'simple',
|
|
997
|
+
monthly: 100,
|
|
998
|
+
limit: { amount: 200, hold: true, period: 'monthly' },
|
|
999
|
+
directive: 'template',
|
|
1000
|
+
priority: 1,
|
|
1001
|
+
},
|
|
1002
|
+
];
|
|
1003
|
+
const instance = new TestCategoryTemplateContext(
|
|
1004
|
+
templates,
|
|
1005
|
+
category,
|
|
1006
|
+
'2024-01',
|
|
1007
|
+
300,
|
|
1008
|
+
0,
|
|
1009
|
+
);
|
|
1010
|
+
const result = instance.getLimitExcess();
|
|
1011
|
+
expect(result).toBe(0);
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
it('should remove funds if over limit', async () => {
|
|
1015
|
+
const category: CategoryEntity = {
|
|
1016
|
+
id: 'test',
|
|
1017
|
+
name: 'Test Category',
|
|
1018
|
+
group: 'test-group',
|
|
1019
|
+
is_income: false,
|
|
1020
|
+
};
|
|
1021
|
+
const templates: Template[] = [
|
|
1022
|
+
{
|
|
1023
|
+
type: 'simple',
|
|
1024
|
+
monthly: 100,
|
|
1025
|
+
limit: { amount: 200, hold: false, period: 'monthly' },
|
|
1026
|
+
directive: 'template',
|
|
1027
|
+
priority: 1,
|
|
1028
|
+
},
|
|
1029
|
+
];
|
|
1030
|
+
const instance = new TestCategoryTemplateContext(
|
|
1031
|
+
templates,
|
|
1032
|
+
category,
|
|
1033
|
+
'2024-01',
|
|
1034
|
+
30000,
|
|
1035
|
+
0,
|
|
1036
|
+
);
|
|
1037
|
+
const result = instance.getLimitExcess();
|
|
1038
|
+
expect(result).toBe(10000);
|
|
1039
|
+
});
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
describe('remainder templates', () => {
|
|
1043
|
+
it('should distribute available funds based on weight', async () => {
|
|
1044
|
+
const category: CategoryEntity = {
|
|
1045
|
+
id: 'test',
|
|
1046
|
+
name: 'Test Category',
|
|
1047
|
+
group: 'test-group',
|
|
1048
|
+
is_income: false,
|
|
1049
|
+
};
|
|
1050
|
+
const templates: Template[] = [
|
|
1051
|
+
{
|
|
1052
|
+
type: 'remainder',
|
|
1053
|
+
weight: 2,
|
|
1054
|
+
directive: 'template',
|
|
1055
|
+
priority: null,
|
|
1056
|
+
},
|
|
1057
|
+
];
|
|
1058
|
+
const instance = new TestCategoryTemplateContext(
|
|
1059
|
+
templates,
|
|
1060
|
+
category,
|
|
1061
|
+
'2024-01',
|
|
1062
|
+
0,
|
|
1063
|
+
0,
|
|
1064
|
+
);
|
|
1065
|
+
const result = instance.runRemainder(100, 50);
|
|
1066
|
+
expect(result).toBe(100); // 2 * 50 = 100
|
|
1067
|
+
});
|
|
1068
|
+
|
|
1069
|
+
it('remainder should handle last cent', async () => {
|
|
1070
|
+
const category: CategoryEntity = {
|
|
1071
|
+
id: 'test',
|
|
1072
|
+
name: 'Test Category',
|
|
1073
|
+
group: 'test-group',
|
|
1074
|
+
is_income: false,
|
|
1075
|
+
};
|
|
1076
|
+
const templates: Template[] = [
|
|
1077
|
+
{
|
|
1078
|
+
type: 'remainder',
|
|
1079
|
+
weight: 1,
|
|
1080
|
+
directive: 'template',
|
|
1081
|
+
priority: null,
|
|
1082
|
+
},
|
|
1083
|
+
];
|
|
1084
|
+
const instance = new TestCategoryTemplateContext(
|
|
1085
|
+
templates,
|
|
1086
|
+
category,
|
|
1087
|
+
'2024-01',
|
|
1088
|
+
0,
|
|
1089
|
+
0,
|
|
1090
|
+
);
|
|
1091
|
+
const result = instance.runRemainder(101, 100);
|
|
1092
|
+
expect(result).toBe(101);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
it('remainder wont over budget', async () => {
|
|
1096
|
+
const category: CategoryEntity = {
|
|
1097
|
+
id: 'test',
|
|
1098
|
+
name: 'Test Category',
|
|
1099
|
+
group: 'test-group',
|
|
1100
|
+
is_income: false,
|
|
1101
|
+
};
|
|
1102
|
+
const templates: Template[] = [
|
|
1103
|
+
{
|
|
1104
|
+
type: 'remainder',
|
|
1105
|
+
weight: 1,
|
|
1106
|
+
directive: 'template',
|
|
1107
|
+
priority: null,
|
|
1108
|
+
},
|
|
1109
|
+
];
|
|
1110
|
+
const instance = new TestCategoryTemplateContext(
|
|
1111
|
+
templates,
|
|
1112
|
+
category,
|
|
1113
|
+
'2024-01',
|
|
1114
|
+
0,
|
|
1115
|
+
0,
|
|
1116
|
+
);
|
|
1117
|
+
const result = instance.runRemainder(99, 100);
|
|
1118
|
+
expect(result).toBe(99);
|
|
1119
|
+
});
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
describe('full process', () => {
|
|
1123
|
+
it('should handle priority limits through the entire process', async () => {
|
|
1124
|
+
const category: CategoryEntity = {
|
|
1125
|
+
id: 'test',
|
|
1126
|
+
name: 'Test Category',
|
|
1127
|
+
group: 'test-group',
|
|
1128
|
+
is_income: false,
|
|
1129
|
+
};
|
|
1130
|
+
const templates: Template[] = [
|
|
1131
|
+
{
|
|
1132
|
+
type: 'simple',
|
|
1133
|
+
monthly: 100,
|
|
1134
|
+
directive: 'template',
|
|
1135
|
+
priority: 1,
|
|
1136
|
+
},
|
|
1137
|
+
{
|
|
1138
|
+
type: 'simple',
|
|
1139
|
+
monthly: 200,
|
|
1140
|
+
directive: 'template',
|
|
1141
|
+
priority: 2,
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
type: 'remainder',
|
|
1145
|
+
weight: 1,
|
|
1146
|
+
directive: 'template',
|
|
1147
|
+
priority: null,
|
|
1148
|
+
},
|
|
1149
|
+
];
|
|
1150
|
+
|
|
1151
|
+
// Mock the sheet values needed for init
|
|
1152
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
|
|
1153
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
|
|
1154
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1155
|
+
mockPreferences(false, 'USD');
|
|
1156
|
+
|
|
1157
|
+
// Initialize the template
|
|
1158
|
+
const instance = await CategoryTemplateContext.init(
|
|
1159
|
+
templates,
|
|
1160
|
+
category,
|
|
1161
|
+
'2024-01',
|
|
1162
|
+
0,
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
// Run each priority level separately
|
|
1166
|
+
const priority1Result = await instance.runTemplatesForPriority(
|
|
1167
|
+
1,
|
|
1168
|
+
15000,
|
|
1169
|
+
15000,
|
|
1170
|
+
);
|
|
1171
|
+
const priority2Result = await instance.runTemplatesForPriority(
|
|
1172
|
+
2,
|
|
1173
|
+
15000 - priority1Result,
|
|
1174
|
+
15000,
|
|
1175
|
+
);
|
|
1176
|
+
|
|
1177
|
+
// Get the final values
|
|
1178
|
+
const values = instance.getValues();
|
|
1179
|
+
|
|
1180
|
+
// Verify the results
|
|
1181
|
+
expect(priority1Result).toBe(10000); // Should get full amount for priority 1
|
|
1182
|
+
expect(priority2Result).toBe(5000); // Should get remaining funds for priority 2
|
|
1183
|
+
expect(values.budgeted).toBe(15000); // Should match the total of both priorities
|
|
1184
|
+
expect(values.goal).toBe(30000); // Should be the sum of all template amounts
|
|
1185
|
+
expect(values.longGoal).toBe(null); // No goal template
|
|
1186
|
+
});
|
|
1187
|
+
|
|
1188
|
+
it('should handle category limits through the entire process', async () => {
|
|
1189
|
+
const category: CategoryEntity = {
|
|
1190
|
+
id: 'test',
|
|
1191
|
+
name: 'Test Category',
|
|
1192
|
+
group: 'test-group',
|
|
1193
|
+
is_income: false,
|
|
1194
|
+
};
|
|
1195
|
+
const templates: Template[] = [
|
|
1196
|
+
{
|
|
1197
|
+
type: 'simple',
|
|
1198
|
+
monthly: 100,
|
|
1199
|
+
directive: 'template',
|
|
1200
|
+
priority: 1,
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
type: 'simple',
|
|
1204
|
+
monthly: 200,
|
|
1205
|
+
directive: 'template',
|
|
1206
|
+
priority: 1,
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
type: 'simple',
|
|
1210
|
+
limit: { amount: 150, hold: false, period: 'monthly' },
|
|
1211
|
+
directive: 'template',
|
|
1212
|
+
priority: 1,
|
|
1213
|
+
},
|
|
1214
|
+
];
|
|
1215
|
+
|
|
1216
|
+
// Mock the sheet values needed for init
|
|
1217
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
|
|
1218
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
|
|
1219
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1220
|
+
mockPreferences(false, 'USD');
|
|
1221
|
+
|
|
1222
|
+
// Initialize the template
|
|
1223
|
+
const instance = await CategoryTemplateContext.init(
|
|
1224
|
+
templates,
|
|
1225
|
+
category,
|
|
1226
|
+
'2024-01',
|
|
1227
|
+
0,
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
// Run the templates with more than enough funds
|
|
1231
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1232
|
+
|
|
1233
|
+
// Get the final values
|
|
1234
|
+
const values = instance.getValues();
|
|
1235
|
+
|
|
1236
|
+
// Verify the results
|
|
1237
|
+
expect(result).toBe(15000); // Should be limited by the category limit
|
|
1238
|
+
expect(values.budgeted).toBe(15000); // Should match the limit
|
|
1239
|
+
expect(values.goal).toBe(15000); // Should be the limit amount
|
|
1240
|
+
expect(values.longGoal).toBe(null); // No goal template
|
|
1241
|
+
});
|
|
1242
|
+
|
|
1243
|
+
it('should handle remainder template at the end of the process', async () => {
|
|
1244
|
+
const category: CategoryEntity = {
|
|
1245
|
+
id: 'test',
|
|
1246
|
+
name: 'Test Category',
|
|
1247
|
+
group: 'test-group',
|
|
1248
|
+
is_income: false,
|
|
1249
|
+
};
|
|
1250
|
+
const templates: Template[] = [
|
|
1251
|
+
{
|
|
1252
|
+
type: 'simple',
|
|
1253
|
+
monthly: 100,
|
|
1254
|
+
directive: 'template',
|
|
1255
|
+
priority: 1,
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
type: 'simple',
|
|
1259
|
+
monthly: 200,
|
|
1260
|
+
directive: 'template',
|
|
1261
|
+
priority: 1,
|
|
1262
|
+
},
|
|
1263
|
+
{
|
|
1264
|
+
type: 'remainder',
|
|
1265
|
+
weight: 1,
|
|
1266
|
+
directive: 'template',
|
|
1267
|
+
priority: null,
|
|
1268
|
+
},
|
|
1269
|
+
];
|
|
1270
|
+
|
|
1271
|
+
// Mock the sheet values needed for init
|
|
1272
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
|
|
1273
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
|
|
1274
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1275
|
+
mockPreferences(false, 'USD');
|
|
1276
|
+
|
|
1277
|
+
// Initialize the template
|
|
1278
|
+
const instance = await CategoryTemplateContext.init(
|
|
1279
|
+
templates,
|
|
1280
|
+
category,
|
|
1281
|
+
'2024-01',
|
|
1282
|
+
0,
|
|
1283
|
+
);
|
|
1284
|
+
const weight = instance.getRemainderWeight();
|
|
1285
|
+
|
|
1286
|
+
// Run the templates with more than enough funds
|
|
1287
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1288
|
+
|
|
1289
|
+
// Run the remainder template
|
|
1290
|
+
const perWeight = (100000 - result) / weight;
|
|
1291
|
+
const remainderResult = instance.runRemainder(perWeight, perWeight);
|
|
1292
|
+
|
|
1293
|
+
// Get the final values
|
|
1294
|
+
const values = instance.getValues();
|
|
1295
|
+
|
|
1296
|
+
// Verify the results
|
|
1297
|
+
expect(result).toBe(30000); // Should get full amount for both simple templates
|
|
1298
|
+
expect(remainderResult).toBe(70000); // Should get remaining funds
|
|
1299
|
+
expect(values.budgeted).toBe(100000); // Should match the total of all templates
|
|
1300
|
+
expect(values.goal).toBe(30000); // Should be the sum of the simple templates
|
|
1301
|
+
expect(values.longGoal).toBe(null); // No goal template
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
it('should handle goal template through the entire process', async () => {
|
|
1305
|
+
const category: CategoryEntity = {
|
|
1306
|
+
id: 'test',
|
|
1307
|
+
name: 'Test Category',
|
|
1308
|
+
group: 'test-group',
|
|
1309
|
+
is_income: false,
|
|
1310
|
+
};
|
|
1311
|
+
const templates: Template[] = [
|
|
1312
|
+
{
|
|
1313
|
+
type: 'simple',
|
|
1314
|
+
monthly: 100,
|
|
1315
|
+
directive: 'template',
|
|
1316
|
+
priority: 1,
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
type: 'simple',
|
|
1320
|
+
monthly: 200,
|
|
1321
|
+
directive: 'template',
|
|
1322
|
+
priority: 1,
|
|
1323
|
+
},
|
|
1324
|
+
{
|
|
1325
|
+
type: 'goal',
|
|
1326
|
+
amount: 1000,
|
|
1327
|
+
directive: 'goal',
|
|
1328
|
+
},
|
|
1329
|
+
];
|
|
1330
|
+
|
|
1331
|
+
// Mock the sheet values needed for init
|
|
1332
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
|
|
1333
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
|
|
1334
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1335
|
+
mockPreferences(false, 'USD');
|
|
1336
|
+
|
|
1337
|
+
// Initialize the template
|
|
1338
|
+
const instance = await CategoryTemplateContext.init(
|
|
1339
|
+
templates,
|
|
1340
|
+
category,
|
|
1341
|
+
'2024-01',
|
|
1342
|
+
0,
|
|
1343
|
+
);
|
|
1344
|
+
|
|
1345
|
+
// Run the templates with more than enough funds
|
|
1346
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1347
|
+
|
|
1348
|
+
// Get the final values
|
|
1349
|
+
const values = instance.getValues();
|
|
1350
|
+
|
|
1351
|
+
// Verify the results
|
|
1352
|
+
expect(result).toBe(30000); // Should get full amount for both simple templates
|
|
1353
|
+
expect(values.budgeted).toBe(30000); // Should match the result
|
|
1354
|
+
expect(values.goal).toBe(100000); // Should be the goal amount
|
|
1355
|
+
expect(values.longGoal).toBe(true); // Should have a long goal
|
|
1356
|
+
expect(instance.isGoalOnly()).toBe(false); // Should not be goal only
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
it('should handle goal-only template through the entire process', async () => {
|
|
1360
|
+
const category: CategoryEntity = {
|
|
1361
|
+
id: 'test',
|
|
1362
|
+
name: 'Test Category',
|
|
1363
|
+
group: 'test-group',
|
|
1364
|
+
is_income: false,
|
|
1365
|
+
};
|
|
1366
|
+
const templates: Template[] = [
|
|
1367
|
+
{
|
|
1368
|
+
type: 'goal',
|
|
1369
|
+
amount: 1000,
|
|
1370
|
+
directive: 'goal',
|
|
1371
|
+
},
|
|
1372
|
+
];
|
|
1373
|
+
|
|
1374
|
+
// Mock the sheet values needed for init
|
|
1375
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(10000); // lastMonthBalance
|
|
1376
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
|
|
1377
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1378
|
+
mockPreferences(false, 'USD');
|
|
1379
|
+
|
|
1380
|
+
// Initialize the template
|
|
1381
|
+
const instance = await CategoryTemplateContext.init(
|
|
1382
|
+
templates,
|
|
1383
|
+
category,
|
|
1384
|
+
'2024-01',
|
|
1385
|
+
10000,
|
|
1386
|
+
);
|
|
1387
|
+
|
|
1388
|
+
expect(instance.isGoalOnly()).toBe(true); // Should be goal only
|
|
1389
|
+
// Get the final values
|
|
1390
|
+
const values = instance.getValues();
|
|
1391
|
+
|
|
1392
|
+
// Verify the results
|
|
1393
|
+
expect(values.budgeted).toBe(10000);
|
|
1394
|
+
expect(values.goal).toBe(100000); // Should be the goal amount
|
|
1395
|
+
expect(values.longGoal).toBe(true); // Should have a long goal
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it('should handle hide fraction', async () => {
|
|
1399
|
+
const category: CategoryEntity = {
|
|
1400
|
+
id: 'test',
|
|
1401
|
+
name: 'Test Category',
|
|
1402
|
+
group: 'test-group',
|
|
1403
|
+
is_income: false,
|
|
1404
|
+
};
|
|
1405
|
+
const templates: Template[] = [
|
|
1406
|
+
{
|
|
1407
|
+
type: 'simple',
|
|
1408
|
+
monthly: 100.89,
|
|
1409
|
+
directive: 'template',
|
|
1410
|
+
priority: 1,
|
|
1411
|
+
},
|
|
1412
|
+
{
|
|
1413
|
+
type: 'goal',
|
|
1414
|
+
amount: 1000,
|
|
1415
|
+
directive: 'goal',
|
|
1416
|
+
},
|
|
1417
|
+
];
|
|
1418
|
+
|
|
1419
|
+
// Mock the sheet values needed for init
|
|
1420
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0); // lastMonthBalance
|
|
1421
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false); // carryover
|
|
1422
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1423
|
+
mockPreferences(true, 'USD');
|
|
1424
|
+
|
|
1425
|
+
// Initialize the template
|
|
1426
|
+
const instance = await CategoryTemplateContext.init(
|
|
1427
|
+
templates,
|
|
1428
|
+
category,
|
|
1429
|
+
'2024-01',
|
|
1430
|
+
0,
|
|
1431
|
+
);
|
|
1432
|
+
|
|
1433
|
+
// Run the templates with more than enough funds
|
|
1434
|
+
const result = await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1435
|
+
|
|
1436
|
+
// Get the final values
|
|
1437
|
+
const values = instance.getValues();
|
|
1438
|
+
|
|
1439
|
+
// Verify the results
|
|
1440
|
+
expect(result).toBe(10100); // Should get full amount rounded up
|
|
1441
|
+
expect(values.budgeted).toBe(10100); // Should match the result
|
|
1442
|
+
expect(values.goal).toBe(100000); // Should be the goal amount
|
|
1443
|
+
expect(values.longGoal).toBe(true); // Should have a long goal
|
|
1444
|
+
expect(instance.isGoalOnly()).toBe(false); // Should not be goal only
|
|
1445
|
+
});
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
describe('JPY currency', () => {
|
|
1449
|
+
it('should handle simple template with JPY correctly', async () => {
|
|
1450
|
+
const category: CategoryEntity = {
|
|
1451
|
+
id: 'test',
|
|
1452
|
+
name: 'Test Category',
|
|
1453
|
+
group: 'test-group',
|
|
1454
|
+
is_income: false,
|
|
1455
|
+
};
|
|
1456
|
+
const template: Template = {
|
|
1457
|
+
type: 'simple',
|
|
1458
|
+
monthly: 50,
|
|
1459
|
+
directive: 'template',
|
|
1460
|
+
priority: 1,
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
|
|
1464
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
|
|
1465
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1466
|
+
mockPreferences(true, 'JPY');
|
|
1467
|
+
|
|
1468
|
+
const instance = await CategoryTemplateContext.init(
|
|
1469
|
+
[template],
|
|
1470
|
+
category,
|
|
1471
|
+
'2024-01',
|
|
1472
|
+
0,
|
|
1473
|
+
);
|
|
1474
|
+
|
|
1475
|
+
await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1476
|
+
const values = instance.getValues();
|
|
1477
|
+
|
|
1478
|
+
expect(values.budgeted).toBe(50);
|
|
1479
|
+
});
|
|
1480
|
+
|
|
1481
|
+
it('should handle small amounts with JPY correctly', async () => {
|
|
1482
|
+
const category: CategoryEntity = {
|
|
1483
|
+
id: 'test',
|
|
1484
|
+
name: 'Test Category',
|
|
1485
|
+
group: 'test-group',
|
|
1486
|
+
is_income: false,
|
|
1487
|
+
};
|
|
1488
|
+
const template: Template = {
|
|
1489
|
+
type: 'simple',
|
|
1490
|
+
monthly: 5,
|
|
1491
|
+
directive: 'template',
|
|
1492
|
+
priority: 1,
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
|
|
1496
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
|
|
1497
|
+
vi.mocked(actions.isReflectBudget).mockResolvedValueOnce(false);
|
|
1498
|
+
mockPreferences(true, 'JPY');
|
|
1499
|
+
|
|
1500
|
+
const instance = await CategoryTemplateContext.init(
|
|
1501
|
+
[template],
|
|
1502
|
+
category,
|
|
1503
|
+
'2024-01',
|
|
1504
|
+
0,
|
|
1505
|
+
);
|
|
1506
|
+
|
|
1507
|
+
await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1508
|
+
const values = instance.getValues();
|
|
1509
|
+
|
|
1510
|
+
expect(values.budgeted).toBe(5);
|
|
1511
|
+
});
|
|
1512
|
+
|
|
1513
|
+
it('should handle larger amounts with JPY correctly', async () => {
|
|
1514
|
+
const category: CategoryEntity = {
|
|
1515
|
+
id: 'test',
|
|
1516
|
+
name: 'Test Category',
|
|
1517
|
+
group: 'test-group',
|
|
1518
|
+
is_income: false,
|
|
1519
|
+
};
|
|
1520
|
+
const template: Template = {
|
|
1521
|
+
type: 'simple',
|
|
1522
|
+
monthly: 250,
|
|
1523
|
+
directive: 'template',
|
|
1524
|
+
priority: 1,
|
|
1525
|
+
};
|
|
1526
|
+
|
|
1527
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
|
|
1528
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
|
|
1529
|
+
mockPreferences(true, 'JPY');
|
|
1530
|
+
|
|
1531
|
+
const instance = await CategoryTemplateContext.init(
|
|
1532
|
+
[template],
|
|
1533
|
+
category,
|
|
1534
|
+
'2024-01',
|
|
1535
|
+
0,
|
|
1536
|
+
);
|
|
1537
|
+
|
|
1538
|
+
await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1539
|
+
const values = instance.getValues();
|
|
1540
|
+
|
|
1541
|
+
expect(values.budgeted).toBe(250);
|
|
1542
|
+
});
|
|
1543
|
+
|
|
1544
|
+
it('should handle weekly limit with JPY correctly', async () => {
|
|
1545
|
+
const category: CategoryEntity = {
|
|
1546
|
+
id: 'test',
|
|
1547
|
+
name: 'Test Category',
|
|
1548
|
+
group: 'test-group',
|
|
1549
|
+
is_income: false,
|
|
1550
|
+
};
|
|
1551
|
+
const template: Template = {
|
|
1552
|
+
type: 'simple',
|
|
1553
|
+
limit: {
|
|
1554
|
+
amount: 100,
|
|
1555
|
+
hold: false,
|
|
1556
|
+
period: 'weekly',
|
|
1557
|
+
start: '2024-01-01',
|
|
1558
|
+
},
|
|
1559
|
+
directive: 'template',
|
|
1560
|
+
priority: 1,
|
|
1561
|
+
};
|
|
1562
|
+
|
|
1563
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
|
|
1564
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
|
|
1565
|
+
mockPreferences(true, 'JPY');
|
|
1566
|
+
|
|
1567
|
+
const instance = await CategoryTemplateContext.init(
|
|
1568
|
+
[template],
|
|
1569
|
+
category,
|
|
1570
|
+
'2024-01',
|
|
1571
|
+
0,
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
const result = CategoryTemplateContext.runSimple(template, instance);
|
|
1575
|
+
|
|
1576
|
+
expect(result).toBeGreaterThanOrEqual(400);
|
|
1577
|
+
expect(result).toBeLessThanOrEqual(500);
|
|
1578
|
+
});
|
|
1579
|
+
|
|
1580
|
+
it('should handle periodic template with JPY correctly', async () => {
|
|
1581
|
+
const category: CategoryEntity = {
|
|
1582
|
+
id: 'test',
|
|
1583
|
+
name: 'Test Category',
|
|
1584
|
+
group: 'test-group',
|
|
1585
|
+
is_income: false,
|
|
1586
|
+
};
|
|
1587
|
+
const template: Template = {
|
|
1588
|
+
type: 'periodic',
|
|
1589
|
+
amount: 1000,
|
|
1590
|
+
period: { period: 'week', amount: 1 },
|
|
1591
|
+
starting: '2024-01-01',
|
|
1592
|
+
directive: 'template',
|
|
1593
|
+
priority: 1,
|
|
1594
|
+
};
|
|
1595
|
+
|
|
1596
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
|
|
1597
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
|
|
1598
|
+
mockPreferences(true, 'JPY');
|
|
1599
|
+
|
|
1600
|
+
const instance = await CategoryTemplateContext.init(
|
|
1601
|
+
[template],
|
|
1602
|
+
category,
|
|
1603
|
+
'2024-01',
|
|
1604
|
+
0,
|
|
1605
|
+
);
|
|
1606
|
+
|
|
1607
|
+
await instance.runTemplatesForPriority(1, 100000, 100000);
|
|
1608
|
+
const values = instance.getValues();
|
|
1609
|
+
|
|
1610
|
+
expect(values.budgeted).toBeGreaterThan(3500);
|
|
1611
|
+
expect(values.budgeted).toBeLessThan(5500);
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
it('should compare JPY vs USD for same template', async () => {
|
|
1615
|
+
const category: CategoryEntity = {
|
|
1616
|
+
id: 'test',
|
|
1617
|
+
name: 'Test Category',
|
|
1618
|
+
group: 'test-group',
|
|
1619
|
+
is_income: false,
|
|
1620
|
+
};
|
|
1621
|
+
const template: Template = {
|
|
1622
|
+
type: 'simple',
|
|
1623
|
+
monthly: 100,
|
|
1624
|
+
directive: 'template',
|
|
1625
|
+
priority: 1,
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
|
|
1629
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
|
|
1630
|
+
mockPreferences(true, 'JPY');
|
|
1631
|
+
|
|
1632
|
+
const instanceJPY = await CategoryTemplateContext.init(
|
|
1633
|
+
[template],
|
|
1634
|
+
category,
|
|
1635
|
+
'2024-01',
|
|
1636
|
+
0,
|
|
1637
|
+
);
|
|
1638
|
+
await instanceJPY.runTemplatesForPriority(1, 100000, 100000);
|
|
1639
|
+
const valuesJPY = instanceJPY.getValues();
|
|
1640
|
+
|
|
1641
|
+
vi.mocked(actions.getSheetValue).mockResolvedValueOnce(0);
|
|
1642
|
+
vi.mocked(actions.getSheetBoolean).mockResolvedValueOnce(false);
|
|
1643
|
+
mockPreferences(false, 'USD');
|
|
1644
|
+
|
|
1645
|
+
const instanceUSD = await CategoryTemplateContext.init(
|
|
1646
|
+
[template],
|
|
1647
|
+
category,
|
|
1648
|
+
'2024-01',
|
|
1649
|
+
0,
|
|
1650
|
+
);
|
|
1651
|
+
await instanceUSD.runTemplatesForPriority(1, 100000, 100000);
|
|
1652
|
+
const valuesUSD = instanceUSD.getValues();
|
|
1653
|
+
|
|
1654
|
+
expect(valuesJPY.budgeted).toBe(100);
|
|
1655
|
+
expect(valuesUSD.budgeted).toBe(10000);
|
|
1656
|
+
});
|
|
1657
|
+
});
|
|
1658
|
+
});
|