@dizzlkheinz/ynab-mcpb 0.18.4 → 0.19.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/CLAUDE.md +87 -8
- package/bin/ynab-mcp-server.cjs +2 -2
- package/bin/ynab-mcp-server.js +3 -3
- package/biome.json +39 -0
- package/dist/bundle/index.cjs +67 -67
- package/dist/index.d.ts +1 -1
- package/dist/index.js +27 -27
- package/dist/server/YNABMCPServer.d.ts +3 -4
- package/dist/server/YNABMCPServer.js +111 -116
- package/dist/server/budgetResolver.d.ts +6 -5
- package/dist/server/budgetResolver.js +46 -36
- package/dist/server/cacheKeys.js +6 -6
- package/dist/server/cacheManager.js +14 -11
- package/dist/server/completions.d.ts +2 -2
- package/dist/server/completions.js +20 -15
- package/dist/server/config.d.ts +10 -5
- package/dist/server/config.js +24 -7
- package/dist/server/deltaCache.d.ts +2 -2
- package/dist/server/deltaCache.js +22 -16
- package/dist/server/deltaCache.merge.d.ts +2 -2
- package/dist/server/diagnostics.d.ts +4 -4
- package/dist/server/diagnostics.js +38 -32
- package/dist/server/errorHandler.d.ts +5 -12
- package/dist/server/errorHandler.js +219 -217
- package/dist/server/prompts.d.ts +2 -2
- package/dist/server/prompts.js +45 -45
- package/dist/server/rateLimiter.js +4 -4
- package/dist/server/requestLogger.d.ts +1 -1
- package/dist/server/requestLogger.js +40 -35
- package/dist/server/resources.d.ts +3 -3
- package/dist/server/resources.js +55 -52
- package/dist/server/responseFormatter.js +6 -6
- package/dist/server/securityMiddleware.d.ts +2 -2
- package/dist/server/securityMiddleware.js +22 -20
- package/dist/server/serverKnowledgeStore.js +1 -1
- package/dist/server/toolRegistry.d.ts +3 -3
- package/dist/server/toolRegistry.js +47 -40
- package/dist/tools/__tests__/deltaTestUtils.d.ts +3 -3
- package/dist/tools/__tests__/deltaTestUtils.js +2 -2
- package/dist/tools/accountTools.d.ts +9 -8
- package/dist/tools/accountTools.js +47 -47
- package/dist/tools/adapters.d.ts +13 -8
- package/dist/tools/adapters.js +21 -11
- package/dist/tools/budgetTools.d.ts +8 -7
- package/dist/tools/budgetTools.js +22 -22
- package/dist/tools/categoryTools.d.ts +9 -8
- package/dist/tools/categoryTools.js +68 -59
- package/dist/tools/compareTransactions/formatter.d.ts +3 -3
- package/dist/tools/compareTransactions/formatter.js +9 -9
- package/dist/tools/compareTransactions/index.d.ts +6 -6
- package/dist/tools/compareTransactions/index.js +58 -43
- package/dist/tools/compareTransactions/matcher.d.ts +1 -1
- package/dist/tools/compareTransactions/matcher.js +28 -15
- package/dist/tools/compareTransactions/parser.d.ts +2 -2
- package/dist/tools/compareTransactions/parser.js +144 -138
- package/dist/tools/compareTransactions/types.d.ts +4 -4
- package/dist/tools/compareTransactions.d.ts +1 -1
- package/dist/tools/compareTransactions.js +1 -1
- package/dist/tools/deltaFetcher.d.ts +2 -2
- package/dist/tools/deltaFetcher.js +16 -15
- package/dist/tools/deltaSupport.d.ts +4 -4
- package/dist/tools/deltaSupport.js +35 -41
- package/dist/tools/exportTransactions.d.ts +5 -4
- package/dist/tools/exportTransactions.js +61 -59
- package/dist/tools/monthTools.d.ts +7 -6
- package/dist/tools/monthTools.js +31 -29
- package/dist/tools/payeeTools.d.ts +7 -6
- package/dist/tools/payeeTools.js +28 -28
- package/dist/tools/reconcileAdapter.d.ts +2 -2
- package/dist/tools/reconcileAdapter.js +19 -12
- package/dist/tools/reconciliation/analyzer.d.ts +4 -4
- package/dist/tools/reconciliation/analyzer.js +73 -59
- package/dist/tools/reconciliation/csvParser.d.ts +3 -3
- package/dist/tools/reconciliation/csvParser.js +128 -104
- package/dist/tools/reconciliation/executor.d.ts +4 -4
- package/dist/tools/reconciliation/executor.js +148 -109
- package/dist/tools/reconciliation/index.d.ts +10 -10
- package/dist/tools/reconciliation/index.js +96 -83
- package/dist/tools/reconciliation/matcher.d.ts +3 -3
- package/dist/tools/reconciliation/matcher.js +17 -16
- package/dist/tools/reconciliation/payeeNormalizer.js +19 -8
- package/dist/tools/reconciliation/recommendationEngine.d.ts +1 -1
- package/dist/tools/reconciliation/recommendationEngine.js +40 -40
- package/dist/tools/reconciliation/reportFormatter.d.ts +2 -2
- package/dist/tools/reconciliation/reportFormatter.js +59 -58
- package/dist/tools/reconciliation/signDetector.d.ts +1 -1
- package/dist/tools/reconciliation/types.d.ts +16 -16
- package/dist/tools/reconciliation/ynabAdapter.d.ts +2 -2
- package/dist/tools/schemas/common.d.ts +1 -1
- package/dist/tools/schemas/common.js +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -18
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/budgetOutputs.js +14 -11
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/categoryOutputs.js +49 -29
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/comparisonOutputs.js +12 -12
- package/dist/tools/schemas/outputs/index.d.ts +14 -14
- package/dist/tools/schemas/outputs/index.js +14 -14
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/monthOutputs.js +56 -41
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/payeeOutputs.js +10 -10
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +2 -2
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +45 -45
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +28 -22
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/transactionOutputs.js +43 -35
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +1 -1
- package/dist/tools/schemas/outputs/utilityOutputs.js +5 -3
- package/dist/tools/schemas/shared/commonOutputs.d.ts +1 -1
- package/dist/tools/schemas/shared/commonOutputs.js +15 -9
- package/dist/tools/transactionReadTools.d.ts +11 -0
- package/dist/tools/transactionReadTools.js +202 -0
- package/dist/tools/transactionSchemas.d.ts +7 -7
- package/dist/tools/transactionSchemas.js +77 -57
- package/dist/tools/transactionTools.d.ts +6 -24
- package/dist/tools/transactionTools.js +7 -1499
- package/dist/tools/transactionUtils.d.ts +6 -6
- package/dist/tools/transactionUtils.js +78 -63
- package/dist/tools/transactionWriteTools.d.ts +20 -0
- package/dist/tools/transactionWriteTools.js +1342 -0
- package/dist/tools/utilityTools.d.ts +5 -4
- package/dist/tools/utilityTools.js +11 -11
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.js +6 -6
- package/dist/types/reconciliation.d.ts +1 -1
- package/dist/types/toolRegistration.d.ts +14 -12
- package/dist/utils/amountUtils.js +1 -1
- package/dist/utils/dateUtils.js +4 -4
- package/dist/utils/errors.d.ts +3 -3
- package/dist/utils/errors.js +4 -4
- package/dist/utils/money.d.ts +2 -2
- package/dist/utils/money.js +8 -8
- package/dist/utils/validationError.d.ts +1 -1
- package/dist/utils/validationError.js +1 -1
- package/docs/assets/examples/reconciliation-with-recommendations.json +66 -66
- package/docs/assets/schemas/reconciliation-v2.json +360 -336
- package/esbuild.config.mjs +53 -50
- package/meta.json +12548 -12548
- package/package.json +98 -111
- package/scripts/analyze-bundle.mjs +33 -30
- package/scripts/create-pr-description.js +169 -120
- package/scripts/run-all-tests.js +178 -169
- package/scripts/run-domain-integration-tests.js +28 -18
- package/scripts/run-generate-mcpb.js +19 -17
- package/scripts/run-throttled-integration-tests.js +92 -83
- package/scripts/test-delta-params.mjs +149 -120
- package/scripts/test-recommendations.ts +36 -32
- package/scripts/tmpTransaction.ts +80 -43
- package/scripts/validate-env.js +98 -91
- package/scripts/verify-build.js +78 -76
- package/src/__tests__/comprehensive.integration.test.ts +1281 -1154
- package/src/__tests__/performance.test.ts +723 -671
- package/src/__tests__/setup.ts +442 -395
- package/src/__tests__/smoke.e2e.test.ts +41 -39
- package/src/__tests__/testRunner.ts +314 -295
- package/src/__tests__/testUtils.ts +456 -364
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +109 -107
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +41 -41
- package/src/index.ts +68 -59
- package/src/server/CLAUDE.md +480 -0
- package/src/server/YNABMCPServer.ts +821 -794
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +929 -893
- package/src/server/__tests__/YNABMCPServer.test.ts +903 -899
- package/src/server/__tests__/budgetResolver.test.ts +466 -423
- package/src/server/__tests__/cacheManager.test.ts +891 -874
- package/src/server/__tests__/completions.integration.test.ts +115 -106
- package/src/server/__tests__/completions.test.ts +334 -313
- package/src/server/__tests__/config.test.ts +98 -86
- package/src/server/__tests__/deltaCache.merge.test.ts +774 -703
- package/src/server/__tests__/deltaCache.swr.test.ts +198 -153
- package/src/server/__tests__/deltaCache.test.ts +946 -759
- package/src/server/__tests__/diagnostics.test.ts +825 -792
- package/src/server/__tests__/errorHandler.integration.test.ts +512 -462
- package/src/server/__tests__/errorHandler.test.ts +402 -397
- package/src/server/__tests__/prompts.test.ts +424 -347
- package/src/server/__tests__/rateLimiter.test.ts +313 -309
- package/src/server/__tests__/requestLogger.test.ts +443 -403
- package/src/server/__tests__/resources.template.test.ts +196 -185
- package/src/server/__tests__/resources.test.ts +294 -288
- package/src/server/__tests__/security.integration.test.ts +487 -421
- package/src/server/__tests__/securityMiddleware.test.ts +519 -444
- package/src/server/__tests__/server-startup.integration.test.ts +509 -490
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -173
- package/src/server/__tests__/toolRegistration.test.ts +239 -210
- package/src/server/__tests__/toolRegistry.test.ts +907 -845
- package/src/server/budgetResolver.ts +221 -181
- package/src/server/cacheKeys.ts +6 -6
- package/src/server/cacheManager.ts +498 -484
- package/src/server/completions.ts +267 -243
- package/src/server/config.ts +35 -14
- package/src/server/deltaCache.merge.ts +146 -128
- package/src/server/deltaCache.ts +352 -309
- package/src/server/diagnostics.ts +257 -242
- package/src/server/errorHandler.ts +747 -744
- package/src/server/prompts.ts +181 -176
- package/src/server/rateLimiter.ts +131 -129
- package/src/server/requestLogger.ts +350 -322
- package/src/server/resources.ts +442 -374
- package/src/server/responseFormatter.ts +41 -37
- package/src/server/securityMiddleware.ts +223 -205
- package/src/server/serverKnowledgeStore.ts +67 -67
- package/src/server/toolRegistry.ts +508 -474
- package/src/tools/CLAUDE.md +604 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -111
- package/src/tools/__tests__/accountTools.integration.test.ts +129 -111
- package/src/tools/__tests__/accountTools.test.ts +685 -638
- package/src/tools/__tests__/adapters.test.ts +142 -108
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +73 -73
- package/src/tools/__tests__/budgetTools.integration.test.ts +132 -124
- package/src/tools/__tests__/budgetTools.test.ts +442 -413
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +76 -68
- package/src/tools/__tests__/categoryTools.integration.test.ts +314 -288
- package/src/tools/__tests__/categoryTools.test.ts +656 -625
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +535 -462
- package/src/tools/__tests__/compareTransactions/index.test.ts +378 -358
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +497 -398
- package/src/tools/__tests__/compareTransactions/parser.test.ts +765 -747
- package/src/tools/__tests__/compareTransactions.test.ts +352 -332
- package/src/tools/__tests__/compareTransactions.window.test.ts +150 -146
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +69 -65
- package/src/tools/__tests__/deltaFetcher.test.ts +325 -265
- package/src/tools/__tests__/deltaSupport.test.ts +211 -184
- package/src/tools/__tests__/deltaTestUtils.ts +37 -33
- package/src/tools/__tests__/exportTransactions.test.ts +205 -200
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/monthTools.integration.test.ts +178 -166
- package/src/tools/__tests__/monthTools.test.ts +561 -512
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +68 -68
- package/src/tools/__tests__/payeeTools.integration.test.ts +158 -142
- package/src/tools/__tests__/payeeTools.test.ts +486 -434
- package/src/tools/__tests__/transactionSchemas.test.ts +1202 -1186
- package/src/tools/__tests__/transactionTools.integration.test.ts +875 -825
- package/src/tools/__tests__/transactionTools.test.ts +4923 -4366
- package/src/tools/__tests__/transactionUtils.test.ts +1004 -977
- package/src/tools/__tests__/utilityTools.integration.test.ts +32 -32
- package/src/tools/__tests__/utilityTools.test.ts +68 -58
- package/src/tools/accountTools.ts +293 -271
- package/src/tools/adapters.ts +120 -63
- package/src/tools/budgetTools.ts +121 -116
- package/src/tools/categoryTools.ts +379 -339
- package/src/tools/compareTransactions/formatter.ts +131 -119
- package/src/tools/compareTransactions/index.ts +249 -214
- package/src/tools/compareTransactions/matcher.ts +259 -209
- package/src/tools/compareTransactions/parser.ts +517 -487
- package/src/tools/compareTransactions/types.ts +38 -38
- package/src/tools/compareTransactions.ts +1 -1
- package/src/tools/deltaFetcher.ts +281 -260
- package/src/tools/deltaSupport.ts +264 -259
- package/src/tools/exportTransactions.ts +230 -218
- package/src/tools/monthTools.ts +180 -165
- package/src/tools/payeeTools.ts +152 -140
- package/src/tools/reconcileAdapter.ts +297 -252
- package/src/tools/reconciliation/CLAUDE.md +506 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +133 -124
- package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -230
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -400
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +71 -69
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +348 -323
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +503 -457
- package/src/tools/reconciliation/__tests__/executor.test.ts +898 -831
- package/src/tools/reconciliation/__tests__/matcher.test.ts +667 -663
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +296 -276
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +692 -624
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1008 -989
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -533
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -74
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -62
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +56 -55
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +209 -206
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +66 -60
- package/src/tools/reconciliation/analyzer.ts +564 -504
- package/src/tools/reconciliation/csvParser.ts +656 -609
- package/src/tools/reconciliation/executor.ts +1290 -1128
- package/src/tools/reconciliation/index.ts +580 -528
- package/src/tools/reconciliation/matcher.ts +256 -240
- package/src/tools/reconciliation/payeeNormalizer.ts +92 -78
- package/src/tools/reconciliation/recommendationEngine.ts +357 -345
- package/src/tools/reconciliation/reportFormatter.ts +343 -307
- package/src/tools/reconciliation/signDetector.ts +89 -83
- package/src/tools/reconciliation/types.ts +164 -159
- package/src/tools/reconciliation/ynabAdapter.ts +17 -15
- package/src/tools/schemas/CLAUDE.md +546 -0
- package/src/tools/schemas/common.ts +1 -1
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +410 -409
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +305 -299
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +431 -430
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +510 -495
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +179 -153
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +293 -254
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +457 -457
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +362 -356
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +402 -399
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +225 -211
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +457 -454
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +316 -315
- package/src/tools/schemas/outputs/accountOutputs.ts +40 -34
- package/src/tools/schemas/outputs/budgetOutputs.ts +24 -19
- package/src/tools/schemas/outputs/categoryOutputs.ts +76 -56
- package/src/tools/schemas/outputs/comparisonOutputs.ts +192 -169
- package/src/tools/schemas/outputs/index.ts +163 -163
- package/src/tools/schemas/outputs/monthOutputs.ts +95 -80
- package/src/tools/schemas/outputs/payeeOutputs.ts +18 -18
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +386 -373
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +259 -231
- package/src/tools/schemas/outputs/transactionOutputs.ts +81 -71
- package/src/tools/schemas/outputs/utilityOutputs.ts +90 -84
- package/src/tools/schemas/shared/commonOutputs.ts +27 -19
- package/src/tools/toolCategories.ts +114 -114
- package/src/tools/transactionReadTools.ts +327 -0
- package/src/tools/transactionSchemas.ts +322 -291
- package/src/tools/transactionTools.ts +84 -2246
- package/src/tools/transactionUtils.ts +507 -422
- package/src/tools/transactionWriteTools.ts +2110 -0
- package/src/tools/utilityTools.ts +46 -41
- package/src/types/CLAUDE.md +477 -0
- package/src/types/__tests__/index.test.ts +51 -51
- package/src/types/index.ts +43 -39
- package/src/types/integration-tests.d.ts +26 -26
- package/src/types/reconciliation.ts +29 -29
- package/src/types/toolAnnotations.ts +30 -30
- package/src/types/toolRegistration.ts +43 -32
- package/src/utils/CLAUDE.md +508 -0
- package/src/utils/__tests__/dateUtils.test.ts +174 -168
- package/src/utils/__tests__/money.test.ts +193 -187
- package/src/utils/amountUtils.ts +5 -5
- package/src/utils/baseError.ts +5 -5
- package/src/utils/dateUtils.ts +29 -26
- package/src/utils/errors.ts +14 -14
- package/src/utils/money.ts +66 -52
- package/src/utils/validationError.ts +1 -1
- package/tsconfig.json +29 -29
- package/tsconfig.prod.json +16 -16
- package/vitest-reporters/split-json-reporter.ts +247 -204
- package/vitest.config.ts +99 -95
- package/.prettierignore +0 -10
- package/.prettierrc.json +0 -10
- package/eslint.config.js +0 -49
|
@@ -5,23 +5,32 @@
|
|
|
5
5
|
* V2 UPDATE: Uses new parser and matcher (milliunits based)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type * as ynab from
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
import type * as ynab from "ynab";
|
|
9
|
+
import {
|
|
10
|
+
type CSVParseResult,
|
|
11
|
+
type ParseCSVOptions,
|
|
12
|
+
parseCSV,
|
|
13
|
+
} from "./csvParser.js";
|
|
14
|
+
import {
|
|
15
|
+
DEFAULT_CONFIG,
|
|
16
|
+
type MatchingConfig,
|
|
17
|
+
findMatches,
|
|
18
|
+
normalizeConfig,
|
|
19
|
+
} from "./matcher.js";
|
|
20
|
+
import { normalizeYNABTransactions } from "./ynabAdapter.js";
|
|
21
|
+
|
|
22
|
+
import { toMoneyValue } from "../../utils/money.js";
|
|
23
|
+
import type { MatchResult } from "./matcher.js"; // Import MatchResult
|
|
24
|
+
import { generateRecommendations } from "./recommendationEngine.js";
|
|
13
25
|
import type {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from
|
|
22
|
-
import type { MatchResult } from './matcher.js'; // Import MatchResult
|
|
23
|
-
import { toMoneyValue } from '../../utils/money.js';
|
|
24
|
-
import { generateRecommendations } from './recommendationEngine.js';
|
|
26
|
+
BalanceInfo,
|
|
27
|
+
BankTransaction,
|
|
28
|
+
ReconciliationAnalysis,
|
|
29
|
+
ReconciliationInsight,
|
|
30
|
+
ReconciliationSummary,
|
|
31
|
+
TransactionMatch,
|
|
32
|
+
YNABTransaction,
|
|
33
|
+
} from "./types.js";
|
|
25
34
|
|
|
26
35
|
// --- Helper Functions ---
|
|
27
36
|
|
|
@@ -30,26 +39,32 @@ import { generateRecommendations } from './recommendationEngine.js';
|
|
|
30
39
|
* Returns { minDate, maxDate } as ISO date strings (YYYY-MM-DD)
|
|
31
40
|
*/
|
|
32
41
|
function calculateDateRange(bankTransactions: BankTransaction[]): {
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
minDate: string;
|
|
43
|
+
maxDate: string;
|
|
35
44
|
} | null {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
if (bankTransactions.length === 0) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const dates = bankTransactions
|
|
50
|
+
.map((t) => t.date)
|
|
51
|
+
.filter((d) => d && /^\d{4}-\d{2}-\d{2}$/.test(d))
|
|
52
|
+
.sort();
|
|
53
|
+
|
|
54
|
+
if (dates.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const minDate = dates[0];
|
|
59
|
+
const maxDate = dates[dates.length - 1];
|
|
60
|
+
if (!minDate || !maxDate) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
minDate,
|
|
66
|
+
maxDate,
|
|
67
|
+
};
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/**
|
|
@@ -59,363 +74,392 @@ function calculateDateRange(bankTransactions: BankTransaction[]): {
|
|
|
59
74
|
* @param dateToleranceDays - Buffer to add to the date range to account for bank posting delays
|
|
60
75
|
*/
|
|
61
76
|
function filterByDateRange(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
ynabTransactions: YNABTransaction[],
|
|
78
|
+
dateRange: { minDate: string; maxDate: string },
|
|
79
|
+
dateToleranceDays = 7,
|
|
65
80
|
): { inRange: YNABTransaction[]; outsideRange: YNABTransaction[] } {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
81
|
+
// Validate dateToleranceDays is non-negative
|
|
82
|
+
const safeToleranceDays = dateToleranceDays < 0 ? 0 : dateToleranceDays;
|
|
83
|
+
if (dateToleranceDays < 0) {
|
|
84
|
+
console.warn(
|
|
85
|
+
`[filterByDateRange] dateToleranceDays must be non-negative, got ${dateToleranceDays}. Using 0.`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const inRange: YNABTransaction[] = [];
|
|
90
|
+
const outsideRange: YNABTransaction[] = [];
|
|
91
|
+
|
|
92
|
+
// Parse date parts and use Date.UTC to avoid timezone issues
|
|
93
|
+
// This prevents 'off-by-one-day' errors from timezone conversions
|
|
94
|
+
const minParts = dateRange.minDate.split("-").map(Number);
|
|
95
|
+
const maxParts = dateRange.maxDate.split("-").map(Number);
|
|
96
|
+
|
|
97
|
+
// Validate date parts are valid numbers
|
|
98
|
+
if (
|
|
99
|
+
minParts.length !== 3 ||
|
|
100
|
+
maxParts.length !== 3 ||
|
|
101
|
+
minParts.some((n) => !Number.isFinite(n)) ||
|
|
102
|
+
maxParts.some((n) => !Number.isFinite(n))
|
|
103
|
+
) {
|
|
104
|
+
console.warn(
|
|
105
|
+
`[filterByDateRange] Invalid date format in range: ${dateRange.minDate} to ${dateRange.maxDate} - returning all transactions`,
|
|
106
|
+
);
|
|
107
|
+
return { inRange: ynabTransactions, outsideRange: [] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const [minYear, minMonth, minDay] = minParts as [number, number, number];
|
|
111
|
+
const [maxYear, maxMonth, maxDay] = maxParts as [number, number, number];
|
|
112
|
+
|
|
113
|
+
// Add buffer to date range to account for bank posting delays
|
|
114
|
+
// Note: Date.UTC automatically handles month rollover if day goes negative
|
|
115
|
+
// (e.g., day 3 - 7 days = -4 correctly rolls back to previous month)
|
|
116
|
+
const minDateWithBuffer = new Date(
|
|
117
|
+
Date.UTC(minYear, minMonth - 1, minDay - safeToleranceDays),
|
|
118
|
+
);
|
|
119
|
+
const minDateStr = minDateWithBuffer.toISOString().split("T")[0] ?? "";
|
|
120
|
+
|
|
121
|
+
const maxDateWithBuffer = new Date(
|
|
122
|
+
Date.UTC(maxYear, maxMonth - 1, maxDay + safeToleranceDays),
|
|
123
|
+
);
|
|
124
|
+
const maxDateStr = maxDateWithBuffer.toISOString().split("T")[0] ?? "";
|
|
125
|
+
|
|
126
|
+
for (const txn of ynabTransactions) {
|
|
127
|
+
// Compare dates as strings (YYYY-MM-DD format sorts correctly)
|
|
128
|
+
if (txn.date >= minDateStr && txn.date <= maxDateStr) {
|
|
129
|
+
inRange.push(txn);
|
|
130
|
+
} else {
|
|
131
|
+
outsideRange.push(txn);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { inRange, outsideRange };
|
|
117
136
|
}
|
|
118
137
|
|
|
119
138
|
function mapToTransactionMatch(result: MatchResult): TransactionMatch {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
const candidates = result.candidates.map((c) => ({
|
|
140
|
+
ynab_transaction: c.ynabTransaction,
|
|
141
|
+
confidence: c.scores.combined,
|
|
142
|
+
match_reason: c.matchReasons.join(", "),
|
|
143
|
+
explanation: c.matchReasons.join(", "),
|
|
144
|
+
}));
|
|
145
|
+
|
|
146
|
+
const match: TransactionMatch = {
|
|
147
|
+
bankTransaction: result.bankTransaction,
|
|
148
|
+
candidates,
|
|
149
|
+
confidence: result.confidence,
|
|
150
|
+
confidenceScore: result.confidenceScore,
|
|
151
|
+
matchReason: result.bestMatch?.matchReasons.join(", ") ?? "No match found",
|
|
152
|
+
actionHint: result.confidence === "high" ? "approve" : "review",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (result.bestMatch) {
|
|
156
|
+
match.ynabTransaction = result.bestMatch.ynabTransaction;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (result.candidates[0]) {
|
|
160
|
+
match.topConfidence = result.candidates[0].scores.combined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (result.confidence === "none") {
|
|
164
|
+
match.recommendation =
|
|
165
|
+
"This bank transaction is not in YNAB. Consider adding it.";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return match;
|
|
149
169
|
}
|
|
150
170
|
|
|
151
171
|
function calculateBalances(
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
172
|
+
ynabTransactions: YNABTransaction[],
|
|
173
|
+
statementBalanceDecimal: number,
|
|
174
|
+
currency: string,
|
|
175
|
+
accountSnapshot?: {
|
|
176
|
+
balance?: number;
|
|
177
|
+
cleared_balance?: number;
|
|
178
|
+
uncleared_balance?: number;
|
|
179
|
+
},
|
|
156
180
|
): BalanceInfo {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
// Compute from the fetched transactions, but prefer the authoritative account snapshot
|
|
182
|
+
// because we usually fetch a limited date window.
|
|
183
|
+
let computedCleared = 0;
|
|
184
|
+
let computedUncleared = 0;
|
|
185
|
+
|
|
186
|
+
for (const txn of ynabTransactions) {
|
|
187
|
+
const amount = txn.amount; // Milliunits
|
|
188
|
+
|
|
189
|
+
if (txn.cleared === "cleared" || txn.cleared === "reconciled") {
|
|
190
|
+
computedCleared += amount;
|
|
191
|
+
} else {
|
|
192
|
+
computedUncleared += amount;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const clearedBalance = accountSnapshot?.cleared_balance ?? computedCleared;
|
|
197
|
+
const unclearedBalance =
|
|
198
|
+
accountSnapshot?.uncleared_balance ?? computedUncleared;
|
|
199
|
+
const totalBalance =
|
|
200
|
+
accountSnapshot?.balance ?? clearedBalance + unclearedBalance;
|
|
201
|
+
|
|
202
|
+
const statementBalanceMilli = Math.round(statementBalanceDecimal * 1000);
|
|
203
|
+
const discrepancy = clearedBalance - statementBalanceMilli;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
current_cleared: toMoneyValue(clearedBalance, currency),
|
|
207
|
+
current_uncleared: toMoneyValue(unclearedBalance, currency),
|
|
208
|
+
current_total: toMoneyValue(totalBalance, currency),
|
|
209
|
+
target_statement: toMoneyValue(statementBalanceMilli, currency),
|
|
210
|
+
discrepancy: toMoneyValue(discrepancy, currency),
|
|
211
|
+
on_track: Math.abs(discrepancy) < 10, // Within 1 cent (10 milliunits)
|
|
212
|
+
};
|
|
187
213
|
}
|
|
188
214
|
|
|
189
215
|
function generateSummary(
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
216
|
+
bankTransactions: BankTransaction[],
|
|
217
|
+
ynabTransactionsInRange: YNABTransaction[],
|
|
218
|
+
ynabTransactionsOutsideRange: YNABTransaction[],
|
|
219
|
+
autoMatches: TransactionMatch[],
|
|
220
|
+
suggestedMatches: TransactionMatch[],
|
|
221
|
+
unmatchedBank: BankTransaction[],
|
|
222
|
+
unmatchedYNAB: YNABTransaction[],
|
|
223
|
+
balances: BalanceInfo,
|
|
198
224
|
): ReconciliationSummary {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
225
|
+
// Determine date range from bank transactions
|
|
226
|
+
const dates = bankTransactions.map((t) => t.date).sort();
|
|
227
|
+
const dateRange =
|
|
228
|
+
dates.length > 0 ? `${dates[0]} to ${dates[dates.length - 1]}` : "Unknown";
|
|
229
|
+
|
|
230
|
+
// Total YNAB transactions = in range + outside range
|
|
231
|
+
const totalYnabCount =
|
|
232
|
+
ynabTransactionsInRange.length + ynabTransactionsOutsideRange.length;
|
|
233
|
+
|
|
234
|
+
// Build discrepancy explanation
|
|
235
|
+
let discrepancyExplanation = "";
|
|
236
|
+
if (balances.on_track) {
|
|
237
|
+
discrepancyExplanation = "Cleared balance matches statement";
|
|
238
|
+
} else {
|
|
239
|
+
const actionsNeeded: string[] = [];
|
|
240
|
+
if (autoMatches.length > 0) {
|
|
241
|
+
actionsNeeded.push(`clear ${autoMatches.length} transactions`);
|
|
242
|
+
}
|
|
243
|
+
if (unmatchedBank.length > 0) {
|
|
244
|
+
actionsNeeded.push(`add ${unmatchedBank.length} missing`);
|
|
245
|
+
}
|
|
246
|
+
if (unmatchedYNAB.length > 0) {
|
|
247
|
+
actionsNeeded.push(`review ${unmatchedYNAB.length} unmatched YNAB`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
discrepancyExplanation =
|
|
251
|
+
actionsNeeded.length > 0
|
|
252
|
+
? `Need to ${actionsNeeded.join(", ")}`
|
|
253
|
+
: "Manual review required";
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
statement_date_range: dateRange,
|
|
258
|
+
bank_transactions_count: bankTransactions.length,
|
|
259
|
+
ynab_transactions_count: totalYnabCount,
|
|
260
|
+
ynab_in_range_count: ynabTransactionsInRange.length,
|
|
261
|
+
ynab_outside_range_count: ynabTransactionsOutsideRange.length,
|
|
262
|
+
auto_matched: autoMatches.length,
|
|
263
|
+
suggested_matches: suggestedMatches.length,
|
|
264
|
+
unmatched_bank: unmatchedBank.length,
|
|
265
|
+
unmatched_ynab: unmatchedYNAB.length,
|
|
266
|
+
current_cleared_balance: balances.current_cleared,
|
|
267
|
+
target_statement_balance: balances.target_statement,
|
|
268
|
+
discrepancy: balances.discrepancy,
|
|
269
|
+
discrepancy_explanation: discrepancyExplanation,
|
|
270
|
+
};
|
|
241
271
|
}
|
|
242
272
|
|
|
243
273
|
function generateNextSteps(summary: ReconciliationSummary): string[] {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
274
|
+
const steps: string[] = [];
|
|
275
|
+
|
|
276
|
+
if (summary.auto_matched > 0) {
|
|
277
|
+
steps.push(
|
|
278
|
+
`Review ${summary.auto_matched} auto-matched transactions for approval`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (summary.suggested_matches > 0) {
|
|
283
|
+
steps.push(
|
|
284
|
+
`Review ${summary.suggested_matches} suggested matches and choose best match`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (summary.unmatched_bank > 0) {
|
|
289
|
+
steps.push(
|
|
290
|
+
`Decide whether to add ${summary.unmatched_bank} missing bank transactions to YNAB`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (summary.unmatched_ynab > 0) {
|
|
295
|
+
steps.push(
|
|
296
|
+
`Decide what to do with ${summary.unmatched_ynab} unmatched YNAB transactions (unclear/delete/ignore)`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (steps.length === 0) {
|
|
301
|
+
steps.push(
|
|
302
|
+
"All transactions matched! Review and approve to complete reconciliation",
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return steps;
|
|
269
307
|
}
|
|
270
308
|
|
|
271
|
-
function formatCurrency(amountMilli: number, currency
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
309
|
+
function formatCurrency(amountMilli: number, currency = "USD"): string {
|
|
310
|
+
const formatter = new Intl.NumberFormat("en-US", {
|
|
311
|
+
style: "currency",
|
|
312
|
+
currency: currency,
|
|
313
|
+
minimumFractionDigits: 2,
|
|
314
|
+
maximumFractionDigits: 2,
|
|
315
|
+
});
|
|
316
|
+
return formatter.format(amountMilli / 1000);
|
|
279
317
|
}
|
|
280
318
|
|
|
281
319
|
// --- Insight Generation ---
|
|
282
320
|
|
|
283
321
|
function repeatAmountInsights(
|
|
284
|
-
|
|
285
|
-
|
|
322
|
+
unmatchedBank: BankTransaction[],
|
|
323
|
+
currency = "USD",
|
|
286
324
|
): ReconciliationInsight[] {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
325
|
+
const insights: ReconciliationInsight[] = [];
|
|
326
|
+
if (unmatchedBank.length === 0) {
|
|
327
|
+
return insights;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Group by milliunits amount
|
|
331
|
+
const frequency = new Map<
|
|
332
|
+
number,
|
|
333
|
+
{ amount: number; txns: BankTransaction[] }
|
|
334
|
+
>();
|
|
335
|
+
|
|
336
|
+
for (const txn of unmatchedBank) {
|
|
337
|
+
const key = txn.amount;
|
|
338
|
+
const entry = frequency.get(key) ?? { amount: txn.amount, txns: [] };
|
|
339
|
+
entry.txns.push(txn);
|
|
340
|
+
frequency.set(key, entry);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const repeated = Array.from(frequency.values())
|
|
344
|
+
.filter((entry) => entry.txns.length >= 2)
|
|
345
|
+
.sort((a, b) => b.txns.length - a.txns.length);
|
|
346
|
+
|
|
347
|
+
if (repeated.length === 0) {
|
|
348
|
+
return insights;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const top = repeated[0];
|
|
352
|
+
if (!top) {
|
|
353
|
+
return insights;
|
|
354
|
+
}
|
|
355
|
+
insights.push({
|
|
356
|
+
id: `repeat-${top.amount}`,
|
|
357
|
+
type: "repeat_amount",
|
|
358
|
+
severity: top.txns.length >= 4 ? "critical" : "warning",
|
|
359
|
+
title: `${top.txns.length} unmatched transactions at ${formatCurrency(top.amount, currency)}`,
|
|
360
|
+
description: `The bank statement shows ${top.txns.length} unmatched transaction(s) at ${formatCurrency(top.amount, currency)}. Repeated amounts are usually the quickest wins — reconcile these first.`,
|
|
361
|
+
evidence: {
|
|
362
|
+
amount: top.amount, // Milliunits
|
|
363
|
+
occurrences: top.txns.length,
|
|
364
|
+
dates: top.txns.map((txn) => txn.date),
|
|
365
|
+
csv_rows: top.txns.map((txn) => txn.sourceRow),
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
return insights;
|
|
328
370
|
}
|
|
329
371
|
|
|
330
372
|
function anomalyInsights(balances: BalanceInfo): ReconciliationInsight[] {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
373
|
+
const insights: ReconciliationInsight[] = [];
|
|
374
|
+
const discrepancyAbs = Math.abs(balances.discrepancy.value_milliunits);
|
|
375
|
+
|
|
376
|
+
if (discrepancyAbs >= 1000) {
|
|
377
|
+
// 1 dollar
|
|
378
|
+
insights.push({
|
|
379
|
+
id: "balance-gap",
|
|
380
|
+
type: "anomaly",
|
|
381
|
+
severity: discrepancyAbs >= 100000 ? "critical" : "warning", // 100 dollars
|
|
382
|
+
title: `Cleared balance off by ${balances.discrepancy.value_display}`,
|
|
383
|
+
description:
|
|
384
|
+
`YNAB cleared balance is ${balances.current_cleared.value_display} but the statement expects ` +
|
|
385
|
+
`${balances.target_statement.value_display}. Focus on closing this gap.`,
|
|
386
|
+
evidence: {
|
|
387
|
+
cleared_balance: balances.current_cleared,
|
|
388
|
+
statement_balance: balances.target_statement,
|
|
389
|
+
discrepancy: balances.discrepancy,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return insights;
|
|
353
395
|
}
|
|
354
396
|
|
|
355
397
|
function detectInsights(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
398
|
+
unmatchedBank: BankTransaction[],
|
|
399
|
+
_summary: ReconciliationSummary,
|
|
400
|
+
balances: BalanceInfo,
|
|
401
|
+
currency: string,
|
|
402
|
+
csvErrors: { row: number; field: string; message: string }[] = [],
|
|
403
|
+
csvWarnings: { row: number; message: string }[] = [],
|
|
362
404
|
): ReconciliationInsight[] {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
405
|
+
const insights: ReconciliationInsight[] = [];
|
|
406
|
+
const seen = new Set<string>();
|
|
407
|
+
|
|
408
|
+
const addUnique = (insight: ReconciliationInsight) => {
|
|
409
|
+
if (seen.has(insight.id)) return;
|
|
410
|
+
seen.add(insight.id);
|
|
411
|
+
insights.push(insight);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Surface CSV parsing errors
|
|
415
|
+
if (csvErrors.length > 0) {
|
|
416
|
+
addUnique({
|
|
417
|
+
id: "csv-parse-errors",
|
|
418
|
+
type: "anomaly",
|
|
419
|
+
severity: csvErrors.length >= 5 ? "critical" : "warning",
|
|
420
|
+
title: `${csvErrors.length} CSV parsing error(s)`,
|
|
421
|
+
description:
|
|
422
|
+
csvErrors
|
|
423
|
+
.slice(0, 3)
|
|
424
|
+
.map((e) => `Row ${e.row}: ${e.message}`)
|
|
425
|
+
.join("; ") +
|
|
426
|
+
(csvErrors.length > 3 ? ` (+${csvErrors.length - 3} more)` : ""),
|
|
427
|
+
evidence: {
|
|
428
|
+
error_count: csvErrors.length,
|
|
429
|
+
errors: csvErrors.slice(0, 5),
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Surface CSV parsing warnings
|
|
435
|
+
if (csvWarnings.length > 0) {
|
|
436
|
+
addUnique({
|
|
437
|
+
id: "csv-parse-warnings",
|
|
438
|
+
type: "anomaly",
|
|
439
|
+
severity: "info",
|
|
440
|
+
title: `${csvWarnings.length} CSV parsing warning(s)`,
|
|
441
|
+
description:
|
|
442
|
+
csvWarnings
|
|
443
|
+
.slice(0, 3)
|
|
444
|
+
.map((w) => `Row ${w.row}: ${w.message}`)
|
|
445
|
+
.join("; ") +
|
|
446
|
+
(csvWarnings.length > 3 ? ` (+${csvWarnings.length - 3} more)` : ""),
|
|
447
|
+
evidence: {
|
|
448
|
+
warning_count: csvWarnings.length,
|
|
449
|
+
warnings: csvWarnings.slice(0, 5),
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
for (const insight of repeatAmountInsights(unmatchedBank, currency)) {
|
|
455
|
+
addUnique(insight);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const insight of anomalyInsights(balances)) {
|
|
459
|
+
addUnique(insight);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return insights.slice(0, 5);
|
|
419
463
|
}
|
|
420
464
|
|
|
421
465
|
// --- Main Analysis Function ---
|
|
@@ -435,147 +479,163 @@ function detectInsights(
|
|
|
435
479
|
* @param csvOptions - Optional CSV parsing options (manual overrides)
|
|
436
480
|
*/
|
|
437
481
|
export function analyzeReconciliation(
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
482
|
+
csvContentOrParsed: string | CSVParseResult,
|
|
483
|
+
_csvFilePath: string | undefined,
|
|
484
|
+
ynabTransactions: ynab.TransactionDetail[],
|
|
485
|
+
statementBalance: number,
|
|
486
|
+
config: MatchingConfig = DEFAULT_CONFIG,
|
|
487
|
+
currency = "USD",
|
|
488
|
+
accountId?: string,
|
|
489
|
+
budgetId?: string,
|
|
490
|
+
invertBankAmounts = false,
|
|
491
|
+
csvOptions?: ParseCSVOptions,
|
|
492
|
+
accountSnapshot?: {
|
|
493
|
+
balance?: number;
|
|
494
|
+
cleared_balance?: number;
|
|
495
|
+
uncleared_balance?: number;
|
|
496
|
+
},
|
|
449
497
|
): ReconciliationAnalysis {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
498
|
+
// Step 1: Parse bank CSV using new Parser (or use provided result)
|
|
499
|
+
let parseResult: CSVParseResult;
|
|
500
|
+
|
|
501
|
+
if (typeof csvContentOrParsed === "string") {
|
|
502
|
+
parseResult = parseCSV(csvContentOrParsed, {
|
|
503
|
+
...csvOptions,
|
|
504
|
+
invertAmounts: invertBankAmounts,
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
parseResult = csvContentOrParsed;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const newBankTransactions = parseResult.transactions;
|
|
511
|
+
const csvParseErrors = parseResult.errors;
|
|
512
|
+
const csvParseWarnings = parseResult.warnings;
|
|
513
|
+
|
|
514
|
+
// Step 2: Normalize YNAB transactions
|
|
515
|
+
const allYNABTransactions = normalizeYNABTransactions(ynabTransactions);
|
|
516
|
+
|
|
517
|
+
// Step 2.5: Filter YNAB transactions by CSV date range
|
|
518
|
+
// Only compare transactions within the statement period (with tolerance buffer)
|
|
519
|
+
const csvDateRange = calculateDateRange(newBankTransactions);
|
|
520
|
+
let ynabInRange: YNABTransaction[];
|
|
521
|
+
let ynabOutsideRange: YNABTransaction[];
|
|
522
|
+
|
|
523
|
+
if (csvDateRange) {
|
|
524
|
+
const dateToleranceDays = config.dateToleranceDays ?? 7;
|
|
525
|
+
const filtered = filterByDateRange(
|
|
526
|
+
allYNABTransactions,
|
|
527
|
+
csvDateRange,
|
|
528
|
+
dateToleranceDays,
|
|
529
|
+
);
|
|
530
|
+
ynabInRange = filtered.inRange;
|
|
531
|
+
ynabOutsideRange = filtered.outsideRange;
|
|
532
|
+
} else {
|
|
533
|
+
// No valid date range from CSV, use all transactions
|
|
534
|
+
ynabInRange = allYNABTransactions;
|
|
535
|
+
ynabOutsideRange = [];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Step 3: Run matching algorithm ONLY on YNAB transactions within date range
|
|
539
|
+
// Use normalizeConfig to convert legacy config to V2 format with defaults
|
|
540
|
+
const normalizedConfig = normalizeConfig(config);
|
|
541
|
+
|
|
542
|
+
const newMatches = findMatches(
|
|
543
|
+
newBankTransactions,
|
|
544
|
+
ynabInRange,
|
|
545
|
+
normalizedConfig,
|
|
546
|
+
);
|
|
547
|
+
const matches: TransactionMatch[] = newMatches.map(mapToTransactionMatch);
|
|
548
|
+
|
|
549
|
+
// Categorize
|
|
550
|
+
const autoMatches = matches.filter((m) => m.confidence === "high");
|
|
551
|
+
|
|
552
|
+
// Build set of YNAB transaction IDs that are already auto-matched
|
|
553
|
+
const autoMatchedYnabIds = new Set<string>();
|
|
554
|
+
for (const match of autoMatches) {
|
|
555
|
+
if (match.ynabTransaction) {
|
|
556
|
+
autoMatchedYnabIds.add(match.ynabTransaction.id);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Only suggest matches for YNAB transactions NOT already auto-matched
|
|
561
|
+
const suggestedMatches = matches.filter(
|
|
562
|
+
(m) =>
|
|
563
|
+
m.confidence === "medium" &&
|
|
564
|
+
(!m.ynabTransaction || !autoMatchedYnabIds.has(m.ynabTransaction.id)),
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
const unmatchedBankMatches = matches.filter(
|
|
568
|
+
(m) => m.confidence === "low" || m.confidence === "none",
|
|
569
|
+
);
|
|
570
|
+
const unmatchedBank = unmatchedBankMatches.map((m) => m.bankTransaction);
|
|
571
|
+
|
|
572
|
+
// Find unmatched YNAB (only from in-range transactions)
|
|
573
|
+
const matchedYnabIds = new Set<string>();
|
|
574
|
+
for (const match of matches) {
|
|
575
|
+
if (match.ynabTransaction) {
|
|
576
|
+
matchedYnabIds.add(match.ynabTransaction.id);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const unmatchedYNAB = ynabInRange.filter((t) => !matchedYnabIds.has(t.id));
|
|
580
|
+
|
|
581
|
+
// Step 6: Calculate balances (use ALL YNAB transactions for balance calculation)
|
|
582
|
+
const balances = calculateBalances(
|
|
583
|
+
allYNABTransactions,
|
|
584
|
+
statementBalance,
|
|
585
|
+
currency,
|
|
586
|
+
accountSnapshot,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Step 7: Generate summary (with date range info)
|
|
590
|
+
const summary = generateSummary(
|
|
591
|
+
matches.map((m) => m.bankTransaction),
|
|
592
|
+
ynabInRange,
|
|
593
|
+
ynabOutsideRange,
|
|
594
|
+
autoMatches,
|
|
595
|
+
suggestedMatches,
|
|
596
|
+
unmatchedBank,
|
|
597
|
+
unmatchedYNAB,
|
|
598
|
+
balances,
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
// Step 8: Generate next steps
|
|
602
|
+
const nextSteps = generateNextSteps(summary);
|
|
603
|
+
|
|
604
|
+
// Step 9: Detect insights (including any CSV parsing issues)
|
|
605
|
+
const insights = detectInsights(
|
|
606
|
+
unmatchedBank,
|
|
607
|
+
summary,
|
|
608
|
+
balances,
|
|
609
|
+
currency,
|
|
610
|
+
csvParseErrors,
|
|
611
|
+
csvParseWarnings,
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
// Step 10: Build the analysis result
|
|
615
|
+
const analysis: ReconciliationAnalysis = {
|
|
616
|
+
success: true,
|
|
617
|
+
phase: "analysis",
|
|
618
|
+
summary,
|
|
619
|
+
auto_matches: autoMatches,
|
|
620
|
+
suggested_matches: suggestedMatches,
|
|
621
|
+
unmatched_bank: unmatchedBank,
|
|
622
|
+
unmatched_ynab: unmatchedYNAB,
|
|
623
|
+
ynab_outside_date_range: ynabOutsideRange,
|
|
624
|
+
balance_info: balances,
|
|
625
|
+
next_steps: nextSteps,
|
|
626
|
+
insights,
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
// Step 11: Generate recommendations
|
|
630
|
+
if (accountId && budgetId) {
|
|
631
|
+
const recommendations = generateRecommendations({
|
|
632
|
+
account_id: accountId,
|
|
633
|
+
budget_id: budgetId,
|
|
634
|
+
analysis,
|
|
635
|
+
matching_config: normalizedConfig,
|
|
636
|
+
});
|
|
637
|
+
analysis.recommendations = recommendations;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return analysis;
|
|
581
641
|
}
|