@dizzlkheinz/ynab-mcpb 0.12.1
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/.chunkhound.json +11 -0
- package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +8 -0
- package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +3 -0
- package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +3 -0
- package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +1 -0
- package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +12 -0
- package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +1 -0
- package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +1 -0
- package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +5 -0
- package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +1453 -0
- package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +8 -0
- package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +9 -0
- package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +1 -0
- package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +5 -0
- package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +81 -0
- package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +610 -0
- package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +1 -0
- package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +1 -0
- package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +1 -0
- package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +3 -0
- package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +8 -0
- package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +5 -0
- package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +1 -0
- package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +9 -0
- package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +1 -0
- package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +13 -0
- package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +14 -0
- package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +5 -0
- package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +5 -0
- package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +5 -0
- package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +3 -0
- package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +1 -0
- package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +1 -0
- package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +5 -0
- package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +5 -0
- package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +8 -0
- package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +8 -0
- package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +5 -0
- package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +1 -0
- package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +1 -0
- package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +1 -0
- package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +1 -0
- package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +1 -0
- package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +5 -0
- package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +1 -0
- package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +1 -0
- package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +1 -0
- package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +3 -0
- package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +8 -0
- package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +1 -0
- package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +1 -0
- package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +8 -0
- package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +3 -0
- package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +5 -0
- package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +1 -0
- package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +1 -0
- package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +5 -0
- package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +1 -0
- package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +14 -0
- package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +1 -0
- package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +5 -0
- package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +5 -0
- package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +11 -0
- package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +1 -0
- package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +8 -0
- package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +1 -0
- package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +3 -0
- package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +3 -0
- package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +8 -0
- package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +1 -0
- package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +5 -0
- package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +1 -0
- package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +1 -0
- package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +1 -0
- package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +1 -0
- package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +3 -0
- package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +8 -0
- package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +1 -0
- package/.dxtignore +57 -0
- package/.env.example +44 -0
- package/.gemini/settings.json +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
- package/.github/ISSUE_TEMPLATE/config.yml +5 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
- package/.github/ISSUE_TEMPLATE/release_checklist.md +31 -0
- package/.github/pull_request_template.md +41 -0
- package/.github/workflows/ci-tests.yml +41 -0
- package/.github/workflows/claude-code-review.yml +57 -0
- package/.github/workflows/claude.yml +50 -0
- package/.github/workflows/full-integration.yml +22 -0
- package/.github/workflows/pr-description-check.yml +88 -0
- package/.github/workflows/publish.yml +33 -0
- package/.github/workflows/release.yml +89 -0
- package/.mcpbignore +58 -0
- package/.prettierignore +10 -0
- package/.prettierrc.json +10 -0
- package/ADOS-2-Module-1-Complete-Manual.md +757 -0
- package/AGENTS.md +36 -0
- package/CHANGELOG.md +187 -0
- package/CLAUDE.md +414 -0
- package/CODEREVIEW_RESPONSE.md +128 -0
- package/LICENSE +17 -0
- package/NUL +1 -0
- package/README.md +222 -0
- package/SCHEMA_IMPROVEMENT_SUMMARY.md +120 -0
- package/TESTING_NOTES.md +217 -0
- package/WARP.md +245 -0
- package/accountactivity-merged.csv +149 -0
- package/bin/ynab-mcp-server.cjs +4 -0
- package/bin/ynab-mcp-server.js +8 -0
- package/bundle-analysis.html +13110 -0
- package/dist/bundle/index.cjs +124 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +85 -0
- package/dist/server/YNABMCPServer.d.ts +264 -0
- package/dist/server/YNABMCPServer.js +845 -0
- package/dist/server/budgetResolver.d.ts +15 -0
- package/dist/server/budgetResolver.js +99 -0
- package/dist/server/cacheManager.d.ts +74 -0
- package/dist/server/cacheManager.js +306 -0
- package/dist/server/config.d.ts +3 -0
- package/dist/server/config.js +19 -0
- package/dist/server/deltaCache.d.ts +61 -0
- package/dist/server/deltaCache.js +206 -0
- package/dist/server/deltaCache.merge.d.ts +9 -0
- package/dist/server/deltaCache.merge.js +111 -0
- package/dist/server/diagnostics.d.ts +90 -0
- package/dist/server/diagnostics.js +163 -0
- package/dist/server/errorHandler.d.ts +69 -0
- package/dist/server/errorHandler.js +524 -0
- package/dist/server/prompts.d.ts +31 -0
- package/dist/server/prompts.js +205 -0
- package/dist/server/rateLimiter.d.ts +27 -0
- package/dist/server/rateLimiter.js +82 -0
- package/dist/server/requestLogger.d.ts +62 -0
- package/dist/server/requestLogger.js +190 -0
- package/dist/server/resources.d.ts +39 -0
- package/dist/server/resources.js +85 -0
- package/dist/server/responseFormatter.d.ts +14 -0
- package/dist/server/responseFormatter.js +42 -0
- package/dist/server/securityMiddleware.d.ts +87 -0
- package/dist/server/securityMiddleware.js +117 -0
- package/dist/server/serverKnowledgeStore.d.ts +11 -0
- package/dist/server/serverKnowledgeStore.js +42 -0
- package/dist/server/toolRegistry.d.ts +85 -0
- package/dist/server/toolRegistry.js +272 -0
- package/dist/tools/__tests__/deltaTestUtils.d.ts +18 -0
- package/dist/tools/__tests__/deltaTestUtils.js +26 -0
- package/dist/tools/accountTools.d.ts +37 -0
- package/dist/tools/accountTools.js +175 -0
- package/dist/tools/budgetTools.d.ts +10 -0
- package/dist/tools/budgetTools.js +68 -0
- package/dist/tools/categoryTools.d.ts +27 -0
- package/dist/tools/categoryTools.js +232 -0
- package/dist/tools/compareTransactions/formatter.d.ts +71 -0
- package/dist/tools/compareTransactions/formatter.js +97 -0
- package/dist/tools/compareTransactions/index.d.ts +30 -0
- package/dist/tools/compareTransactions/index.js +160 -0
- package/dist/tools/compareTransactions/matcher.d.ts +12 -0
- package/dist/tools/compareTransactions/matcher.js +140 -0
- package/dist/tools/compareTransactions/parser.d.ts +14 -0
- package/dist/tools/compareTransactions/parser.js +430 -0
- package/dist/tools/compareTransactions/types.d.ts +27 -0
- package/dist/tools/compareTransactions/types.js +1 -0
- package/dist/tools/compareTransactions.d.ts +1 -0
- package/dist/tools/compareTransactions.js +1 -0
- package/dist/tools/deltaFetcher.d.ts +22 -0
- package/dist/tools/deltaFetcher.js +137 -0
- package/dist/tools/deltaSupport.d.ts +20 -0
- package/dist/tools/deltaSupport.js +176 -0
- package/dist/tools/exportTransactions.d.ts +17 -0
- package/dist/tools/exportTransactions.js +191 -0
- package/dist/tools/monthTools.d.ts +16 -0
- package/dist/tools/monthTools.js +107 -0
- package/dist/tools/payeeTools.d.ts +17 -0
- package/dist/tools/payeeTools.js +82 -0
- package/dist/tools/reconcileAdapter.d.ts +25 -0
- package/dist/tools/reconcileAdapter.js +167 -0
- package/dist/tools/reconciliation/analyzer.d.ts +3 -0
- package/dist/tools/reconciliation/analyzer.js +567 -0
- package/dist/tools/reconciliation/executor.d.ts +94 -0
- package/dist/tools/reconciliation/executor.js +611 -0
- package/dist/tools/reconciliation/index.d.ts +54 -0
- package/dist/tools/reconciliation/index.js +249 -0
- package/dist/tools/reconciliation/matcher.d.ts +3 -0
- package/dist/tools/reconciliation/matcher.js +160 -0
- package/dist/tools/reconciliation/payeeNormalizer.d.ts +6 -0
- package/dist/tools/reconciliation/payeeNormalizer.js +77 -0
- package/dist/tools/reconciliation/recommendationEngine.d.ts +2 -0
- package/dist/tools/reconciliation/recommendationEngine.js +273 -0
- package/dist/tools/reconciliation/reportFormatter.d.ts +13 -0
- package/dist/tools/reconciliation/reportFormatter.js +214 -0
- package/dist/tools/reconciliation/types.d.ts +172 -0
- package/dist/tools/reconciliation/types.js +7 -0
- package/dist/tools/schemas/outputs/accountOutputs.d.ts +58 -0
- package/dist/tools/schemas/outputs/accountOutputs.js +24 -0
- package/dist/tools/schemas/outputs/budgetOutputs.d.ts +48 -0
- package/dist/tools/schemas/outputs/budgetOutputs.js +15 -0
- package/dist/tools/schemas/outputs/categoryOutputs.d.ts +93 -0
- package/dist/tools/schemas/outputs/categoryOutputs.js +37 -0
- package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +269 -0
- package/dist/tools/schemas/outputs/comparisonOutputs.js +181 -0
- package/dist/tools/schemas/outputs/index.d.ts +14 -0
- package/dist/tools/schemas/outputs/index.js +14 -0
- package/dist/tools/schemas/outputs/monthOutputs.d.ts +122 -0
- package/dist/tools/schemas/outputs/monthOutputs.js +51 -0
- package/dist/tools/schemas/outputs/payeeOutputs.d.ts +34 -0
- package/dist/tools/schemas/outputs/payeeOutputs.js +16 -0
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +1275 -0
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +377 -0
- package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +717 -0
- package/dist/tools/schemas/outputs/transactionMutationOutputs.js +260 -0
- package/dist/tools/schemas/outputs/transactionOutputs.d.ts +98 -0
- package/dist/tools/schemas/outputs/transactionOutputs.js +49 -0
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +219 -0
- package/dist/tools/schemas/outputs/utilityOutputs.js +120 -0
- package/dist/tools/schemas/shared/commonOutputs.d.ts +24 -0
- package/dist/tools/schemas/shared/commonOutputs.js +27 -0
- package/dist/tools/toolCategories.d.ts +32 -0
- package/dist/tools/toolCategories.js +32 -0
- package/dist/tools/transactionTools.d.ts +315 -0
- package/dist/tools/transactionTools.js +1722 -0
- package/dist/tools/utilityTools.d.ts +10 -0
- package/dist/tools/utilityTools.js +56 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/index.js +16 -0
- package/dist/types/toolAnnotations.d.ts +7 -0
- package/dist/types/toolAnnotations.js +1 -0
- package/dist/utils/amountUtils.d.ts +3 -0
- package/dist/utils/amountUtils.js +10 -0
- package/dist/utils/dateUtils.d.ts +9 -0
- package/dist/utils/dateUtils.js +43 -0
- package/dist/utils/money.d.ts +21 -0
- package/dist/utils/money.js +51 -0
- package/docs/README.md +72 -0
- package/docs/assets/examples/reconciliation-with-recommendations.json +68 -0
- package/docs/assets/schemas/reconciliation-v2.json +338 -0
- package/docs/getting-started/CONFIGURATION.md +175 -0
- package/docs/getting-started/INSTALLATION.md +333 -0
- package/docs/getting-started/QUICKSTART.md +282 -0
- package/docs/guides/ARCHITECTURE.md +650 -0
- package/docs/guides/DEPLOYMENT.md +189 -0
- package/docs/guides/INTEGRATION_TESTING.md +730 -0
- package/docs/guides/TESTING.md +591 -0
- package/docs/reconciliation-flow.md +83 -0
- package/docs/reference/API.md +1450 -0
- package/docs/reference/EXAMPLES.md +946 -0
- package/docs/reference/TOOLS.md +348 -0
- package/docs/reference/TROUBLESHOOTING.md +481 -0
- package/esbuild.config.mjs +68 -0
- package/eslint.config.js +49 -0
- package/fix-types.sh +17 -0
- package/meta.json +12550 -0
- package/package.json +105 -0
- package/package.json.tmp +105 -0
- package/scripts/analyze-bundle.mjs +41 -0
- package/scripts/create-pr-description.js +203 -0
- package/scripts/generate-mcpb.ps1 +96 -0
- package/scripts/run-domain-integration-tests.js +33 -0
- package/scripts/run-generate-mcpb.js +29 -0
- package/scripts/run-throttled-integration-tests.js +116 -0
- package/scripts/test-delta-params.mjs +140 -0
- package/scripts/test-recommendations.ts +53 -0
- package/scripts/tmpTransaction.ts +48 -0
- package/scripts/validate-env.js +122 -0
- package/scripts/verify-build.js +105 -0
- package/scripts/watch-and-restart.ps1 +50 -0
- package/src/__tests__/comprehensive.integration.test.ts +1196 -0
- package/src/__tests__/delta.performance.test.ts +80 -0
- package/src/__tests__/performance.test.ts +725 -0
- package/src/__tests__/setup.ts +449 -0
- package/src/__tests__/testRunner.ts +444 -0
- package/src/__tests__/testUtils.ts +563 -0
- package/src/__tests__/workflows.e2e.test.ts +1675 -0
- package/src/index.ts +124 -0
- package/src/server/.gitkeep +1 -0
- package/src/server/YNABMCPServer.ts +1188 -0
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +903 -0
- package/src/server/__tests__/YNABMCPServer.test.ts +894 -0
- package/src/server/__tests__/budgetResolver.test.ts +425 -0
- package/src/server/__tests__/cacheManager.test.ts +880 -0
- package/src/server/__tests__/config.test.ts +166 -0
- package/src/server/__tests__/deltaCache.merge.test.ts +724 -0
- package/src/server/__tests__/deltaCache.swr.test.ts +168 -0
- package/src/server/__tests__/deltaCache.test.ts +774 -0
- package/src/server/__tests__/diagnostics.test.ts +823 -0
- package/src/server/__tests__/errorHandler.integration.test.ts +466 -0
- package/src/server/__tests__/errorHandler.test.ts +416 -0
- package/src/server/__tests__/prompts.test.ts +354 -0
- package/src/server/__tests__/rateLimiter.test.ts +314 -0
- package/src/server/__tests__/requestLogger.test.ts +408 -0
- package/src/server/__tests__/resources.test.ts +299 -0
- package/src/server/__tests__/security.integration.test.ts +426 -0
- package/src/server/__tests__/securityMiddleware.test.ts +449 -0
- package/src/server/__tests__/server-startup.integration.test.ts +477 -0
- package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -0
- package/src/server/__tests__/toolRegistry.test.ts +855 -0
- package/src/server/budgetResolver.ts +235 -0
- package/src/server/cacheManager.ts +503 -0
- package/src/server/config.ts +41 -0
- package/src/server/deltaCache.merge.ts +149 -0
- package/src/server/deltaCache.ts +341 -0
- package/src/server/diagnostics.ts +338 -0
- package/src/server/errorHandler.ts +756 -0
- package/src/server/prompts.ts +291 -0
- package/src/server/rateLimiter.ts +156 -0
- package/src/server/requestLogger.ts +344 -0
- package/src/server/resources.ts +168 -0
- package/src/server/responseFormatter.ts +51 -0
- package/src/server/securityMiddleware.ts +236 -0
- package/src/server/serverKnowledgeStore.ts +91 -0
- package/src/server/toolRegistry.ts +489 -0
- package/src/tools/.gitkeep +1 -0
- package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -0
- package/src/tools/__tests__/accountTools.integration.test.ts +117 -0
- package/src/tools/__tests__/accountTools.test.ts +653 -0
- package/src/tools/__tests__/budgetTools.delta.integration.test.ts +90 -0
- package/src/tools/__tests__/budgetTools.integration.test.ts +134 -0
- package/src/tools/__tests__/budgetTools.test.ts +423 -0
- package/src/tools/__tests__/categoryTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/categoryTools.integration.test.ts +295 -0
- package/src/tools/__tests__/categoryTools.test.ts +622 -0
- package/src/tools/__tests__/compareTransactions/formatter.test.ts +486 -0
- package/src/tools/__tests__/compareTransactions/index.test.ts +383 -0
- package/src/tools/__tests__/compareTransactions/matcher.test.ts +410 -0
- package/src/tools/__tests__/compareTransactions/parser.test.ts +764 -0
- package/src/tools/__tests__/compareTransactions.test.ts +342 -0
- package/src/tools/__tests__/compareTransactions.window.test.ts +147 -0
- package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +76 -0
- package/src/tools/__tests__/deltaFetcher.test.ts +270 -0
- package/src/tools/__tests__/deltaSupport.test.ts +188 -0
- package/src/tools/__tests__/deltaTestUtils.ts +46 -0
- package/src/tools/__tests__/exportTransactions.test.ts +213 -0
- package/src/tools/__tests__/monthTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/monthTools.integration.test.ts +174 -0
- package/src/tools/__tests__/monthTools.test.ts +523 -0
- package/src/tools/__tests__/payeeTools.delta.integration.test.ts +80 -0
- package/src/tools/__tests__/payeeTools.integration.test.ts +150 -0
- package/src/tools/__tests__/payeeTools.test.ts +445 -0
- package/src/tools/__tests__/transactionTools.integration.test.ts +762 -0
- package/src/tools/__tests__/transactionTools.test.ts +3521 -0
- package/src/tools/__tests__/utilityTools.integration.test.ts +128 -0
- package/src/tools/__tests__/utilityTools.test.ts +205 -0
- package/src/tools/accountTools.ts +283 -0
- package/src/tools/budgetTools.ts +112 -0
- package/src/tools/categoryTools.ts +366 -0
- package/src/tools/compareTransactions/formatter.ts +163 -0
- package/src/tools/compareTransactions/index.ts +228 -0
- package/src/tools/compareTransactions/matcher.ts +240 -0
- package/src/tools/compareTransactions/parser.ts +557 -0
- package/src/tools/compareTransactions/types.ts +60 -0
- package/src/tools/compareTransactions.ts +3 -0
- package/src/tools/deltaFetcher.ts +278 -0
- package/src/tools/deltaSupport.ts +293 -0
- package/src/tools/exportTransactions.ts +273 -0
- package/src/tools/monthTools.ts +164 -0
- package/src/tools/payeeTools.ts +140 -0
- package/src/tools/reconcileAdapter.ts +312 -0
- package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +122 -0
- package/src/tools/reconciliation/__tests__/adapter.test.ts +234 -0
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +406 -0
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +366 -0
- package/src/tools/reconciliation/__tests__/executor.test.ts +779 -0
- package/src/tools/reconciliation/__tests__/matcher.test.ts +650 -0
- package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +278 -0
- package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +658 -0
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1000 -0
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +151 -0
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +573 -0
- package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +78 -0
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +47 -0
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +61 -0
- package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +49 -0
- package/src/tools/reconciliation/analyzer.ts +824 -0
- package/src/tools/reconciliation/executor.ts +880 -0
- package/src/tools/reconciliation/index.ts +400 -0
- package/src/tools/reconciliation/matcher.ts +269 -0
- package/src/tools/reconciliation/payeeNormalizer.ts +167 -0
- package/src/tools/reconciliation/recommendationEngine.ts +506 -0
- package/src/tools/reconciliation/reportFormatter.ts +363 -0
- package/src/tools/reconciliation/types.ts +314 -0
- package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +424 -0
- package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +310 -0
- package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +448 -0
- package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +519 -0
- package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +155 -0
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +288 -0
- package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +478 -0
- package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +370 -0
- package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +401 -0
- package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +213 -0
- package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +474 -0
- package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +333 -0
- package/src/tools/schemas/outputs/accountOutputs.ts +137 -0
- package/src/tools/schemas/outputs/budgetOutputs.ts +86 -0
- package/src/tools/schemas/outputs/categoryOutputs.ts +194 -0
- package/src/tools/schemas/outputs/comparisonOutputs.ts +600 -0
- package/src/tools/schemas/outputs/index.ts +270 -0
- package/src/tools/schemas/outputs/monthOutputs.ts +243 -0
- package/src/tools/schemas/outputs/payeeOutputs.ts +105 -0
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +796 -0
- package/src/tools/schemas/outputs/transactionMutationOutputs.ts +758 -0
- package/src/tools/schemas/outputs/transactionOutputs.ts +243 -0
- package/src/tools/schemas/outputs/utilityOutputs.ts +411 -0
- package/src/tools/schemas/shared/commonOutputs.ts +140 -0
- package/src/tools/toolCategories.ts +140 -0
- package/src/tools/transactionTools.ts +2509 -0
- package/src/tools/utilityTools.ts +90 -0
- package/src/types/.gitkeep +1 -0
- package/src/types/__tests__/index.test.ts +52 -0
- package/src/types/index.ts +67 -0
- package/src/types/integration-tests.d.ts +35 -0
- package/src/types/toolAnnotations.ts +44 -0
- package/src/utils/__tests__/dateUtils.test.ts +170 -0
- package/src/utils/__tests__/money.test.ts +189 -0
- package/src/utils/amountUtils.ts +32 -0
- package/src/utils/dateUtils.ts +108 -0
- package/src/utils/money.ts +123 -0
- package/test-csv-sample.csv +28 -0
- package/test-exports/sample_bank_statement.csv +7 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +23 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +23 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +44 -0
- package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +58 -0
- package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +115 -0
- package/test-reconcile-autodetect.js +40 -0
- package/test-reconcile-tool.js +152 -0
- package/test-reconcile-with-csv.cjs +89 -0
- package/test-statement.csv +8 -0
- package/test_debug.js +47 -0
- package/test_simple.mjs +16 -0
- package/tsconfig.json +31 -0
- package/tsconfig.prod.json +18 -0
- package/vitest-reporters/split-json-reporter.ts +211 -0
- package/vitest.config.ts +96 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconciliation tool - Phase 1: Analysis Only
|
|
3
|
+
* Implements guided reconciliation workflow with conservative matching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod/v4';
|
|
7
|
+
import type * as ynab from 'ynab';
|
|
8
|
+
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import { withToolErrorHandling } from '../../types/index.js';
|
|
10
|
+
import { analyzeReconciliation } from './analyzer.js';
|
|
11
|
+
import type { MatchingConfig } from './types.js';
|
|
12
|
+
import { buildReconciliationPayload } from '../reconcileAdapter.js';
|
|
13
|
+
import {
|
|
14
|
+
executeReconciliation,
|
|
15
|
+
type AccountSnapshot,
|
|
16
|
+
type LegacyReconciliationResult,
|
|
17
|
+
} from './executor.js';
|
|
18
|
+
import { responseFormatter } from '../../server/responseFormatter.js';
|
|
19
|
+
import { extractDateRangeFromCSV, autoDetectCSVFormat } from '../compareTransactions/parser.js';
|
|
20
|
+
import type { DeltaFetcher } from '../deltaFetcher.js';
|
|
21
|
+
import { resolveDeltaFetcherArgs } from '../deltaSupport.js';
|
|
22
|
+
|
|
23
|
+
// Re-export types for external use
|
|
24
|
+
export type * from './types.js';
|
|
25
|
+
export { analyzeReconciliation } from './analyzer.js';
|
|
26
|
+
export { findMatches, findBestMatch } from './matcher.js';
|
|
27
|
+
export { normalizePayee, normalizedMatch, fuzzyMatch, payeeSimilarity } from './payeeNormalizer.js';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Helper function to determine audit data source based on fetch result
|
|
31
|
+
*/
|
|
32
|
+
function getAuditDataSource(
|
|
33
|
+
transactionsResult: { usedDelta?: boolean; wasCached?: boolean },
|
|
34
|
+
forceFullRefresh: boolean,
|
|
35
|
+
): string {
|
|
36
|
+
if (forceFullRefresh) {
|
|
37
|
+
return 'full_api_fetch_no_delta';
|
|
38
|
+
}
|
|
39
|
+
if (transactionsResult.usedDelta) {
|
|
40
|
+
return 'delta_fetch_with_merge';
|
|
41
|
+
}
|
|
42
|
+
if (transactionsResult.wasCached) {
|
|
43
|
+
return 'delta_fetch_cache_hit';
|
|
44
|
+
}
|
|
45
|
+
return 'delta_fetch_full_refresh';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Helper function to determine data freshness based on fetch result
|
|
50
|
+
*/
|
|
51
|
+
function getDataFreshness(
|
|
52
|
+
transactionsResult: { wasCached?: boolean },
|
|
53
|
+
forceFullRefresh: boolean,
|
|
54
|
+
): string {
|
|
55
|
+
if (forceFullRefresh) {
|
|
56
|
+
return 'guaranteed_fresh';
|
|
57
|
+
}
|
|
58
|
+
if (transactionsResult.wasCached) {
|
|
59
|
+
return 'cache_validated_via_server_knowledge';
|
|
60
|
+
}
|
|
61
|
+
return 'fresh_via_delta_fetch';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Schema for reconcile_account tool
|
|
66
|
+
*/
|
|
67
|
+
export const ReconcileAccountSchema = z
|
|
68
|
+
.object({
|
|
69
|
+
budget_id: z.string().min(1, 'Budget ID is required'),
|
|
70
|
+
account_id: z.string().min(1, 'Account ID is required'),
|
|
71
|
+
|
|
72
|
+
// CSV input (one required)
|
|
73
|
+
csv_file_path: z.string().optional(),
|
|
74
|
+
csv_data: z.string().optional(),
|
|
75
|
+
|
|
76
|
+
csv_format: z
|
|
77
|
+
.object({
|
|
78
|
+
date_column: z.union([z.string(), z.number()]).optional().default('Date'),
|
|
79
|
+
amount_column: z.union([z.string(), z.number()]).optional(),
|
|
80
|
+
debit_column: z.union([z.string(), z.number()]).optional(),
|
|
81
|
+
credit_column: z.union([z.string(), z.number()]).optional(),
|
|
82
|
+
description_column: z.union([z.string(), z.number()]).optional().default('Description'),
|
|
83
|
+
date_format: z.string().optional().default('MM/DD/YYYY'),
|
|
84
|
+
has_header: z.boolean().optional().default(true),
|
|
85
|
+
delimiter: z.string().optional().default(','),
|
|
86
|
+
})
|
|
87
|
+
.strict()
|
|
88
|
+
.optional()
|
|
89
|
+
.default(() => ({
|
|
90
|
+
date_column: 'Date',
|
|
91
|
+
amount_column: 'Amount',
|
|
92
|
+
description_column: 'Description',
|
|
93
|
+
date_format: 'MM/DD/YYYY',
|
|
94
|
+
has_header: true,
|
|
95
|
+
delimiter: ',',
|
|
96
|
+
})),
|
|
97
|
+
|
|
98
|
+
// Statement information
|
|
99
|
+
statement_balance: z.number({
|
|
100
|
+
message: 'Statement balance is required and must be a number',
|
|
101
|
+
}),
|
|
102
|
+
statement_start_date: z.string().optional(),
|
|
103
|
+
statement_end_date: z.string().optional(),
|
|
104
|
+
statement_date: z.string().optional(),
|
|
105
|
+
expected_bank_balance: z.number().optional(),
|
|
106
|
+
as_of_timezone: z.string().optional(),
|
|
107
|
+
|
|
108
|
+
// Matching configuration (optional)
|
|
109
|
+
date_tolerance_days: z.number().min(0).max(7).optional().default(5),
|
|
110
|
+
amount_tolerance_cents: z.number().min(0).max(100).optional().default(1),
|
|
111
|
+
auto_match_threshold: z.number().min(0).max(100).optional().default(90),
|
|
112
|
+
suggestion_threshold: z.number().min(0).max(100).optional().default(60),
|
|
113
|
+
amount_tolerance: z.number().min(0).max(1).optional(),
|
|
114
|
+
|
|
115
|
+
auto_create_transactions: z.boolean().optional().default(false),
|
|
116
|
+
auto_update_cleared_status: z.boolean().optional().default(false),
|
|
117
|
+
auto_unclear_missing: z.boolean().optional().default(true),
|
|
118
|
+
auto_adjust_dates: z.boolean().optional().default(false),
|
|
119
|
+
invert_bank_amounts: z.boolean().optional(),
|
|
120
|
+
dry_run: z.boolean().optional().default(true),
|
|
121
|
+
balance_verification_mode: z
|
|
122
|
+
.enum(['ANALYSIS_ONLY', 'GUIDED_RESOLUTION', 'AUTO_RESOLVE'])
|
|
123
|
+
.optional()
|
|
124
|
+
.default('ANALYSIS_ONLY'),
|
|
125
|
+
require_exact_match: z.boolean().optional().default(true),
|
|
126
|
+
confidence_threshold: z.number().min(0).max(1).optional().default(0.8),
|
|
127
|
+
max_resolution_attempts: z.number().int().min(1).max(10).optional().default(5),
|
|
128
|
+
|
|
129
|
+
// Response options
|
|
130
|
+
include_structured_data: z.boolean().optional().default(false),
|
|
131
|
+
force_full_refresh: z.boolean().optional().default(true),
|
|
132
|
+
})
|
|
133
|
+
.refine((data) => data.csv_file_path || data.csv_data, {
|
|
134
|
+
message: 'Either csv_file_path or csv_data must be provided',
|
|
135
|
+
path: ['csv_data'],
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export type ReconcileAccountRequest = z.infer<typeof ReconcileAccountSchema>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Handle reconciliation analysis and optional execution
|
|
142
|
+
*
|
|
143
|
+
* Provides intelligent transaction matching, insight detection, and optional
|
|
144
|
+
* execution of reconciliation actions. Returns human-readable narrative and
|
|
145
|
+
* structured JSON data.
|
|
146
|
+
*/
|
|
147
|
+
export async function handleReconcileAccount(
|
|
148
|
+
ynabAPI: ynab.API,
|
|
149
|
+
deltaFetcher: DeltaFetcher,
|
|
150
|
+
params: ReconcileAccountRequest,
|
|
151
|
+
): Promise<CallToolResult>;
|
|
152
|
+
export async function handleReconcileAccount(
|
|
153
|
+
ynabAPI: ynab.API,
|
|
154
|
+
params: ReconcileAccountRequest,
|
|
155
|
+
): Promise<CallToolResult>;
|
|
156
|
+
export async function handleReconcileAccount(
|
|
157
|
+
ynabAPI: ynab.API,
|
|
158
|
+
deltaFetcherOrParams: DeltaFetcher | ReconcileAccountRequest,
|
|
159
|
+
maybeParams?: ReconcileAccountRequest,
|
|
160
|
+
): Promise<CallToolResult> {
|
|
161
|
+
const { deltaFetcher, params } = resolveDeltaFetcherArgs(
|
|
162
|
+
ynabAPI,
|
|
163
|
+
deltaFetcherOrParams,
|
|
164
|
+
maybeParams,
|
|
165
|
+
);
|
|
166
|
+
const forceFullRefresh = params.force_full_refresh ?? true;
|
|
167
|
+
return await withToolErrorHandling(
|
|
168
|
+
async () => {
|
|
169
|
+
// Build matching configuration from parameters
|
|
170
|
+
const config: MatchingConfig = {
|
|
171
|
+
dateToleranceDays: params.date_tolerance_days,
|
|
172
|
+
amountToleranceCents: params.amount_tolerance_cents,
|
|
173
|
+
descriptionSimilarityThreshold: 0.8, // Fixed for Phase 1
|
|
174
|
+
autoMatchThreshold: params.auto_match_threshold,
|
|
175
|
+
suggestionThreshold: params.suggestion_threshold,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const accountResult = forceFullRefresh
|
|
179
|
+
? await deltaFetcher.fetchAccountsFull(params.budget_id)
|
|
180
|
+
: await deltaFetcher.fetchAccounts(params.budget_id);
|
|
181
|
+
const accountData = accountResult.data.find((account) => account.id === params.account_id);
|
|
182
|
+
if (!accountData) {
|
|
183
|
+
throw new Error(`Account ${params.account_id} not found in budget ${params.budget_id}`);
|
|
184
|
+
}
|
|
185
|
+
const accountName = accountData.name;
|
|
186
|
+
const accountType = accountData.type;
|
|
187
|
+
|
|
188
|
+
// For liability accounts (credit cards, loans, debts), statement balance should be negative
|
|
189
|
+
// A positive balance on a credit card statement means you OWE that amount
|
|
190
|
+
const accountIsLiability =
|
|
191
|
+
accountType === 'creditCard' ||
|
|
192
|
+
accountType === 'lineOfCredit' ||
|
|
193
|
+
accountType === 'mortgage' ||
|
|
194
|
+
accountType === 'autoLoan' ||
|
|
195
|
+
accountType === 'studentLoan' ||
|
|
196
|
+
accountType === 'personalLoan' ||
|
|
197
|
+
accountType === 'medicalDebt' ||
|
|
198
|
+
accountType === 'otherDebt' ||
|
|
199
|
+
accountType === 'otherLiability';
|
|
200
|
+
|
|
201
|
+
// Determine whether to invert bank amounts
|
|
202
|
+
// If invert_bank_amounts is explicitly set, use that value
|
|
203
|
+
// Otherwise, default to true for liability accounts (legacy behavior)
|
|
204
|
+
// Note: Some banks (e.g., Wealthsimple) show charges as negative already, matching YNAB
|
|
205
|
+
const shouldInvertBankAmounts =
|
|
206
|
+
params.invert_bank_amounts !== undefined ? params.invert_bank_amounts : accountIsLiability;
|
|
207
|
+
|
|
208
|
+
// Negate statement balance for liability accounts
|
|
209
|
+
const adjustedStatementBalance = accountIsLiability
|
|
210
|
+
? -Math.abs(params.statement_balance)
|
|
211
|
+
: params.statement_balance;
|
|
212
|
+
|
|
213
|
+
const budgetResponse = await ynabAPI.budgets.getBudgetById(params.budget_id);
|
|
214
|
+
const currencyCode = budgetResponse.data.budget?.currency_format?.iso_code ?? 'USD';
|
|
215
|
+
|
|
216
|
+
// Fetch YNAB transactions for the account
|
|
217
|
+
// Auto-detect date range from CSV if not explicitly provided
|
|
218
|
+
let sinceDate: Date;
|
|
219
|
+
|
|
220
|
+
if (params.statement_start_date) {
|
|
221
|
+
// User provided explicit start date
|
|
222
|
+
sinceDate = new Date(params.statement_start_date);
|
|
223
|
+
} else {
|
|
224
|
+
// Auto-detect from CSV content
|
|
225
|
+
try {
|
|
226
|
+
const csvContent = params.csv_data || params.csv_file_path || '';
|
|
227
|
+
const csvFormat = params.csv_format || autoDetectCSVFormat(csvContent);
|
|
228
|
+
|
|
229
|
+
// Convert schema format to parser format
|
|
230
|
+
const parserFormat = {
|
|
231
|
+
date_column: csvFormat.date_column || 'Date',
|
|
232
|
+
amount_column: csvFormat.amount_column,
|
|
233
|
+
debit_column: csvFormat.debit_column,
|
|
234
|
+
credit_column: csvFormat.credit_column,
|
|
235
|
+
description_column: csvFormat.description_column || 'Description',
|
|
236
|
+
date_format: csvFormat.date_format || 'MM/DD/YYYY',
|
|
237
|
+
has_header: csvFormat.has_header ?? true,
|
|
238
|
+
delimiter: csvFormat.delimiter || ',',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const { minDate } = extractDateRangeFromCSV(csvContent, parserFormat);
|
|
242
|
+
|
|
243
|
+
// Add 7-day buffer before min date for pending transactions
|
|
244
|
+
const minDateObj = new Date(minDate);
|
|
245
|
+
minDateObj.setDate(minDateObj.getDate() - 7);
|
|
246
|
+
sinceDate = minDateObj;
|
|
247
|
+
} catch {
|
|
248
|
+
// Fallback to 90 days if CSV parsing fails
|
|
249
|
+
sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const sinceDateString = sinceDate.toISOString().split('T')[0];
|
|
254
|
+
const transactionsResult = forceFullRefresh
|
|
255
|
+
? await deltaFetcher.fetchTransactionsByAccountFull(
|
|
256
|
+
params.budget_id,
|
|
257
|
+
params.account_id,
|
|
258
|
+
sinceDateString,
|
|
259
|
+
)
|
|
260
|
+
: await deltaFetcher.fetchTransactionsByAccount(
|
|
261
|
+
params.budget_id,
|
|
262
|
+
params.account_id,
|
|
263
|
+
sinceDateString,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const ynabTransactions = transactionsResult.data;
|
|
267
|
+
|
|
268
|
+
const auditMetadata = {
|
|
269
|
+
data_freshness: getDataFreshness(transactionsResult, forceFullRefresh),
|
|
270
|
+
data_source: getAuditDataSource(transactionsResult, forceFullRefresh),
|
|
271
|
+
server_knowledge: transactionsResult.serverKnowledge,
|
|
272
|
+
fetched_at: new Date().toISOString(),
|
|
273
|
+
accounts_count: accountResult.data.length,
|
|
274
|
+
transactions_count: transactionsResult.data.length,
|
|
275
|
+
cache_status: {
|
|
276
|
+
accounts_cached: accountResult.wasCached,
|
|
277
|
+
transactions_cached: transactionsResult.wasCached,
|
|
278
|
+
delta_merge_applied: transactionsResult.usedDelta,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Perform analysis
|
|
283
|
+
const analysis = analyzeReconciliation(
|
|
284
|
+
params.csv_data || params.csv_file_path || '',
|
|
285
|
+
params.csv_file_path,
|
|
286
|
+
ynabTransactions,
|
|
287
|
+
adjustedStatementBalance,
|
|
288
|
+
config,
|
|
289
|
+
currencyCode,
|
|
290
|
+
params.account_id,
|
|
291
|
+
params.budget_id,
|
|
292
|
+
shouldInvertBankAmounts,
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const initialAccount: AccountSnapshot = {
|
|
296
|
+
balance: accountData.balance,
|
|
297
|
+
cleared_balance: accountData.cleared_balance,
|
|
298
|
+
uncleared_balance: accountData.uncleared_balance,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
let executionData: LegacyReconciliationResult | undefined;
|
|
302
|
+
const wantsBalanceVerification = Boolean(params.statement_date);
|
|
303
|
+
const shouldExecute =
|
|
304
|
+
params.auto_create_transactions ||
|
|
305
|
+
params.auto_update_cleared_status ||
|
|
306
|
+
params.auto_unclear_missing ||
|
|
307
|
+
params.auto_adjust_dates ||
|
|
308
|
+
params.balance_verification_mode !== 'ANALYSIS_ONLY' ||
|
|
309
|
+
wantsBalanceVerification;
|
|
310
|
+
|
|
311
|
+
if (shouldExecute) {
|
|
312
|
+
executionData = await executeReconciliation({
|
|
313
|
+
ynabAPI,
|
|
314
|
+
analysis,
|
|
315
|
+
params,
|
|
316
|
+
budgetId: params.budget_id,
|
|
317
|
+
accountId: params.account_id,
|
|
318
|
+
initialAccount,
|
|
319
|
+
currencyCode,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const csvFormatForPayload = mapCsvFormatForPayload(params.csv_format);
|
|
324
|
+
|
|
325
|
+
const adapterOptions: Parameters<typeof buildReconciliationPayload>[1] = {
|
|
326
|
+
accountName,
|
|
327
|
+
accountId: params.account_id,
|
|
328
|
+
currencyCode,
|
|
329
|
+
auditMetadata,
|
|
330
|
+
};
|
|
331
|
+
if (csvFormatForPayload !== undefined) {
|
|
332
|
+
adapterOptions.csvFormat = csvFormatForPayload;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const payload = buildReconciliationPayload(analysis, adapterOptions, executionData);
|
|
336
|
+
|
|
337
|
+
// Build response payload matching ReconcileAccountOutputSchema
|
|
338
|
+
// Schema expects: { human: string } OR { human: string, structured: object }
|
|
339
|
+
const responseData: Record<string, unknown> = {
|
|
340
|
+
human: payload.human,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Only include structured data if requested (can be very large)
|
|
344
|
+
if (params.include_structured_data) {
|
|
345
|
+
responseData['structured'] = payload.structured;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return {
|
|
349
|
+
content: [
|
|
350
|
+
{
|
|
351
|
+
type: 'text',
|
|
352
|
+
text: responseFormatter.format(responseData),
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
};
|
|
356
|
+
},
|
|
357
|
+
'ynab:reconcile_account',
|
|
358
|
+
'analyzing account reconciliation',
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function mapCsvFormatForPayload(format: ReconcileAccountRequest['csv_format'] | undefined):
|
|
363
|
+
| {
|
|
364
|
+
delimiter: string;
|
|
365
|
+
decimal_separator: string;
|
|
366
|
+
thousands_separator: string | null;
|
|
367
|
+
date_format: string;
|
|
368
|
+
header_row: boolean;
|
|
369
|
+
date_column: string | null;
|
|
370
|
+
amount_column: string | null;
|
|
371
|
+
payee_column: string | null;
|
|
372
|
+
}
|
|
373
|
+
| undefined {
|
|
374
|
+
if (!format) {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const coerceString = (value: string | number | undefined | null, fallback?: string) => {
|
|
379
|
+
if (value === undefined || value === null) {
|
|
380
|
+
return fallback ?? null;
|
|
381
|
+
}
|
|
382
|
+
return String(value);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const delimiter = coerceString(format.delimiter, ',');
|
|
386
|
+
const decimalSeparator = '.'; // Default decimal separator
|
|
387
|
+
const thousandsSeparator = ','; // Default thousands separator
|
|
388
|
+
const dateFormat = coerceString(format.date_format, 'MM/DD/YYYY');
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
delimiter: delimiter ?? ',',
|
|
392
|
+
decimal_separator: decimalSeparator,
|
|
393
|
+
thousands_separator: thousandsSeparator,
|
|
394
|
+
date_format: dateFormat ?? 'MM/DD/YYYY',
|
|
395
|
+
header_row: format.has_header ?? true,
|
|
396
|
+
date_column: coerceString(format.date_column, '') ?? null,
|
|
397
|
+
amount_column: coerceString(format.amount_column, '') ?? null,
|
|
398
|
+
payee_column: coerceString(format.description_column, '') ?? null,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction matching algorithm for reconciliation
|
|
3
|
+
* Implements confidence-based matching with auto-match and suggestion tiers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { normalizedMatch, payeeSimilarity } from './payeeNormalizer.js';
|
|
7
|
+
import { DEFAULT_MATCHING_CONFIG } from './types.js';
|
|
8
|
+
import type {
|
|
9
|
+
BankTransaction,
|
|
10
|
+
YNABTransaction,
|
|
11
|
+
TransactionMatch,
|
|
12
|
+
MatchCandidate,
|
|
13
|
+
MatchingConfig,
|
|
14
|
+
} from './types.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Check if two amounts match within tolerance
|
|
18
|
+
*/
|
|
19
|
+
function amountsMatch(bankAmount: number, ynabAmount: number, toleranceCents: number): boolean {
|
|
20
|
+
// Convert YNAB milliunits to dollars
|
|
21
|
+
const ynabDollars = ynabAmount / 1000;
|
|
22
|
+
|
|
23
|
+
// Round to avoid floating point precision issues
|
|
24
|
+
const difference = Math.round(Math.abs(bankAmount - ynabDollars) * 100) / 100;
|
|
25
|
+
const toleranceDollars = toleranceCents / 100;
|
|
26
|
+
|
|
27
|
+
return difference <= toleranceDollars;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if two dates match within tolerance
|
|
32
|
+
*/
|
|
33
|
+
function datesMatch(date1: string, date2: string, toleranceDays: number): boolean {
|
|
34
|
+
const d1 = new Date(date1);
|
|
35
|
+
const d2 = new Date(date2);
|
|
36
|
+
|
|
37
|
+
const diffMs = Math.abs(d1.getTime() - d2.getTime());
|
|
38
|
+
const diffDays = diffMs / (1000 * 60 * 60 * 24);
|
|
39
|
+
|
|
40
|
+
return diffDays <= toleranceDays;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculate match confidence score between bank and YNAB transaction
|
|
45
|
+
* Returns score 0-100 and match reasons
|
|
46
|
+
*/
|
|
47
|
+
function calculateMatchScore(
|
|
48
|
+
bankTxn: BankTransaction,
|
|
49
|
+
ynabTxn: YNABTransaction,
|
|
50
|
+
config: MatchingConfig,
|
|
51
|
+
): { score: number; reasons: string[] } {
|
|
52
|
+
const reasons: string[] = [];
|
|
53
|
+
let score = 0;
|
|
54
|
+
|
|
55
|
+
// Amount match (40% weight) - REQUIRED
|
|
56
|
+
const amountMatch = amountsMatch(bankTxn.amount, ynabTxn.amount, config.amountToleranceCents);
|
|
57
|
+
if (!amountMatch) {
|
|
58
|
+
return { score: 0, reasons: ['Amount does not match'] };
|
|
59
|
+
}
|
|
60
|
+
score += 40;
|
|
61
|
+
reasons.push('Amount matches');
|
|
62
|
+
|
|
63
|
+
// Date match (40% weight)
|
|
64
|
+
const dateWithinTolerance = datesMatch(bankTxn.date, ynabTxn.date, config.dateToleranceDays);
|
|
65
|
+
if (dateWithinTolerance) {
|
|
66
|
+
score += 40;
|
|
67
|
+
const daysDiff = Math.abs(
|
|
68
|
+
(new Date(bankTxn.date).getTime() - new Date(ynabTxn.date).getTime()) / (1000 * 60 * 60 * 24),
|
|
69
|
+
);
|
|
70
|
+
if (daysDiff === 0) {
|
|
71
|
+
reasons.push('Exact date match');
|
|
72
|
+
} else {
|
|
73
|
+
reasons.push(`Date within ${Math.round(daysDiff)} days`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Payee match (20% weight)
|
|
78
|
+
const payeeScore = payeeSimilarity(bankTxn.payee, ynabTxn.payee_name);
|
|
79
|
+
|
|
80
|
+
if (normalizedMatch(bankTxn.payee, ynabTxn.payee_name)) {
|
|
81
|
+
score += 20;
|
|
82
|
+
reasons.push('Payee exact match');
|
|
83
|
+
} else if (payeeScore >= 95) {
|
|
84
|
+
score += 15;
|
|
85
|
+
reasons.push(`Payee highly similar (${Math.round(payeeScore)}%)`);
|
|
86
|
+
} else if (payeeScore >= 80) {
|
|
87
|
+
score += 10;
|
|
88
|
+
reasons.push(`Payee similar (${Math.round(payeeScore)}%)`);
|
|
89
|
+
} else if (payeeScore >= 60) {
|
|
90
|
+
score += 6;
|
|
91
|
+
reasons.push(`Payee somewhat similar (${Math.round(payeeScore)}%)`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { score: Math.round(score), reasons };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Priority scoring for YNAB transactions
|
|
99
|
+
* Uncleared transactions get higher priority than cleared ones
|
|
100
|
+
*/
|
|
101
|
+
function getPriority(ynabTxn: YNABTransaction): number {
|
|
102
|
+
// Uncleared transactions are expecting bank confirmation
|
|
103
|
+
if (ynabTxn.cleared === 'uncleared') return 10;
|
|
104
|
+
if (ynabTxn.cleared === 'cleared') return 5;
|
|
105
|
+
if (ynabTxn.cleared === 'reconciled') return 1;
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Find all matching candidates for a bank transaction
|
|
111
|
+
*/
|
|
112
|
+
function findMatchCandidates(
|
|
113
|
+
bankTxn: BankTransaction,
|
|
114
|
+
ynabTransactions: YNABTransaction[],
|
|
115
|
+
usedIds: Set<string>,
|
|
116
|
+
config: MatchingConfig,
|
|
117
|
+
): MatchCandidate[] {
|
|
118
|
+
const candidates: MatchCandidate[] = [];
|
|
119
|
+
|
|
120
|
+
for (const ynabTxn of ynabTransactions) {
|
|
121
|
+
// Skip already matched transactions
|
|
122
|
+
if (usedIds.has(ynabTxn.id)) continue;
|
|
123
|
+
|
|
124
|
+
// Skip opposite-signed transactions (refunds vs purchases)
|
|
125
|
+
if (bankTxn.amount > 0 !== ynabTxn.amount > 0) continue;
|
|
126
|
+
|
|
127
|
+
// Calculate match score
|
|
128
|
+
const { score, reasons } = calculateMatchScore(bankTxn, ynabTxn, config);
|
|
129
|
+
|
|
130
|
+
// Only include candidates with minimum score
|
|
131
|
+
if (score >= 30) {
|
|
132
|
+
candidates.push({
|
|
133
|
+
ynab_transaction: ynabTxn,
|
|
134
|
+
confidence: score,
|
|
135
|
+
match_reason: reasons.join(', '),
|
|
136
|
+
explanation: buildExplanation(bankTxn, ynabTxn, score, reasons),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Sort by confidence (desc), then priority (desc), then date proximity
|
|
142
|
+
candidates.sort((a, b) => {
|
|
143
|
+
if (b.confidence !== a.confidence) {
|
|
144
|
+
return b.confidence - a.confidence;
|
|
145
|
+
}
|
|
146
|
+
const priorityDiff = getPriority(b.ynab_transaction) - getPriority(a.ynab_transaction);
|
|
147
|
+
if (priorityDiff !== 0) return priorityDiff;
|
|
148
|
+
|
|
149
|
+
// Date proximity as tiebreaker
|
|
150
|
+
const dateProximityA = Math.abs(
|
|
151
|
+
new Date(bankTxn.date).getTime() - new Date(a.ynab_transaction.date).getTime(),
|
|
152
|
+
);
|
|
153
|
+
const dateProximityB = Math.abs(
|
|
154
|
+
new Date(bankTxn.date).getTime() - new Date(b.ynab_transaction.date).getTime(),
|
|
155
|
+
);
|
|
156
|
+
return dateProximityA - dateProximityB;
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return candidates;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build human-readable explanation for a match
|
|
164
|
+
*/
|
|
165
|
+
function buildExplanation(
|
|
166
|
+
_bankTxn: BankTransaction,
|
|
167
|
+
ynabTxn: YNABTransaction,
|
|
168
|
+
score: number,
|
|
169
|
+
reasons: string[],
|
|
170
|
+
): string {
|
|
171
|
+
const parts: string[] = [];
|
|
172
|
+
|
|
173
|
+
parts.push(`Match confidence: ${score}%`);
|
|
174
|
+
parts.push(reasons.join(', '));
|
|
175
|
+
|
|
176
|
+
if (ynabTxn.cleared === 'uncleared') {
|
|
177
|
+
parts.push('(Uncleared - awaiting confirmation)');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return parts.join(' | ');
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Find best match for a single bank transaction
|
|
185
|
+
*/
|
|
186
|
+
export function findBestMatch(
|
|
187
|
+
bankTxn: BankTransaction,
|
|
188
|
+
ynabTransactions: YNABTransaction[],
|
|
189
|
+
usedIds: Set<string>,
|
|
190
|
+
config: MatchingConfig,
|
|
191
|
+
): TransactionMatch {
|
|
192
|
+
const candidates = findMatchCandidates(bankTxn, ynabTransactions, usedIds, config);
|
|
193
|
+
|
|
194
|
+
if (candidates.length === 0) {
|
|
195
|
+
// No match found
|
|
196
|
+
return {
|
|
197
|
+
bank_transaction: bankTxn,
|
|
198
|
+
confidence: 'none',
|
|
199
|
+
confidence_score: 0,
|
|
200
|
+
match_reason: 'No matching transaction found in YNAB',
|
|
201
|
+
action_hint: 'add_to_ynab',
|
|
202
|
+
recommendation: 'This transaction appears on bank statement but not in YNAB',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const bestCandidate = candidates[0]!; // Safe: we checked candidates.length > 0
|
|
207
|
+
const bestScore = bestCandidate.confidence;
|
|
208
|
+
|
|
209
|
+
// HIGH confidence: Auto-match candidate (≥90%)
|
|
210
|
+
if (bestScore >= config.autoMatchThreshold) {
|
|
211
|
+
return {
|
|
212
|
+
bank_transaction: bankTxn,
|
|
213
|
+
ynab_transaction: bestCandidate.ynab_transaction,
|
|
214
|
+
confidence: 'high',
|
|
215
|
+
confidence_score: bestScore,
|
|
216
|
+
match_reason: bestCandidate.match_reason,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// MEDIUM confidence: Suggested match (60-89%)
|
|
221
|
+
if (bestScore >= config.suggestionThreshold) {
|
|
222
|
+
return {
|
|
223
|
+
bank_transaction: bankTxn,
|
|
224
|
+
ynab_transaction: bestCandidate.ynab_transaction,
|
|
225
|
+
candidates: candidates.slice(0, 3), // Top 3 candidates
|
|
226
|
+
confidence: 'medium',
|
|
227
|
+
confidence_score: bestScore,
|
|
228
|
+
match_reason: bestCandidate.match_reason,
|
|
229
|
+
top_confidence: bestScore,
|
|
230
|
+
action_hint: 'review_and_choose',
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// LOW confidence: Show as possible match but don't auto-suggest (30-59%)
|
|
235
|
+
return {
|
|
236
|
+
bank_transaction: bankTxn,
|
|
237
|
+
candidates: candidates.slice(0, 3),
|
|
238
|
+
confidence: 'low',
|
|
239
|
+
confidence_score: bestScore,
|
|
240
|
+
match_reason: 'Low confidence match',
|
|
241
|
+
top_confidence: bestScore,
|
|
242
|
+
action_hint: 'review_or_add_new',
|
|
243
|
+
recommendation: 'Consider reviewing candidates or adding as new transaction',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Find matches for all bank transactions
|
|
249
|
+
*/
|
|
250
|
+
export function findMatches(
|
|
251
|
+
bankTransactions: BankTransaction[],
|
|
252
|
+
ynabTransactions: YNABTransaction[],
|
|
253
|
+
config: MatchingConfig = DEFAULT_MATCHING_CONFIG as MatchingConfig,
|
|
254
|
+
): TransactionMatch[] {
|
|
255
|
+
const matches: TransactionMatch[] = [];
|
|
256
|
+
const usedIds = new Set<string>();
|
|
257
|
+
|
|
258
|
+
for (const bankTxn of bankTransactions) {
|
|
259
|
+
const match = findBestMatch(bankTxn, ynabTransactions, usedIds, config);
|
|
260
|
+
matches.push(match);
|
|
261
|
+
|
|
262
|
+
// Mark high-confidence matches as used to prevent duplicate matching
|
|
263
|
+
if (match.confidence === 'high' && match.ynab_transaction) {
|
|
264
|
+
usedIds.add(match.ynab_transaction.id);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return matches;
|
|
269
|
+
}
|