@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
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { YNABAPIError } from
|
|
2
|
-
import { toMilli, toMoneyValue
|
|
3
|
-
import {
|
|
1
|
+
import { YNABAPIError, YNABErrorCode } from "../../server/errorHandler.js";
|
|
2
|
+
import { addMilli, toMilli, toMoneyValue } from "../../utils/money.js";
|
|
3
|
+
import { correlateResults, generateCorrelationKey, toCorrelationPayload, } from "../transactionTools.js";
|
|
4
4
|
const MONEY_EPSILON_MILLI = 100;
|
|
5
5
|
const DEFAULT_TOLERANCE_CENTS = 1;
|
|
6
6
|
const CENTS_TO_MILLI = 10;
|
|
@@ -10,7 +10,7 @@ const BATCH_DELAY_MS = 200;
|
|
|
10
10
|
const MAX_MEMO_LENGTH = 500;
|
|
11
11
|
function chunkArray(array, size) {
|
|
12
12
|
if (size <= 0) {
|
|
13
|
-
throw new Error(
|
|
13
|
+
throw new Error("chunk size must be positive");
|
|
14
14
|
}
|
|
15
15
|
const chunks = [];
|
|
16
16
|
for (let i = 0; i < array.length; i += size) {
|
|
@@ -23,10 +23,10 @@ function sleep(ms) {
|
|
|
23
23
|
}
|
|
24
24
|
function truncateMemo(memo) {
|
|
25
25
|
if (!memo)
|
|
26
|
-
return
|
|
26
|
+
return "Auto-reconciled from bank statement";
|
|
27
27
|
if (memo.length <= MAX_MEMO_LENGTH)
|
|
28
28
|
return memo;
|
|
29
|
-
return memo.substring(0, MAX_MEMO_LENGTH - 3)
|
|
29
|
+
return `${memo.substring(0, MAX_MEMO_LENGTH - 3)}...`;
|
|
30
30
|
}
|
|
31
31
|
function parseISODate(dateStr) {
|
|
32
32
|
if (!dateStr)
|
|
@@ -46,8 +46,10 @@ function resolveStatementWindow(params, analysisDateRange) {
|
|
|
46
46
|
window.end = end;
|
|
47
47
|
return window;
|
|
48
48
|
}
|
|
49
|
-
if (analysisDateRange
|
|
50
|
-
const [rawStart, rawEnd] = analysisDateRange
|
|
49
|
+
if (analysisDateRange?.includes(" to ")) {
|
|
50
|
+
const [rawStart, rawEnd] = analysisDateRange
|
|
51
|
+
.split(" to ")
|
|
52
|
+
.map((part) => part.trim());
|
|
51
53
|
const parsedStart = parseISODate(rawStart);
|
|
52
54
|
const parsedEnd = parseISODate(rawEnd);
|
|
53
55
|
if (parsedStart || parsedEnd) {
|
|
@@ -106,7 +108,8 @@ export async function executeReconciliation(options) {
|
|
|
106
108
|
let accountSnapshotDirty = false;
|
|
107
109
|
const statementTargetMilli = resolveStatementBalanceMilli(analysis.balance_info, params.statement_balance);
|
|
108
110
|
let clearedDeltaMilli = addMilli(initialAccount.cleared_balance ?? 0, -statementTargetMilli);
|
|
109
|
-
const balanceToleranceMilli = Math.max(0, params.amount_tolerance_cents ?? DEFAULT_TOLERANCE_CENTS) *
|
|
111
|
+
const balanceToleranceMilli = Math.max(0, params.amount_tolerance_cents ?? DEFAULT_TOLERANCE_CENTS) *
|
|
112
|
+
CENTS_TO_MILLI;
|
|
110
113
|
let balanceAligned = false;
|
|
111
114
|
const applyClearedDelta = (delta) => {
|
|
112
115
|
if (delta === 0)
|
|
@@ -123,7 +126,7 @@ export async function executeReconciliation(options) {
|
|
|
123
126
|
const deltaDisplay = toMoneyValue(clearedDeltaMilli, currencyCode).value_display;
|
|
124
127
|
const toleranceDisplay = toMoneyValue(balanceToleranceMilli, currencyCode).value_display;
|
|
125
128
|
actions_taken.push({
|
|
126
|
-
type:
|
|
129
|
+
type: "balance_checkpoint",
|
|
127
130
|
transaction: null,
|
|
128
131
|
reason: `Cleared delta ${deltaDisplay} within ±${toleranceDisplay} after ${trigger} - halting newest-to-oldest pass`,
|
|
129
132
|
});
|
|
@@ -132,7 +135,7 @@ export async function executeReconciliation(options) {
|
|
|
132
135
|
}
|
|
133
136
|
return false;
|
|
134
137
|
};
|
|
135
|
-
recordAlignmentIfNeeded(
|
|
138
|
+
recordAlignmentIfNeeded("initial balance check", { log: false });
|
|
136
139
|
const orderedUnmatchedBank = params.auto_create_transactions
|
|
137
140
|
? sortByDateDescending(analysis.unmatched_bank)
|
|
138
141
|
: [];
|
|
@@ -151,7 +154,7 @@ export async function executeReconciliation(options) {
|
|
|
151
154
|
date: bankTxn.date,
|
|
152
155
|
payee_name: bankTxn.payee ?? undefined,
|
|
153
156
|
memo: truncateMemo(bankTxn.memo),
|
|
154
|
-
cleared:
|
|
157
|
+
cleared: "cleared",
|
|
155
158
|
approved: true,
|
|
156
159
|
};
|
|
157
160
|
const correlationKey = generateCorrelationKey(toCorrelationPayload(saveTransaction));
|
|
@@ -166,9 +169,9 @@ export async function executeReconciliation(options) {
|
|
|
166
169
|
const { entry, createdTxn, chunkIndex, prefix } = args;
|
|
167
170
|
summary.transactions_created += 1;
|
|
168
171
|
const action = {
|
|
169
|
-
type:
|
|
172
|
+
type: "create_transaction",
|
|
170
173
|
transaction: createdTxn,
|
|
171
|
-
reason: `${prefix ??
|
|
174
|
+
reason: `${prefix ?? "Created missing transaction"}: ${entry.bankTransaction.payee ?? "Unknown"} (${formatDisplay(entry.bankTransaction.amount, currencyCode)})`,
|
|
172
175
|
correlation_key: entry.correlationKey,
|
|
173
176
|
};
|
|
174
177
|
if (chunkIndex !== undefined) {
|
|
@@ -193,8 +196,8 @@ export async function executeReconciliation(options) {
|
|
|
193
196
|
entry,
|
|
194
197
|
createdTxn: createdTransaction,
|
|
195
198
|
prefix: options.fallbackError
|
|
196
|
-
?
|
|
197
|
-
:
|
|
199
|
+
? "Created missing transaction after bulk fallback"
|
|
200
|
+
: "Created missing transaction",
|
|
198
201
|
};
|
|
199
202
|
if (options.chunkIndex !== undefined) {
|
|
200
203
|
recordArgs.chunkIndex = options.chunkIndex;
|
|
@@ -205,8 +208,8 @@ export async function executeReconciliation(options) {
|
|
|
205
208
|
completedOperations += 1;
|
|
206
209
|
await reportProgress(`Created ${completedOperations} of ${totalOperations} transactions`);
|
|
207
210
|
const trigger = options.chunkIndex
|
|
208
|
-
? `creating ${entry.bankTransaction.payee ??
|
|
209
|
-
: `creating ${entry.bankTransaction.payee ??
|
|
211
|
+
? `creating ${entry.bankTransaction.payee ?? "missing transaction"} (chunk ${options.chunkIndex})`
|
|
212
|
+
: `creating ${entry.bankTransaction.payee ?? "missing transaction"}`;
|
|
210
213
|
recordAlignmentIfNeeded(trigger);
|
|
211
214
|
}
|
|
212
215
|
catch (error) {
|
|
@@ -214,14 +217,16 @@ export async function executeReconciliation(options) {
|
|
|
214
217
|
if (bulkOperationDetails) {
|
|
215
218
|
bulkOperationDetails.transaction_failures += 1;
|
|
216
219
|
}
|
|
217
|
-
const failureReason = ynabError.message ||
|
|
218
|
-
const statusSuffix = ynabError.status
|
|
220
|
+
const failureReason = ynabError.message || "Unknown error occurred";
|
|
221
|
+
const statusSuffix = ynabError.status
|
|
222
|
+
? ` (HTTP ${ynabError.status})`
|
|
223
|
+
: "";
|
|
219
224
|
const failureAction = {
|
|
220
|
-
type:
|
|
225
|
+
type: "create_transaction_failed",
|
|
221
226
|
transaction: entry.saveTransaction,
|
|
222
227
|
reason: options.fallbackError
|
|
223
|
-
? `Bulk fallback failed for ${entry.bankTransaction.payee ??
|
|
224
|
-
: `Failed to create transaction ${entry.bankTransaction.payee ??
|
|
228
|
+
? `Bulk fallback failed for ${entry.bankTransaction.payee ?? "Unknown"} (${failureReason}${statusSuffix})`
|
|
229
|
+
: `Failed to create transaction ${entry.bankTransaction.payee ?? "Unknown"} (${failureReason}${statusSuffix})`,
|
|
225
230
|
correlation_key: entry.correlationKey,
|
|
226
231
|
};
|
|
227
232
|
if (options.chunkIndex !== undefined) {
|
|
@@ -233,12 +238,17 @@ export async function executeReconciliation(options) {
|
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
}
|
|
236
|
-
if (bulkOperationDetails &&
|
|
241
|
+
if (bulkOperationDetails &&
|
|
242
|
+
options.fallbackError &&
|
|
243
|
+
sequentialAttempts > 0) {
|
|
237
244
|
bulkOperationDetails.sequential_attempts =
|
|
238
245
|
(bulkOperationDetails.sequential_attempts ?? 0) + sequentialAttempts;
|
|
239
246
|
}
|
|
240
247
|
};
|
|
241
248
|
const processBulkChunk = async (chunk, chunkIndex) => {
|
|
249
|
+
if (!bulkOperationDetails) {
|
|
250
|
+
throw new Error("Bulk operation details not initialized");
|
|
251
|
+
}
|
|
242
252
|
const bulkDetails = bulkOperationDetails;
|
|
243
253
|
const payload = chunk.map((entry) => entry.saveTransaction);
|
|
244
254
|
const response = await ynabAPI.transactions.createTransactions(budgetId, {
|
|
@@ -258,7 +268,7 @@ export async function executeReconciliation(options) {
|
|
|
258
268
|
const entry = chunk[result.request_index];
|
|
259
269
|
if (!entry)
|
|
260
270
|
continue;
|
|
261
|
-
if (result.status ===
|
|
271
|
+
if (result.status === "created") {
|
|
262
272
|
const createdTransaction = result.transaction_id
|
|
263
273
|
? (transactionMap.get(result.transaction_id) ?? null)
|
|
264
274
|
: null;
|
|
@@ -266,21 +276,21 @@ export async function executeReconciliation(options) {
|
|
|
266
276
|
entry,
|
|
267
277
|
createdTxn: createdTransaction,
|
|
268
278
|
chunkIndex,
|
|
269
|
-
prefix:
|
|
279
|
+
prefix: "Created missing transaction via bulk",
|
|
270
280
|
});
|
|
271
281
|
accountSnapshotDirty = true;
|
|
272
282
|
applyClearedDelta(entry.amountMilli);
|
|
273
|
-
recordAlignmentIfNeeded(`creating ${entry.bankTransaction.payee ??
|
|
283
|
+
recordAlignmentIfNeeded(`creating ${entry.bankTransaction.payee ?? "missing transaction"} via bulk chunk ${chunkIndex}`);
|
|
274
284
|
}
|
|
275
|
-
else if (result.status ===
|
|
285
|
+
else if (result.status === "duplicate") {
|
|
276
286
|
bulkDetails.duplicates_detected += 1;
|
|
277
287
|
actions_taken.push({
|
|
278
|
-
type:
|
|
288
|
+
type: "create_transaction_duplicate",
|
|
279
289
|
transaction: {
|
|
280
290
|
transaction_id: result.transaction_id ?? null,
|
|
281
291
|
import_id: entry.saveTransaction.import_id,
|
|
282
292
|
},
|
|
283
|
-
reason: `Duplicate import detected for ${entry.bankTransaction.payee ??
|
|
293
|
+
reason: `Duplicate import detected for ${entry.bankTransaction.payee ?? "Unknown"} (import_id ${entry.saveTransaction.import_id})`,
|
|
284
294
|
bulk_chunk_index: chunkIndex,
|
|
285
295
|
correlation_key: result.correlation_key,
|
|
286
296
|
duplicate: true,
|
|
@@ -289,9 +299,10 @@ export async function executeReconciliation(options) {
|
|
|
289
299
|
else {
|
|
290
300
|
bulkDetails.transaction_failures += 1;
|
|
291
301
|
actions_taken.push({
|
|
292
|
-
type:
|
|
302
|
+
type: "create_transaction_failed",
|
|
293
303
|
transaction: entry.saveTransaction,
|
|
294
|
-
reason: result.error ??
|
|
304
|
+
reason: result.error ??
|
|
305
|
+
`Bulk create failed for ${entry.bankTransaction.payee ?? "Unknown"}`,
|
|
295
306
|
bulk_chunk_index: chunkIndex,
|
|
296
307
|
correlation_key: result.correlation_key,
|
|
297
308
|
});
|
|
@@ -305,13 +316,13 @@ export async function executeReconciliation(options) {
|
|
|
305
316
|
const entry = buildPreparedEntry(bankTxn);
|
|
306
317
|
summary.transactions_created += 1;
|
|
307
318
|
actions_taken.push({
|
|
308
|
-
type:
|
|
319
|
+
type: "create_transaction",
|
|
309
320
|
transaction: entry.saveTransaction,
|
|
310
|
-
reason: `Would create missing transaction: ${bankTxn.payee ??
|
|
321
|
+
reason: `Would create missing transaction: ${bankTxn.payee ?? "Unknown"} (${formatDisplay(bankTxn.amount, currencyCode)})`,
|
|
311
322
|
correlation_key: entry.correlationKey,
|
|
312
323
|
});
|
|
313
324
|
applyClearedDelta(entry.amountMilli);
|
|
314
|
-
recordAlignmentIfNeeded(`creating ${bankTxn.payee ??
|
|
325
|
+
recordAlignmentIfNeeded(`creating ${bankTxn.payee ?? "missing transaction"}`);
|
|
315
326
|
}
|
|
316
327
|
}
|
|
317
328
|
else if (orderedUnmatchedBank.length >= 2) {
|
|
@@ -359,7 +370,7 @@ export async function executeReconciliation(options) {
|
|
|
359
370
|
}
|
|
360
371
|
catch (error) {
|
|
361
372
|
const ynabError = normalizeYnabError(error);
|
|
362
|
-
const failureReason = ynabError.message ||
|
|
373
|
+
const failureReason = ynabError.message || "unknown error";
|
|
363
374
|
bulkOperationDetails.bulk_chunk_failures += 1;
|
|
364
375
|
if (shouldPropagateYnabError(ynabError)) {
|
|
365
376
|
bulkOperationDetails.transaction_failures += chunk.length;
|
|
@@ -367,12 +378,15 @@ export async function executeReconciliation(options) {
|
|
|
367
378
|
}
|
|
368
379
|
bulkOperationDetails.sequential_fallbacks += 1;
|
|
369
380
|
actions_taken.push({
|
|
370
|
-
type:
|
|
381
|
+
type: "bulk_create_fallback",
|
|
371
382
|
transaction: null,
|
|
372
|
-
reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${ynabError.status ? ` (HTTP ${ynabError.status})` :
|
|
383
|
+
reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${ynabError.status ? ` (HTTP ${ynabError.status})` : ""}) - falling back to sequential creation`,
|
|
373
384
|
bulk_chunk_index: chunkIndex,
|
|
374
385
|
});
|
|
375
|
-
await processSequentialEntries(chunk, {
|
|
386
|
+
await processSequentialEntries(chunk, {
|
|
387
|
+
chunkIndex,
|
|
388
|
+
fallbackError: ynabError,
|
|
389
|
+
});
|
|
376
390
|
}
|
|
377
391
|
}
|
|
378
392
|
}
|
|
@@ -402,24 +416,26 @@ export async function executeReconciliation(options) {
|
|
|
402
416
|
updatePayload.date = match.bankTransaction.date;
|
|
403
417
|
}
|
|
404
418
|
if (flags.needsClearedUpdate) {
|
|
405
|
-
updatePayload.cleared =
|
|
419
|
+
updatePayload.cleared = "cleared";
|
|
406
420
|
}
|
|
407
421
|
if (params.dry_run) {
|
|
408
422
|
summary.transactions_updated += 1;
|
|
409
423
|
if (flags.needsDateUpdate)
|
|
410
424
|
summary.dates_adjusted += 1;
|
|
411
425
|
actions_taken.push({
|
|
412
|
-
type:
|
|
426
|
+
type: "update_transaction",
|
|
413
427
|
transaction: {
|
|
414
428
|
transaction_id: match.ynabTransaction.id,
|
|
415
|
-
new_date: flags.needsDateUpdate
|
|
416
|
-
|
|
429
|
+
new_date: flags.needsDateUpdate
|
|
430
|
+
? match.bankTransaction.date
|
|
431
|
+
: undefined,
|
|
432
|
+
cleared: flags.needsClearedUpdate ? "cleared" : undefined,
|
|
417
433
|
},
|
|
418
434
|
reason: `Would update transaction: ${updateReason(match, flags, currencyCode)}`,
|
|
419
435
|
});
|
|
420
436
|
if (flags.needsClearedUpdate) {
|
|
421
437
|
applyClearedDelta(match.ynabTransaction.amount);
|
|
422
|
-
if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id ??
|
|
438
|
+
if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id ?? "transaction"} (dry run)`)) {
|
|
423
439
|
break;
|
|
424
440
|
}
|
|
425
441
|
}
|
|
@@ -440,6 +456,8 @@ export async function executeReconciliation(options) {
|
|
|
440
456
|
const updateChunks = chunkArray(transactionsToUpdate, MAX_BULK_UPDATE_CHUNK);
|
|
441
457
|
for (let chunkIdx = 0; chunkIdx < updateChunks.length; chunkIdx++) {
|
|
442
458
|
const chunk = updateChunks[chunkIdx];
|
|
459
|
+
if (!chunk)
|
|
460
|
+
continue;
|
|
443
461
|
try {
|
|
444
462
|
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
445
463
|
transactions: chunk,
|
|
@@ -452,9 +470,9 @@ export async function executeReconciliation(options) {
|
|
|
452
470
|
? computeUpdateFlags(match, params)
|
|
453
471
|
: { needsClearedUpdate: false, needsDateUpdate: false };
|
|
454
472
|
actions_taken.push({
|
|
455
|
-
type:
|
|
473
|
+
type: "update_transaction",
|
|
456
474
|
transaction: updatedTransaction,
|
|
457
|
-
reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) :
|
|
475
|
+
reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) : "cleared"}`,
|
|
458
476
|
});
|
|
459
477
|
}
|
|
460
478
|
accountSnapshotDirty = true;
|
|
@@ -463,10 +481,12 @@ export async function executeReconciliation(options) {
|
|
|
463
481
|
}
|
|
464
482
|
catch (error) {
|
|
465
483
|
const ynabError = normalizeYnabError(error);
|
|
466
|
-
const failureReason = ynabError.message ||
|
|
467
|
-
const statusSuffix = ynabError.status
|
|
484
|
+
const failureReason = ynabError.message || "Unknown error occurred";
|
|
485
|
+
const statusSuffix = ynabError.status
|
|
486
|
+
? ` (HTTP ${ynabError.status})`
|
|
487
|
+
: "";
|
|
468
488
|
actions_taken.push({
|
|
469
|
-
type:
|
|
489
|
+
type: "batch_update_failed",
|
|
470
490
|
transaction: null,
|
|
471
491
|
reason: `Failed to update chunk ${chunkIdx + 1}/${updateChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
472
492
|
});
|
|
@@ -482,7 +502,7 @@ export async function executeReconciliation(options) {
|
|
|
482
502
|
}
|
|
483
503
|
const shouldRunSanityPass = params.auto_unclear_missing && !balanceAligned;
|
|
484
504
|
actions_taken.push({
|
|
485
|
-
type:
|
|
505
|
+
type: "diagnostic_step3_entry",
|
|
486
506
|
transaction: null,
|
|
487
507
|
reason: `STEP 3 diagnostics: auto_unclear_missing=${params.auto_unclear_missing}, balanceAligned=${balanceAligned}, shouldRunSanityPass=${shouldRunSanityPass}, orderedUnmatchedYNAB.length=${orderedUnmatchedYNAB.length}`,
|
|
488
508
|
});
|
|
@@ -492,26 +512,28 @@ export async function executeReconciliation(options) {
|
|
|
492
512
|
date: t.date,
|
|
493
513
|
cleared: t.cleared,
|
|
494
514
|
amount: formatDisplay(t.amount, currencyCode),
|
|
495
|
-
payee: t.payee ??
|
|
515
|
+
payee: t.payee ?? "Unknown",
|
|
496
516
|
}));
|
|
497
517
|
actions_taken.push({
|
|
498
|
-
type:
|
|
499
|
-
transaction: {
|
|
518
|
+
type: "diagnostic_unmatched_ynab",
|
|
519
|
+
transaction: {
|
|
520
|
+
unmatched_transactions: unmatchedDetails,
|
|
521
|
+
},
|
|
500
522
|
reason: `First ${Math.min(10, orderedUnmatchedYNAB.length)} unmatched YNAB transactions (cleared status and amounts)`,
|
|
501
523
|
});
|
|
502
524
|
}
|
|
503
525
|
if (shouldRunSanityPass) {
|
|
504
526
|
const transactionsToUnclear = [];
|
|
505
527
|
for (const ynabTxn of orderedUnmatchedYNAB) {
|
|
506
|
-
if (ynabTxn.cleared !==
|
|
528
|
+
if (ynabTxn.cleared !== "cleared")
|
|
507
529
|
continue;
|
|
508
530
|
if (balanceAligned)
|
|
509
531
|
break;
|
|
510
532
|
if (params.dry_run) {
|
|
511
533
|
summary.transactions_updated += 1;
|
|
512
534
|
actions_taken.push({
|
|
513
|
-
type:
|
|
514
|
-
transaction: { transaction_id: ynabTxn.id, cleared:
|
|
535
|
+
type: "update_transaction",
|
|
536
|
+
transaction: { transaction_id: ynabTxn.id, cleared: "uncleared" },
|
|
515
537
|
reason: `Would mark transaction ${ynabTxn.id} as uncleared - not present on statement`,
|
|
516
538
|
});
|
|
517
539
|
applyClearedDelta(-ynabTxn.amount);
|
|
@@ -522,7 +544,7 @@ export async function executeReconciliation(options) {
|
|
|
522
544
|
else {
|
|
523
545
|
transactionsToUnclear.push({
|
|
524
546
|
id: ynabTxn.id,
|
|
525
|
-
cleared:
|
|
547
|
+
cleared: "uncleared",
|
|
526
548
|
});
|
|
527
549
|
applyClearedDelta(-ynabTxn.amount);
|
|
528
550
|
if (recordAlignmentIfNeeded(`unclearing ${ynabTxn.id}`)) {
|
|
@@ -534,6 +556,8 @@ export async function executeReconciliation(options) {
|
|
|
534
556
|
const unclearChunks = chunkArray(transactionsToUnclear, MAX_BULK_UPDATE_CHUNK);
|
|
535
557
|
for (let chunkIdx = 0; chunkIdx < unclearChunks.length; chunkIdx++) {
|
|
536
558
|
const chunk = unclearChunks[chunkIdx];
|
|
559
|
+
if (!chunk)
|
|
560
|
+
continue;
|
|
537
561
|
try {
|
|
538
562
|
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
539
563
|
transactions: chunk,
|
|
@@ -542,7 +566,7 @@ export async function executeReconciliation(options) {
|
|
|
542
566
|
summary.transactions_updated += updatedTransactions.length;
|
|
543
567
|
for (const updatedTransaction of updatedTransactions) {
|
|
544
568
|
actions_taken.push({
|
|
545
|
-
type:
|
|
569
|
+
type: "update_transaction",
|
|
546
570
|
transaction: updatedTransaction,
|
|
547
571
|
reason: `Marked transaction ${updatedTransaction.id} as uncleared - not found on statement`,
|
|
548
572
|
});
|
|
@@ -553,10 +577,12 @@ export async function executeReconciliation(options) {
|
|
|
553
577
|
}
|
|
554
578
|
catch (error) {
|
|
555
579
|
const ynabError = normalizeYnabError(error);
|
|
556
|
-
const failureReason = ynabError.message ||
|
|
557
|
-
const statusSuffix = ynabError.status
|
|
580
|
+
const failureReason = ynabError.message || "Unknown error occurred";
|
|
581
|
+
const statusSuffix = ynabError.status
|
|
582
|
+
? ` (HTTP ${ynabError.status})`
|
|
583
|
+
: "";
|
|
558
584
|
actions_taken.push({
|
|
559
|
-
type:
|
|
585
|
+
type: "batch_unclear_failed",
|
|
560
586
|
transaction: null,
|
|
561
587
|
reason: `Failed to unclear chunk ${chunkIdx + 1}/${unclearChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
562
588
|
});
|
|
@@ -575,17 +601,19 @@ export async function executeReconciliation(options) {
|
|
|
575
601
|
for (const match of orderedAutoMatches) {
|
|
576
602
|
if (!match.ynabTransaction)
|
|
577
603
|
continue;
|
|
578
|
-
if (match.ynabTransaction.cleared ===
|
|
604
|
+
if (match.ynabTransaction.cleared === "reconciled")
|
|
579
605
|
continue;
|
|
580
606
|
transactionsToReconcile.push({
|
|
581
607
|
id: match.ynabTransaction.id,
|
|
582
|
-
cleared:
|
|
608
|
+
cleared: "reconciled",
|
|
583
609
|
});
|
|
584
610
|
}
|
|
585
611
|
if (transactionsToReconcile.length > 0) {
|
|
586
612
|
const reconcileChunks = chunkArray(transactionsToReconcile, MAX_BULK_UPDATE_CHUNK);
|
|
587
613
|
for (let chunkIdx = 0; chunkIdx < reconcileChunks.length; chunkIdx++) {
|
|
588
614
|
const chunk = reconcileChunks[chunkIdx];
|
|
615
|
+
if (!chunk)
|
|
616
|
+
continue;
|
|
589
617
|
try {
|
|
590
618
|
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
591
619
|
transactions: chunk,
|
|
@@ -595,19 +623,21 @@ export async function executeReconciliation(options) {
|
|
|
595
623
|
for (const reconciledTransaction of reconciledTransactions) {
|
|
596
624
|
const match = orderedAutoMatches.find((m) => m.ynabTransaction?.id === reconciledTransaction.id);
|
|
597
625
|
actions_taken.push({
|
|
598
|
-
type:
|
|
626
|
+
type: "update_transaction",
|
|
599
627
|
transaction: reconciledTransaction,
|
|
600
|
-
reason: `Marked as reconciled: ${match?.bankTransaction.payee ??
|
|
628
|
+
reason: `Marked as reconciled: ${match?.bankTransaction.payee ?? "transaction"} (${formatDisplay(reconciledTransaction.amount, currencyCode)})`,
|
|
601
629
|
});
|
|
602
630
|
}
|
|
603
631
|
accountSnapshotDirty = true;
|
|
604
632
|
}
|
|
605
633
|
catch (error) {
|
|
606
634
|
const ynabError = normalizeYnabError(error);
|
|
607
|
-
const failureReason = ynabError.message ||
|
|
608
|
-
const statusSuffix = ynabError.status
|
|
635
|
+
const failureReason = ynabError.message || "Unknown error occurred";
|
|
636
|
+
const statusSuffix = ynabError.status
|
|
637
|
+
? ` (HTTP ${ynabError.status})`
|
|
638
|
+
: "";
|
|
609
639
|
actions_taken.push({
|
|
610
|
-
type:
|
|
640
|
+
type: "batch_reconcile_failed",
|
|
611
641
|
transaction: null,
|
|
612
642
|
reason: `Failed to reconcile chunk ${chunkIdx + 1}/${reconcileChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
613
643
|
});
|
|
@@ -620,7 +650,7 @@ export async function executeReconciliation(options) {
|
|
|
620
650
|
}
|
|
621
651
|
}
|
|
622
652
|
actions_taken.push({
|
|
623
|
-
type:
|
|
653
|
+
type: "reconciliation_complete",
|
|
624
654
|
transaction: null,
|
|
625
655
|
reason: `Marked ${transactionsToReconcile.length} matched transaction(s) as reconciled - balance aligned within tolerance`,
|
|
626
656
|
});
|
|
@@ -640,7 +670,9 @@ export async function executeReconciliation(options) {
|
|
|
640
670
|
if (!params.dry_run && accountSnapshotDirty) {
|
|
641
671
|
afterAccount = await refreshAccountSnapshot(ynabAPI, budgetId, accountId);
|
|
642
672
|
}
|
|
643
|
-
const balanceChangeMilli = params.dry_run || !accountSnapshotDirty
|
|
673
|
+
const balanceChangeMilli = params.dry_run || !accountSnapshotDirty
|
|
674
|
+
? 0
|
|
675
|
+
: afterAccount.balance - initialAccount.balance;
|
|
644
676
|
const recommendations = buildRecommendations({
|
|
645
677
|
summary,
|
|
646
678
|
params,
|
|
@@ -661,7 +693,8 @@ export async function executeReconciliation(options) {
|
|
|
661
693
|
result.balance_reconciliation = balance_reconciliation;
|
|
662
694
|
}
|
|
663
695
|
if (bulkOperationDetails) {
|
|
664
|
-
bulkOperationDetails.failed_transactions =
|
|
696
|
+
bulkOperationDetails.failed_transactions =
|
|
697
|
+
bulkOperationDetails.transaction_failures;
|
|
665
698
|
result.bulk_operation_details = bulkOperationDetails;
|
|
666
699
|
}
|
|
667
700
|
return result;
|
|
@@ -669,9 +702,9 @@ export async function executeReconciliation(options) {
|
|
|
669
702
|
const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500, 503]);
|
|
670
703
|
export function normalizeYnabError(error) {
|
|
671
704
|
const parseStatus = (value) => {
|
|
672
|
-
if (typeof value ===
|
|
705
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
673
706
|
return value;
|
|
674
|
-
if (typeof value ===
|
|
707
|
+
if (typeof value === "string") {
|
|
675
708
|
const numeric = Number(value);
|
|
676
709
|
if (Number.isFinite(numeric))
|
|
677
710
|
return numeric;
|
|
@@ -682,10 +715,12 @@ export function normalizeYnabError(error) {
|
|
|
682
715
|
const status = parseStatus(error.status) ??
|
|
683
716
|
parseStatus(error.response?.status);
|
|
684
717
|
const detailSource = error.detail;
|
|
685
|
-
const detail = typeof detailSource ===
|
|
718
|
+
const detail = typeof detailSource === "string" && detailSource.trim().length > 0
|
|
719
|
+
? detailSource
|
|
720
|
+
: undefined;
|
|
686
721
|
const result = {
|
|
687
722
|
name: error.name,
|
|
688
|
-
message: error.message ||
|
|
723
|
+
message: error.message || "Unknown error occurred",
|
|
689
724
|
};
|
|
690
725
|
if (status !== undefined)
|
|
691
726
|
result.status = status;
|
|
@@ -693,18 +728,21 @@ export function normalizeYnabError(error) {
|
|
|
693
728
|
result.detail = detail;
|
|
694
729
|
return result;
|
|
695
730
|
}
|
|
696
|
-
if (error && typeof error ===
|
|
731
|
+
if (error && typeof error === "object") {
|
|
697
732
|
const errObj = error.error ?? error;
|
|
698
|
-
const status = parseStatus(errObj.id ??
|
|
733
|
+
const status = parseStatus(errObj.id ??
|
|
734
|
+
errObj.status);
|
|
699
735
|
const detailCandidate = errObj.detail ??
|
|
700
736
|
errObj.message ??
|
|
701
737
|
errObj.name;
|
|
702
|
-
const detail = typeof detailCandidate ===
|
|
738
|
+
const detail = typeof detailCandidate === "string" && detailCandidate.trim().length > 0
|
|
703
739
|
? detailCandidate
|
|
704
740
|
: undefined;
|
|
705
741
|
const message = detail ??
|
|
706
|
-
(typeof errObj ===
|
|
707
|
-
|
|
742
|
+
(typeof errObj === "string" && errObj.trim().length > 0
|
|
743
|
+
? errObj
|
|
744
|
+
: "Unknown error occurred");
|
|
745
|
+
const name = typeof errObj.name === "string"
|
|
708
746
|
? errObj.name
|
|
709
747
|
: undefined;
|
|
710
748
|
const result = { message };
|
|
@@ -716,27 +754,27 @@ export function normalizeYnabError(error) {
|
|
|
716
754
|
result.detail = detail;
|
|
717
755
|
return result;
|
|
718
756
|
}
|
|
719
|
-
if (typeof error ===
|
|
757
|
+
if (typeof error === "string") {
|
|
720
758
|
return { message: error };
|
|
721
759
|
}
|
|
722
|
-
return { message:
|
|
760
|
+
return { message: "Unknown error occurred" };
|
|
723
761
|
}
|
|
724
762
|
export function shouldPropagateYnabError(error) {
|
|
725
|
-
return error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status);
|
|
763
|
+
return (error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status));
|
|
726
764
|
}
|
|
727
765
|
function attachStatusToError(error, originalError) {
|
|
728
|
-
const message = error.message ||
|
|
729
|
-
const isKnownCode = error.status ===
|
|
730
|
-
error.status ===
|
|
731
|
-
error.status ===
|
|
732
|
-
error.status ===
|
|
733
|
-
error.status ===
|
|
734
|
-
error.status ===
|
|
766
|
+
const message = error.message || "YNAB API error";
|
|
767
|
+
const isKnownCode = error.status === YNABErrorCode.BAD_REQUEST ||
|
|
768
|
+
error.status === YNABErrorCode.UNAUTHORIZED ||
|
|
769
|
+
error.status === YNABErrorCode.FORBIDDEN ||
|
|
770
|
+
error.status === YNABErrorCode.NOT_FOUND ||
|
|
771
|
+
error.status === YNABErrorCode.TOO_MANY_REQUESTS ||
|
|
772
|
+
error.status === YNABErrorCode.INTERNAL_SERVER_ERROR;
|
|
735
773
|
if (isKnownCode) {
|
|
736
774
|
return new YNABAPIError(error.status, message, originalError);
|
|
737
775
|
}
|
|
738
|
-
const statusFragment = error.status ? ` (HTTP ${error.status})` :
|
|
739
|
-
const detailFragment = error.detail && !message.includes(error.detail) ? ` (${error.detail})` :
|
|
776
|
+
const statusFragment = error.status ? ` (HTTP ${error.status})` : "";
|
|
777
|
+
const detailFragment = error.detail && !message.includes(error.detail) ? ` (${error.detail})` : "";
|
|
740
778
|
const err = new Error(`${message}${statusFragment}${detailFragment}`);
|
|
741
779
|
if (error.status !== undefined) {
|
|
742
780
|
err.status = error.status;
|
|
@@ -755,26 +793,26 @@ function computeUpdateFlags(match, params) {
|
|
|
755
793
|
if (!ynabTxn) {
|
|
756
794
|
return { needsClearedUpdate: false, needsDateUpdate: false };
|
|
757
795
|
}
|
|
758
|
-
const needsClearedUpdate = Boolean(params.auto_update_cleared_status && ynabTxn.cleared !==
|
|
796
|
+
const needsClearedUpdate = Boolean(params.auto_update_cleared_status && ynabTxn.cleared !== "cleared");
|
|
759
797
|
const needsDateUpdate = Boolean(params.auto_adjust_dates && ynabTxn.date !== bankTxn.date);
|
|
760
798
|
return { needsClearedUpdate, needsDateUpdate };
|
|
761
799
|
}
|
|
762
800
|
function updateReason(match, flags, _currency) {
|
|
763
801
|
const parts = [];
|
|
764
802
|
if (flags.needsClearedUpdate) {
|
|
765
|
-
parts.push(
|
|
803
|
+
parts.push("marked as cleared");
|
|
766
804
|
}
|
|
767
805
|
if (flags.needsDateUpdate) {
|
|
768
806
|
parts.push(`date adjusted to ${match.bankTransaction.date}`);
|
|
769
807
|
}
|
|
770
|
-
return parts.join(
|
|
808
|
+
return parts.join(", ");
|
|
771
809
|
}
|
|
772
810
|
async function buildBalanceReconciliation(args) {
|
|
773
811
|
const { ynabAPI, budgetId, accountId, statementDate, statementBalance } = args;
|
|
774
812
|
const ynabMilli = await clearedBalanceAsOf(ynabAPI, budgetId, accountId, statementDate);
|
|
775
813
|
const bankMilli = toMilli(statementBalance);
|
|
776
814
|
const discrepancy = bankMilli - ynabMilli;
|
|
777
|
-
const status = discrepancy === 0 ?
|
|
815
|
+
const status = discrepancy === 0 ? "PERFECTLY_RECONCILED" : "DISCREPANCY_FOUND";
|
|
778
816
|
const precision_calculations = {
|
|
779
817
|
bank_statement_balance_milliunits: bankMilli,
|
|
780
818
|
ynab_calculated_balance_milliunits: ynabMilli,
|
|
@@ -800,7 +838,7 @@ async function buildBalanceReconciliation(args) {
|
|
|
800
838
|
async function clearedBalanceAsOf(api, budgetId, accountId, dateISO) {
|
|
801
839
|
const response = await api.transactions.getTransactionsByAccount(budgetId, accountId);
|
|
802
840
|
const asOf = new Date(dateISO);
|
|
803
|
-
const cleared = response.data.transactions.filter((txn) => txn.cleared ===
|
|
841
|
+
const cleared = response.data.transactions.filter((txn) => txn.cleared === "cleared" && new Date(txn.date) <= asOf);
|
|
804
842
|
const sum = cleared.reduce((acc, txn) => addMilli(acc, txn.amount ?? 0), 0);
|
|
805
843
|
return sum;
|
|
806
844
|
}
|
|
@@ -821,13 +859,13 @@ function buildLikelyCauses(discrepancyMilli) {
|
|
|
821
859
|
const abs = Math.abs(discrepancyMilli);
|
|
822
860
|
if (abs % 1000 === 0 || abs % 500 === 0) {
|
|
823
861
|
causes.push({
|
|
824
|
-
cause_type:
|
|
825
|
-
description:
|
|
862
|
+
cause_type: "bank_fee",
|
|
863
|
+
description: "Round amount suggests a bank fee or interest adjustment.",
|
|
826
864
|
confidence: 0.8,
|
|
827
865
|
amount_milliunits: discrepancyMilli,
|
|
828
866
|
suggested_resolution: discrepancyMilli < 0
|
|
829
|
-
?
|
|
830
|
-
:
|
|
867
|
+
? "Create bank fee transaction and mark cleared"
|
|
868
|
+
: "Record interest income",
|
|
831
869
|
evidence: [],
|
|
832
870
|
});
|
|
833
871
|
}
|
|
@@ -835,7 +873,7 @@ function buildLikelyCauses(discrepancyMilli) {
|
|
|
835
873
|
? {
|
|
836
874
|
confidence_level: Math.max(...causes.map((cause) => cause.confidence)),
|
|
837
875
|
likely_causes: causes,
|
|
838
|
-
risk_assessment:
|
|
876
|
+
risk_assessment: "LOW",
|
|
839
877
|
}
|
|
840
878
|
: undefined;
|
|
841
879
|
}
|
|
@@ -849,13 +887,13 @@ function buildRecommendations(args) {
|
|
|
849
887
|
recommendations.push(`Consider enabling auto_create_transactions to automatically create ${analysis.summary.unmatched_bank} missing transaction(s)`);
|
|
850
888
|
}
|
|
851
889
|
if (!params.auto_adjust_dates && analysis.auto_matches.length > 0) {
|
|
852
|
-
recommendations.push(
|
|
890
|
+
recommendations.push("Consider enabling auto_adjust_dates to align YNAB dates with bank statement dates");
|
|
853
891
|
}
|
|
854
892
|
if (analysis.summary.unmatched_ynab > 0) {
|
|
855
893
|
recommendations.push(`${analysis.summary.unmatched_ynab} transaction(s) exist in YNAB but not on the bank statement — review for duplicates or pending items`);
|
|
856
894
|
}
|
|
857
895
|
if (params.dry_run) {
|
|
858
|
-
recommendations.push(
|
|
896
|
+
recommendations.push("Dry run only — re-run with dry_run=false to apply these changes");
|
|
859
897
|
}
|
|
860
898
|
if (Math.abs(balanceChangeMilli) > MONEY_EPSILON_MILLI) {
|
|
861
899
|
recommendations.push(`Account balance changed by ${toMoneyValue(balanceChangeMilli, currencyCode).value_display} during reconciliation`);
|
|
@@ -863,7 +901,7 @@ function buildRecommendations(args) {
|
|
|
863
901
|
return recommendations;
|
|
864
902
|
}
|
|
865
903
|
function resolveStatementBalanceMilli(balanceInfo, provided) {
|
|
866
|
-
if (typeof provided ===
|
|
904
|
+
if (typeof provided === "number" && Number.isFinite(provided)) {
|
|
867
905
|
return toMilli(provided);
|
|
868
906
|
}
|
|
869
907
|
return (extractMoneyValue(balanceInfo?.target_statement) ??
|
|
@@ -871,13 +909,14 @@ function resolveStatementBalanceMilli(balanceInfo, provided) {
|
|
|
871
909
|
0);
|
|
872
910
|
}
|
|
873
911
|
function extractMoneyValue(value) {
|
|
874
|
-
if (typeof value ===
|
|
912
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
875
913
|
return toMilli(value);
|
|
876
914
|
}
|
|
877
915
|
if (value &&
|
|
878
|
-
typeof value ===
|
|
879
|
-
|
|
880
|
-
typeof value.value_milliunits ===
|
|
916
|
+
typeof value === "object" &&
|
|
917
|
+
"value_milliunits" in value &&
|
|
918
|
+
typeof value.value_milliunits ===
|
|
919
|
+
"number") {
|
|
881
920
|
return value.value_milliunits;
|
|
882
921
|
}
|
|
883
922
|
return undefined;
|