@dizzlkheinz/ynab-mcpb 0.18.3 → 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/CHANGELOG.md +17 -0
- 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 +21 -11
- package/dist/tools/reconciliation/analyzer.d.ts +4 -4
- package/dist/tools/reconciliation/analyzer.js +136 -57
- 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 +79 -54
- package/dist/tools/reconciliation/signDetector.d.ts +1 -1
- package/dist/tools/reconciliation/types.d.ts +19 -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 +309 -0
- package/dist/tools/transactionSchemas.js +235 -0
- package/dist/tools/transactionTools.d.ts +6 -302
- package/dist/tools/transactionTools.js +7 -2054
- package/dist/tools/transactionUtils.d.ts +31 -0
- package/dist/tools/transactionUtils.js +364 -0
- 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/docs/plans/2025-12-25-transaction-tools-refactor-design.md +211 -0
- package/docs/plans/2025-12-25-transaction-tools-refactor.md +905 -0
- package/esbuild.config.mjs +53 -50
- package/meta.json +12548 -12548
- package/package.json +98 -109
- package/scripts/analyze-bundle.mjs +33 -30
- package/scripts/create-pr-description.js +169 -120
- package/scripts/run-all-tests.js +205 -0
- 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 +1204 -0
- 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 +1016 -0
- 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 -246
- package/src/tools/reconciliation/CLAUDE.md +506 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +135 -112
- package/src/tools/reconciliation/__tests__/adapter.test.ts +249 -227
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +408 -335
- 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 -986
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +187 -146
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +583 -530
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +75 -71
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +70 -58
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +102 -88
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +58 -43
- 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 +582 -406
- 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 +349 -276
- package/src/tools/reconciliation/signDetector.ts +89 -83
- package/src/tools/reconciliation/types.ts +164 -153
- 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 +484 -0
- package/src/tools/transactionTools.ts +107 -2990
- package/src/tools/transactionUtils.ts +621 -0
- 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,320 +5,461 @@
|
|
|
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
|
|
|
37
|
+
/**
|
|
38
|
+
* Calculate the date range from bank transactions
|
|
39
|
+
* Returns { minDate, maxDate } as ISO date strings (YYYY-MM-DD)
|
|
40
|
+
*/
|
|
41
|
+
function calculateDateRange(bankTransactions: BankTransaction[]): {
|
|
42
|
+
minDate: string;
|
|
43
|
+
maxDate: string;
|
|
44
|
+
} | null {
|
|
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
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Filter YNAB transactions to only those within the given date range
|
|
72
|
+
* Returns { inRange, outsideRange } arrays
|
|
73
|
+
*
|
|
74
|
+
* @param dateToleranceDays - Buffer to add to the date range to account for bank posting delays
|
|
75
|
+
*/
|
|
76
|
+
function filterByDateRange(
|
|
77
|
+
ynabTransactions: YNABTransaction[],
|
|
78
|
+
dateRange: { minDate: string; maxDate: string },
|
|
79
|
+
dateToleranceDays = 7,
|
|
80
|
+
): { inRange: YNABTransaction[]; outsideRange: YNABTransaction[] } {
|
|
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 };
|
|
136
|
+
}
|
|
137
|
+
|
|
28
138
|
function mapToTransactionMatch(result: MatchResult): TransactionMatch {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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;
|
|
58
169
|
}
|
|
59
170
|
|
|
60
171
|
function calculateBalances(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
172
|
+
ynabTransactions: YNABTransaction[],
|
|
173
|
+
statementBalanceDecimal: number,
|
|
174
|
+
currency: string,
|
|
175
|
+
accountSnapshot?: {
|
|
176
|
+
balance?: number;
|
|
177
|
+
cleared_balance?: number;
|
|
178
|
+
uncleared_balance?: number;
|
|
179
|
+
},
|
|
65
180
|
): BalanceInfo {
|
|
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
|
-
|
|
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
|
+
};
|
|
96
213
|
}
|
|
97
214
|
|
|
98
215
|
function generateSummary(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
216
|
+
bankTransactions: BankTransaction[],
|
|
217
|
+
ynabTransactionsInRange: YNABTransaction[],
|
|
218
|
+
ynabTransactionsOutsideRange: YNABTransaction[],
|
|
219
|
+
autoMatches: TransactionMatch[],
|
|
220
|
+
suggestedMatches: TransactionMatch[],
|
|
221
|
+
unmatchedBank: BankTransaction[],
|
|
222
|
+
unmatchedYNAB: YNABTransaction[],
|
|
223
|
+
balances: BalanceInfo,
|
|
106
224
|
): ReconciliationSummary {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
+
};
|
|
144
271
|
}
|
|
145
272
|
|
|
146
273
|
function generateNextSteps(summary: ReconciliationSummary): string[] {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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;
|
|
172
307
|
}
|
|
173
308
|
|
|
174
|
-
function formatCurrency(amountMilli: number, currency
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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);
|
|
182
317
|
}
|
|
183
318
|
|
|
184
319
|
// --- Insight Generation ---
|
|
185
320
|
|
|
186
321
|
function repeatAmountInsights(
|
|
187
|
-
|
|
188
|
-
|
|
322
|
+
unmatchedBank: BankTransaction[],
|
|
323
|
+
currency = "USD",
|
|
189
324
|
): ReconciliationInsight[] {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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;
|
|
231
370
|
}
|
|
232
371
|
|
|
233
372
|
function anomalyInsights(balances: BalanceInfo): ReconciliationInsight[] {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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;
|
|
256
395
|
}
|
|
257
396
|
|
|
258
397
|
function detectInsights(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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 }[] = [],
|
|
265
404
|
): ReconciliationInsight[] {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
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);
|
|
322
463
|
}
|
|
323
464
|
|
|
324
465
|
// --- Main Analysis Function ---
|
|
@@ -338,128 +479,163 @@ function detectInsights(
|
|
|
338
479
|
* @param csvOptions - Optional CSV parsing options (manual overrides)
|
|
339
480
|
*/
|
|
340
481
|
export function analyzeReconciliation(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
+
},
|
|
352
497
|
): ReconciliationAnalysis {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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;
|
|
465
641
|
}
|