@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,824 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analysis phase orchestration for reconciliation
|
|
3
|
+
* Coordinates CSV parsing, YNAB transaction fetching, and matching
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import type * as ynab from 'ynab';
|
|
8
|
+
import * as bankParser from '../compareTransactions/parser.js';
|
|
9
|
+
import type { CSVFormat as ParserCSVFormat } from '../compareTransactions/types.js';
|
|
10
|
+
import { findMatches } from './matcher.js';
|
|
11
|
+
import { DEFAULT_MATCHING_CONFIG } from './types.js';
|
|
12
|
+
import type {
|
|
13
|
+
BankTransaction,
|
|
14
|
+
YNABTransaction,
|
|
15
|
+
ReconciliationAnalysis,
|
|
16
|
+
TransactionMatch,
|
|
17
|
+
MatchingConfig,
|
|
18
|
+
BalanceInfo,
|
|
19
|
+
ReconciliationSummary,
|
|
20
|
+
ReconciliationInsight,
|
|
21
|
+
} from './types.js';
|
|
22
|
+
import { toMoneyValueFromDecimal } from '../../utils/money.js';
|
|
23
|
+
import { generateRecommendations } from './recommendationEngine.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Convert YNAB API transaction to simplified format
|
|
27
|
+
*/
|
|
28
|
+
function convertYNABTransaction(apiTxn: ynab.TransactionDetail): YNABTransaction {
|
|
29
|
+
return {
|
|
30
|
+
id: apiTxn.id,
|
|
31
|
+
date: apiTxn.date,
|
|
32
|
+
amount: apiTxn.amount,
|
|
33
|
+
payee_name: apiTxn.payee_name || null,
|
|
34
|
+
category_name: apiTxn.category_name || null,
|
|
35
|
+
cleared: apiTxn.cleared,
|
|
36
|
+
approved: apiTxn.approved,
|
|
37
|
+
memo: apiTxn.memo || null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse CSV bank statement and generate unique IDs for tracking
|
|
43
|
+
*/
|
|
44
|
+
const FALLBACK_CSV_FORMAT: ParserCSVFormat = {
|
|
45
|
+
date_column: 'Date',
|
|
46
|
+
amount_column: 'Amount',
|
|
47
|
+
description_column: 'Description',
|
|
48
|
+
date_format: 'MM/DD/YYYY',
|
|
49
|
+
has_header: true,
|
|
50
|
+
delimiter: ',',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const ENABLE_COMBINATION_MATCHING = true;
|
|
54
|
+
|
|
55
|
+
const DAYS_IN_MS = 24 * 60 * 60 * 1000;
|
|
56
|
+
|
|
57
|
+
function toDollars(milliunits: number): number {
|
|
58
|
+
return milliunits / 1000;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function amountTolerance(config: MatchingConfig): number {
|
|
62
|
+
const toleranceCents =
|
|
63
|
+
config.amountToleranceCents ?? DEFAULT_MATCHING_CONFIG.amountToleranceCents ?? 1;
|
|
64
|
+
return Math.max(0, toleranceCents) / 100;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function dateTolerance(config: MatchingConfig): number {
|
|
68
|
+
return config.dateToleranceDays ?? DEFAULT_MATCHING_CONFIG.dateToleranceDays ?? 2;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function daysBetween(dateA: string, dateB: string): number {
|
|
72
|
+
const a = new Date(`${dateA}T00:00:00Z`).getTime();
|
|
73
|
+
const b = new Date(`${dateB}T00:00:00Z`).getTime();
|
|
74
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return Number.POSITIVE_INFINITY;
|
|
75
|
+
return Math.abs(a - b) / DAYS_IN_MS;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function withinDateTolerance(
|
|
79
|
+
bankDate: string,
|
|
80
|
+
ynabTxns: YNABTransaction[],
|
|
81
|
+
toleranceDays: number,
|
|
82
|
+
): boolean {
|
|
83
|
+
return ynabTxns.every((txn) => daysBetween(bankDate, txn.date) <= toleranceDays);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function hasMatchingSign(bankAmount: number, ynabTxns: YNABTransaction[]): boolean {
|
|
87
|
+
const bankSign = Math.sign(bankAmount);
|
|
88
|
+
const sumSign = Math.sign(ynabTxns.reduce((sum, txn) => sum + toDollars(txn.amount), 0));
|
|
89
|
+
return bankSign === sumSign || Math.abs(bankAmount) === 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function computeCombinationConfidence(diff: number, tolerance: number, legCount: number): number {
|
|
93
|
+
const safeTolerance = tolerance > 0 ? tolerance : 0.01;
|
|
94
|
+
const ratio = diff / safeTolerance;
|
|
95
|
+
let base = legCount === 2 ? 75 : 70;
|
|
96
|
+
if (ratio <= 0.25) {
|
|
97
|
+
base += 5;
|
|
98
|
+
} else if (ratio <= 0.5) {
|
|
99
|
+
base += 3;
|
|
100
|
+
} else if (ratio >= 0.9) {
|
|
101
|
+
base -= 5;
|
|
102
|
+
}
|
|
103
|
+
return Math.max(65, Math.min(80, Math.round(base)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatDifference(diff: number): string {
|
|
107
|
+
return formatCurrency(diff); // diff already absolute; formatCurrency handles sign
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface CombinationResult {
|
|
111
|
+
matches: TransactionMatch[];
|
|
112
|
+
insights: ReconciliationInsight[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function findCombinationMatches(
|
|
116
|
+
unmatchedBank: BankTransaction[],
|
|
117
|
+
unmatchedYNAB: YNABTransaction[],
|
|
118
|
+
config: MatchingConfig,
|
|
119
|
+
): CombinationResult {
|
|
120
|
+
if (!ENABLE_COMBINATION_MATCHING || unmatchedBank.length === 0 || unmatchedYNAB.length === 0) {
|
|
121
|
+
return { matches: [], insights: [] };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const tolerance = amountTolerance(config);
|
|
125
|
+
const toleranceDays = dateTolerance(config);
|
|
126
|
+
|
|
127
|
+
const matches: TransactionMatch[] = [];
|
|
128
|
+
const insights: ReconciliationInsight[] = [];
|
|
129
|
+
const seenCombinations = new Set<string>();
|
|
130
|
+
|
|
131
|
+
for (const bankTxn of unmatchedBank) {
|
|
132
|
+
const viableYnab = unmatchedYNAB.filter((txn) => hasMatchingSign(bankTxn.amount, [txn]));
|
|
133
|
+
if (viableYnab.length < 2) continue;
|
|
134
|
+
|
|
135
|
+
const evaluated: { txns: YNABTransaction[]; diff: number; sum: number }[] = [];
|
|
136
|
+
|
|
137
|
+
const addIfValid = (combo: YNABTransaction[]) => {
|
|
138
|
+
const sum = combo.reduce((acc, txn) => acc + toDollars(txn.amount), 0);
|
|
139
|
+
const diff = Math.abs(sum - bankTxn.amount);
|
|
140
|
+
if (diff > tolerance) return;
|
|
141
|
+
if (!withinDateTolerance(bankTxn.date, combo, toleranceDays)) return;
|
|
142
|
+
if (!hasMatchingSign(bankTxn.amount, combo)) return;
|
|
143
|
+
evaluated.push({ txns: combo, diff, sum });
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const n = viableYnab.length;
|
|
147
|
+
for (let i = 0; i < n - 1; i++) {
|
|
148
|
+
for (let j = i + 1; j < n; j++) {
|
|
149
|
+
addIfValid([viableYnab[i]!, viableYnab[j]!]);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (n >= 3) {
|
|
154
|
+
for (let i = 0; i < n - 2; i++) {
|
|
155
|
+
for (let j = i + 1; j < n - 1; j++) {
|
|
156
|
+
for (let k = j + 1; k < n; k++) {
|
|
157
|
+
addIfValid([viableYnab[i]!, viableYnab[j]!, viableYnab[k]!]);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (evaluated.length === 0) continue;
|
|
164
|
+
|
|
165
|
+
evaluated.sort((a, b) => a.diff - b.diff);
|
|
166
|
+
const recordedSizes = new Set<number>();
|
|
167
|
+
|
|
168
|
+
for (const combo of evaluated) {
|
|
169
|
+
if (recordedSizes.has(combo.txns.length)) continue; // surface best per size
|
|
170
|
+
const comboIds = combo.txns.map((txn) => txn.id).sort();
|
|
171
|
+
const key = `${bankTxn.id}|${comboIds.join('+')}`;
|
|
172
|
+
if (seenCombinations.has(key)) continue;
|
|
173
|
+
seenCombinations.add(key);
|
|
174
|
+
recordedSizes.add(combo.txns.length);
|
|
175
|
+
|
|
176
|
+
const score = computeCombinationConfidence(combo.diff, tolerance, combo.txns.length);
|
|
177
|
+
const candidateConfidence = Math.max(60, score - 5);
|
|
178
|
+
const descriptionTotal = formatCurrency(combo.sum);
|
|
179
|
+
const diffLabel = formatDifference(combo.diff);
|
|
180
|
+
|
|
181
|
+
matches.push({
|
|
182
|
+
bank_transaction: bankTxn,
|
|
183
|
+
confidence: 'medium',
|
|
184
|
+
confidence_score: score,
|
|
185
|
+
match_reason: 'combination_match',
|
|
186
|
+
top_confidence: score,
|
|
187
|
+
candidates: combo.txns.map((txn) => ({
|
|
188
|
+
ynab_transaction: txn,
|
|
189
|
+
confidence: candidateConfidence,
|
|
190
|
+
match_reason: 'combination_component',
|
|
191
|
+
explanation: `Part of combination totaling ${descriptionTotal} (difference ${diffLabel}).`,
|
|
192
|
+
})),
|
|
193
|
+
action_hint: 'review_combination',
|
|
194
|
+
recommendation:
|
|
195
|
+
`Combination of ${combo.txns.length} YNAB transactions totals ${descriptionTotal} versus ` +
|
|
196
|
+
`${formatCurrency(bankTxn.amount)} on the bank statement.`,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const insightId = `combination-${bankTxn.id}-${comboIds.join('+')}`;
|
|
200
|
+
insights.push({
|
|
201
|
+
id: insightId,
|
|
202
|
+
type: 'combination_match' as unknown as ReconciliationInsight['type'],
|
|
203
|
+
severity: 'info',
|
|
204
|
+
title: `Combination of ${combo.txns.length} transactions matches ${formatCurrency(
|
|
205
|
+
bankTxn.amount,
|
|
206
|
+
)}`,
|
|
207
|
+
description:
|
|
208
|
+
`${combo.txns.length} YNAB transactions totaling ${descriptionTotal} align with ` +
|
|
209
|
+
`${formatCurrency(bankTxn.amount)} from ${bankTxn.payee}. Difference ${diffLabel}.`,
|
|
210
|
+
evidence: {
|
|
211
|
+
bank_transaction_id: bankTxn.id,
|
|
212
|
+
bank_amount: bankTxn.amount,
|
|
213
|
+
ynab_transaction_ids: comboIds,
|
|
214
|
+
ynab_amounts_milliunits: combo.txns.map((txn) => txn.amount),
|
|
215
|
+
combination_size: combo.txns.length,
|
|
216
|
+
difference: combo.diff,
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { matches, insights };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
type ParserResult =
|
|
226
|
+
| {
|
|
227
|
+
transactions: unknown[];
|
|
228
|
+
format_detected?: string;
|
|
229
|
+
delimiter?: string;
|
|
230
|
+
total_rows?: number;
|
|
231
|
+
valid_rows?: number;
|
|
232
|
+
errors?: string[];
|
|
233
|
+
}
|
|
234
|
+
| unknown[];
|
|
235
|
+
|
|
236
|
+
function isParsedCSVData(
|
|
237
|
+
result: ParserResult,
|
|
238
|
+
): result is Extract<ParserResult, { transactions: unknown[] }> {
|
|
239
|
+
return (
|
|
240
|
+
typeof result === 'object' &&
|
|
241
|
+
result !== null &&
|
|
242
|
+
!Array.isArray(result) &&
|
|
243
|
+
'transactions' in result
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeDate(value: unknown): string {
|
|
248
|
+
if (value instanceof Date) {
|
|
249
|
+
return value.toISOString().split('T')[0]!;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (typeof value === 'string') {
|
|
253
|
+
const trimmed = value.trim();
|
|
254
|
+
if (!trimmed) return trimmed;
|
|
255
|
+
|
|
256
|
+
const parsed = new Date(trimmed);
|
|
257
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
258
|
+
return parsed.toISOString().split('T')[0]!;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return trimmed;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return new Date().toISOString().split('T')[0]!;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeAmount(record: Record<string, unknown>): number {
|
|
268
|
+
const raw = record['amount'];
|
|
269
|
+
|
|
270
|
+
if (typeof raw === 'number') {
|
|
271
|
+
if (record['date'] instanceof Date || 'raw_amount' in record || 'raw_date' in record) {
|
|
272
|
+
return Math.round(raw) / 1000;
|
|
273
|
+
}
|
|
274
|
+
return raw;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (typeof raw === 'string') {
|
|
278
|
+
const cleaned = raw.replace(/[$,\s]/g, '');
|
|
279
|
+
const parsed = Number.parseFloat(cleaned);
|
|
280
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizePayee(record: Record<string, unknown>): string {
|
|
287
|
+
const candidates = [record['payee'], record['description'], record['memo']];
|
|
288
|
+
for (const candidate of candidates) {
|
|
289
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
290
|
+
return candidate.trim();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return 'Unknown Payee';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function determineRow(record: Record<string, unknown>, index: number): number {
|
|
297
|
+
if (typeof record['original_csv_row'] === 'number') {
|
|
298
|
+
return record['original_csv_row'];
|
|
299
|
+
}
|
|
300
|
+
if (typeof record['row_number'] === 'number') {
|
|
301
|
+
return record['row_number'];
|
|
302
|
+
}
|
|
303
|
+
return index + 1;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function convertParserRecord(record: unknown, index: number): BankTransaction {
|
|
307
|
+
const data =
|
|
308
|
+
typeof record === 'object' && record !== null ? (record as Record<string, unknown>) : {};
|
|
309
|
+
|
|
310
|
+
const dateValue = normalizeDate(data['date']);
|
|
311
|
+
const amountValue = normalizeAmount(data);
|
|
312
|
+
const payeeValue = normalizePayee(data);
|
|
313
|
+
const memoValue =
|
|
314
|
+
typeof data['memo'] === 'string' && data['memo'].trim() ? data['memo'].trim() : undefined;
|
|
315
|
+
const originalRow = determineRow(data, index);
|
|
316
|
+
|
|
317
|
+
const transaction: BankTransaction = {
|
|
318
|
+
id: randomUUID(),
|
|
319
|
+
date: dateValue,
|
|
320
|
+
amount: amountValue,
|
|
321
|
+
payee: payeeValue,
|
|
322
|
+
original_csv_row: originalRow,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
if (memoValue !== undefined) {
|
|
326
|
+
transaction.memo = memoValue;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return transaction;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function parseBankStatement(csvContent: string, csvFilePath?: string): BankTransaction[] {
|
|
333
|
+
const content = csvFilePath ? bankParser.readCSVFile(csvFilePath) : csvContent;
|
|
334
|
+
|
|
335
|
+
let format: ParserCSVFormat = FALLBACK_CSV_FORMAT;
|
|
336
|
+
let autoDetect: ((content: string) => ParserCSVFormat) | undefined;
|
|
337
|
+
try {
|
|
338
|
+
autoDetect = (bankParser as { autoDetectCSVFormat?: (content: string) => ParserCSVFormat })
|
|
339
|
+
.autoDetectCSVFormat;
|
|
340
|
+
} catch {
|
|
341
|
+
autoDetect = undefined;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (typeof autoDetect === 'function') {
|
|
345
|
+
try {
|
|
346
|
+
format = autoDetect(content);
|
|
347
|
+
} catch {
|
|
348
|
+
format = FALLBACK_CSV_FORMAT;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const rawResult = bankParser.parseBankCSV(content, format) as unknown as ParserResult;
|
|
353
|
+
const records = isParsedCSVData(rawResult) ? rawResult.transactions : rawResult;
|
|
354
|
+
|
|
355
|
+
return records.map(convertParserRecord);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Categorize matches by confidence level
|
|
360
|
+
*/
|
|
361
|
+
function categorizeMatches(matches: TransactionMatch[]): {
|
|
362
|
+
autoMatches: TransactionMatch[];
|
|
363
|
+
suggestedMatches: TransactionMatch[];
|
|
364
|
+
unmatchedBank: BankTransaction[];
|
|
365
|
+
} {
|
|
366
|
+
const autoMatches: TransactionMatch[] = [];
|
|
367
|
+
const suggestedMatches: TransactionMatch[] = [];
|
|
368
|
+
const unmatchedBank: BankTransaction[] = [];
|
|
369
|
+
|
|
370
|
+
for (const match of matches) {
|
|
371
|
+
if (match.confidence === 'high') {
|
|
372
|
+
autoMatches.push(match);
|
|
373
|
+
} else if (match.confidence === 'medium') {
|
|
374
|
+
suggestedMatches.push(match);
|
|
375
|
+
} else {
|
|
376
|
+
// low or none confidence
|
|
377
|
+
unmatchedBank.push(match.bank_transaction);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return { autoMatches, suggestedMatches, unmatchedBank };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Find unmatched YNAB transactions
|
|
386
|
+
* These are transactions in YNAB that don't appear on the bank statement
|
|
387
|
+
*/
|
|
388
|
+
function findUnmatchedYNAB(
|
|
389
|
+
ynabTransactions: YNABTransaction[],
|
|
390
|
+
matches: TransactionMatch[],
|
|
391
|
+
): YNABTransaction[] {
|
|
392
|
+
const matchedIds = new Set<string>();
|
|
393
|
+
|
|
394
|
+
for (const match of matches) {
|
|
395
|
+
if (match.ynab_transaction) {
|
|
396
|
+
matchedIds.add(match.ynab_transaction.id);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return ynabTransactions.filter((txn) => !matchedIds.has(txn.id));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Calculate balance information
|
|
405
|
+
*/
|
|
406
|
+
function calculateBalances(
|
|
407
|
+
ynabTransactions: YNABTransaction[],
|
|
408
|
+
statementBalance: number,
|
|
409
|
+
currency: string,
|
|
410
|
+
): BalanceInfo {
|
|
411
|
+
let clearedBalance = 0;
|
|
412
|
+
let unclearedBalance = 0;
|
|
413
|
+
|
|
414
|
+
for (const txn of ynabTransactions) {
|
|
415
|
+
const amount = txn.amount / 1000; // Convert from milliunits to dollars
|
|
416
|
+
|
|
417
|
+
if (txn.cleared === 'cleared' || txn.cleared === 'reconciled') {
|
|
418
|
+
clearedBalance += amount;
|
|
419
|
+
} else {
|
|
420
|
+
unclearedBalance += amount;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const totalBalance = clearedBalance + unclearedBalance;
|
|
425
|
+
const discrepancy = clearedBalance - statementBalance;
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
current_cleared: toMoneyValueFromDecimal(clearedBalance, currency),
|
|
429
|
+
current_uncleared: toMoneyValueFromDecimal(unclearedBalance, currency),
|
|
430
|
+
current_total: toMoneyValueFromDecimal(totalBalance, currency),
|
|
431
|
+
target_statement: toMoneyValueFromDecimal(statementBalance, currency),
|
|
432
|
+
discrepancy: toMoneyValueFromDecimal(discrepancy, currency),
|
|
433
|
+
on_track: Math.abs(discrepancy) < 0.01, // Within 1 cent
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Generate reconciliation summary
|
|
439
|
+
*/
|
|
440
|
+
function generateSummary(
|
|
441
|
+
bankTransactions: BankTransaction[],
|
|
442
|
+
ynabTransactions: YNABTransaction[],
|
|
443
|
+
autoMatches: TransactionMatch[],
|
|
444
|
+
suggestedMatches: TransactionMatch[],
|
|
445
|
+
unmatchedBank: BankTransaction[],
|
|
446
|
+
unmatchedYNAB: YNABTransaction[],
|
|
447
|
+
balances: BalanceInfo,
|
|
448
|
+
): ReconciliationSummary {
|
|
449
|
+
// Determine date range from bank transactions
|
|
450
|
+
const dates = bankTransactions.map((t) => t.date).sort();
|
|
451
|
+
const dateRange = dates.length > 0 ? `${dates[0]} to ${dates[dates.length - 1]}` : 'Unknown';
|
|
452
|
+
|
|
453
|
+
// Build discrepancy explanation
|
|
454
|
+
let discrepancyExplanation = '';
|
|
455
|
+
if (balances.on_track) {
|
|
456
|
+
discrepancyExplanation = 'Cleared balance matches statement';
|
|
457
|
+
} else {
|
|
458
|
+
const actionsNeeded: string[] = [];
|
|
459
|
+
if (autoMatches.length > 0) {
|
|
460
|
+
actionsNeeded.push(`clear ${autoMatches.length} transactions`);
|
|
461
|
+
}
|
|
462
|
+
if (unmatchedBank.length > 0) {
|
|
463
|
+
actionsNeeded.push(`add ${unmatchedBank.length} missing`);
|
|
464
|
+
}
|
|
465
|
+
if (unmatchedYNAB.length > 0) {
|
|
466
|
+
actionsNeeded.push(`review ${unmatchedYNAB.length} unmatched YNAB`);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
discrepancyExplanation =
|
|
470
|
+
actionsNeeded.length > 0 ? `Need to ${actionsNeeded.join(', ')}` : 'Manual review required';
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
statement_date_range: dateRange,
|
|
475
|
+
bank_transactions_count: bankTransactions.length,
|
|
476
|
+
ynab_transactions_count: ynabTransactions.length,
|
|
477
|
+
auto_matched: autoMatches.length,
|
|
478
|
+
suggested_matches: suggestedMatches.length,
|
|
479
|
+
unmatched_bank: unmatchedBank.length,
|
|
480
|
+
unmatched_ynab: unmatchedYNAB.length,
|
|
481
|
+
current_cleared_balance: balances.current_cleared,
|
|
482
|
+
target_statement_balance: balances.target_statement,
|
|
483
|
+
discrepancy: balances.discrepancy,
|
|
484
|
+
discrepancy_explanation: discrepancyExplanation,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Generate next steps for user
|
|
490
|
+
*/
|
|
491
|
+
function generateNextSteps(summary: ReconciliationSummary): string[] {
|
|
492
|
+
const steps: string[] = [];
|
|
493
|
+
|
|
494
|
+
if (summary.auto_matched > 0) {
|
|
495
|
+
steps.push(`Review ${summary.auto_matched} auto-matched transactions for approval`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (summary.suggested_matches > 0) {
|
|
499
|
+
steps.push(`Review ${summary.suggested_matches} suggested matches and choose best match`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (summary.unmatched_bank > 0) {
|
|
503
|
+
steps.push(`Decide whether to add ${summary.unmatched_bank} missing bank transactions to YNAB`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (summary.unmatched_ynab > 0) {
|
|
507
|
+
steps.push(
|
|
508
|
+
`Decide what to do with ${summary.unmatched_ynab} unmatched YNAB transactions (unclear/delete/ignore)`,
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (steps.length === 0) {
|
|
513
|
+
steps.push('All transactions matched! Review and approve to complete reconciliation');
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return steps;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function formatCurrency(amount: number): string {
|
|
520
|
+
const formatter = new Intl.NumberFormat('en-US', {
|
|
521
|
+
style: 'currency',
|
|
522
|
+
currency: 'USD',
|
|
523
|
+
minimumFractionDigits: 2,
|
|
524
|
+
maximumFractionDigits: 2,
|
|
525
|
+
});
|
|
526
|
+
return formatter.format(amount);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function repeatAmountInsights(unmatchedBank: BankTransaction[]): ReconciliationInsight[] {
|
|
530
|
+
const insights: ReconciliationInsight[] = [];
|
|
531
|
+
if (unmatchedBank.length === 0) {
|
|
532
|
+
return insights;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const frequency = new Map<string, { amount: number; txns: BankTransaction[] }>();
|
|
536
|
+
|
|
537
|
+
for (const txn of unmatchedBank) {
|
|
538
|
+
const key = txn.amount.toFixed(2);
|
|
539
|
+
const entry = frequency.get(key) ?? { amount: txn.amount, txns: [] };
|
|
540
|
+
entry.txns.push(txn);
|
|
541
|
+
frequency.set(key, entry);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const repeated = Array.from(frequency.values())
|
|
545
|
+
.filter((entry) => entry.txns.length >= 2)
|
|
546
|
+
.sort((a, b) => b.txns.length - a.txns.length);
|
|
547
|
+
|
|
548
|
+
if (repeated.length === 0) {
|
|
549
|
+
return insights;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const top = repeated[0]!;
|
|
553
|
+
insights.push({
|
|
554
|
+
id: `repeat-${top.amount.toFixed(2)}`,
|
|
555
|
+
type: 'repeat_amount',
|
|
556
|
+
severity: top.txns.length >= 4 ? 'critical' : 'warning',
|
|
557
|
+
title: `${top.txns.length} unmatched transactions at ${formatCurrency(top.amount)}`,
|
|
558
|
+
description:
|
|
559
|
+
`The bank statement shows ${top.txns.length} unmatched transaction(s) at ${formatCurrency(top.amount)}. ` +
|
|
560
|
+
'Repeated amounts are usually the quickest wins — reconcile these first.',
|
|
561
|
+
evidence: {
|
|
562
|
+
amount: top.amount,
|
|
563
|
+
occurrences: top.txns.length,
|
|
564
|
+
dates: top.txns.map((txn) => txn.date),
|
|
565
|
+
csv_rows: top.txns.map((txn) => txn.original_csv_row),
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
return insights;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function nearMatchInsights(
|
|
573
|
+
matches: TransactionMatch[],
|
|
574
|
+
config: MatchingConfig,
|
|
575
|
+
): ReconciliationInsight[] {
|
|
576
|
+
const insights: ReconciliationInsight[] = [];
|
|
577
|
+
|
|
578
|
+
for (const match of matches) {
|
|
579
|
+
if (!match.candidates || match.candidates.length === 0) continue;
|
|
580
|
+
if (match.confidence === 'high') continue;
|
|
581
|
+
|
|
582
|
+
const topCandidate = match.candidates[0]!;
|
|
583
|
+
const score = topCandidate.confidence;
|
|
584
|
+
const highSignal =
|
|
585
|
+
(match.confidence === 'medium' && score >= config.autoMatchThreshold - 5) ||
|
|
586
|
+
(match.confidence === 'low' && score >= config.suggestionThreshold) ||
|
|
587
|
+
(match.confidence === 'none' && score >= config.suggestionThreshold);
|
|
588
|
+
|
|
589
|
+
if (!highSignal) continue;
|
|
590
|
+
|
|
591
|
+
const bankTxn = match.bank_transaction;
|
|
592
|
+
const ynabTxn = topCandidate.ynab_transaction;
|
|
593
|
+
|
|
594
|
+
insights.push({
|
|
595
|
+
id: `near-${bankTxn.id}`,
|
|
596
|
+
type: 'near_match',
|
|
597
|
+
severity: score >= config.autoMatchThreshold ? 'warning' : 'info',
|
|
598
|
+
title: `${formatCurrency(bankTxn.amount)} nearly matches ${formatCurrency(ynabTxn.amount / 1000)}`,
|
|
599
|
+
description:
|
|
600
|
+
`Bank transaction on ${bankTxn.date} (${formatCurrency(bankTxn.amount)}) nearly matches ` +
|
|
601
|
+
`${ynabTxn.payee_name ?? 'unknown payee'} on ${ynabTxn.date}. Confidence ${score}% — review and confirm.`,
|
|
602
|
+
evidence: {
|
|
603
|
+
bank_transaction: {
|
|
604
|
+
id: bankTxn.id,
|
|
605
|
+
date: bankTxn.date,
|
|
606
|
+
amount: bankTxn.amount,
|
|
607
|
+
payee: bankTxn.payee,
|
|
608
|
+
},
|
|
609
|
+
candidate: {
|
|
610
|
+
id: ynabTxn.id,
|
|
611
|
+
date: ynabTxn.date,
|
|
612
|
+
amount_milliunits: ynabTxn.amount,
|
|
613
|
+
payee_name: ynabTxn.payee_name,
|
|
614
|
+
confidence: score,
|
|
615
|
+
reasons: topCandidate.match_reason,
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return insights.slice(0, 3);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function anomalyInsights(
|
|
625
|
+
summary: ReconciliationSummary,
|
|
626
|
+
balances: BalanceInfo,
|
|
627
|
+
): ReconciliationInsight[] {
|
|
628
|
+
const insights: ReconciliationInsight[] = [];
|
|
629
|
+
const discrepancyAbs = Math.abs(balances.discrepancy.value);
|
|
630
|
+
|
|
631
|
+
if (discrepancyAbs >= 1) {
|
|
632
|
+
insights.push({
|
|
633
|
+
id: 'balance-gap',
|
|
634
|
+
type: 'anomaly',
|
|
635
|
+
severity: discrepancyAbs >= 100 ? 'critical' : 'warning',
|
|
636
|
+
title: `Cleared balance off by ${balances.discrepancy.value_display}`,
|
|
637
|
+
description:
|
|
638
|
+
`YNAB cleared balance is ${balances.current_cleared.value_display} but the statement expects ` +
|
|
639
|
+
`${balances.target_statement.value_display}. Focus on closing this gap.`,
|
|
640
|
+
evidence: {
|
|
641
|
+
cleared_balance: balances.current_cleared,
|
|
642
|
+
statement_balance: balances.target_statement,
|
|
643
|
+
discrepancy: balances.discrepancy,
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (summary.unmatched_bank >= 5) {
|
|
649
|
+
insights.push({
|
|
650
|
+
id: 'bulk-missing-bank',
|
|
651
|
+
type: 'anomaly',
|
|
652
|
+
severity: summary.unmatched_bank >= 10 ? 'critical' : 'warning',
|
|
653
|
+
title: `${summary.unmatched_bank} bank transactions still unmatched`,
|
|
654
|
+
description:
|
|
655
|
+
`There are ${summary.unmatched_bank} bank transactions without a match. ` +
|
|
656
|
+
'Consider bulk importing or reviewing by date sequence.',
|
|
657
|
+
evidence: {
|
|
658
|
+
unmatched_bank: summary.unmatched_bank,
|
|
659
|
+
},
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return insights;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function detectInsights(
|
|
667
|
+
matches: TransactionMatch[],
|
|
668
|
+
unmatchedBank: BankTransaction[],
|
|
669
|
+
summary: ReconciliationSummary,
|
|
670
|
+
balances: BalanceInfo,
|
|
671
|
+
config: MatchingConfig,
|
|
672
|
+
): ReconciliationInsight[] {
|
|
673
|
+
const insights: ReconciliationInsight[] = [];
|
|
674
|
+
const seen = new Set<string>();
|
|
675
|
+
|
|
676
|
+
const addUnique = (insight: ReconciliationInsight) => {
|
|
677
|
+
if (seen.has(insight.id)) return;
|
|
678
|
+
seen.add(insight.id);
|
|
679
|
+
insights.push(insight);
|
|
680
|
+
};
|
|
681
|
+
|
|
682
|
+
for (const insight of repeatAmountInsights(unmatchedBank)) {
|
|
683
|
+
addUnique(insight);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
for (const insight of nearMatchInsights(matches, config)) {
|
|
687
|
+
addUnique(insight);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
for (const insight of anomalyInsights(summary, balances)) {
|
|
691
|
+
addUnique(insight);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
return insights.slice(0, 5);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function mergeInsights(
|
|
698
|
+
base: ReconciliationInsight[],
|
|
699
|
+
additional: ReconciliationInsight[],
|
|
700
|
+
): ReconciliationInsight[] {
|
|
701
|
+
if (additional.length === 0) {
|
|
702
|
+
return base;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const seen = new Set(base.map((insight) => insight.id));
|
|
706
|
+
const merged = [...base];
|
|
707
|
+
|
|
708
|
+
for (const insight of additional) {
|
|
709
|
+
if (seen.has(insight.id)) continue;
|
|
710
|
+
seen.add(insight.id);
|
|
711
|
+
merged.push(insight);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return merged.slice(0, 5);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Perform reconciliation analysis
|
|
719
|
+
*
|
|
720
|
+
* @param csvContent - CSV file content or file path
|
|
721
|
+
* @param csvFilePath - Optional file path (if csvContent is a path)
|
|
722
|
+
* @param ynabTransactions - YNAB transactions from API
|
|
723
|
+
* @param statementBalance - Expected cleared balance from statement
|
|
724
|
+
* @param config - Matching configuration
|
|
725
|
+
* @param currency - Currency code (default: USD)
|
|
726
|
+
* @param accountId - Account ID for recommendation context
|
|
727
|
+
* @param budgetId - Budget ID for recommendation context
|
|
728
|
+
* @param invertBankAmounts - Whether to invert bank transaction amounts (for banks that show charges as positive)
|
|
729
|
+
*/
|
|
730
|
+
export function analyzeReconciliation(
|
|
731
|
+
csvContent: string,
|
|
732
|
+
csvFilePath: string | undefined,
|
|
733
|
+
ynabTransactions: ynab.TransactionDetail[],
|
|
734
|
+
statementBalance: number,
|
|
735
|
+
config: MatchingConfig = DEFAULT_MATCHING_CONFIG as MatchingConfig,
|
|
736
|
+
currency: string = 'USD',
|
|
737
|
+
accountId?: string,
|
|
738
|
+
budgetId?: string,
|
|
739
|
+
invertBankAmounts: boolean = false,
|
|
740
|
+
): ReconciliationAnalysis {
|
|
741
|
+
// Step 1: Parse bank CSV
|
|
742
|
+
let bankTransactions = parseBankStatement(csvContent, csvFilePath);
|
|
743
|
+
|
|
744
|
+
// Step 1b: Optionally invert bank transaction amounts
|
|
745
|
+
// Some banks show charges as positive (need inversion to match YNAB's negative convention)
|
|
746
|
+
// Other banks (e.g., Wealthsimple) show charges as negative already (no inversion needed)
|
|
747
|
+
if (invertBankAmounts) {
|
|
748
|
+
bankTransactions = bankTransactions.map((txn) => ({
|
|
749
|
+
...txn,
|
|
750
|
+
amount: -txn.amount,
|
|
751
|
+
}));
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Step 2: Convert YNAB transactions
|
|
755
|
+
const convertedYNABTxns = ynabTransactions.map(convertYNABTransaction);
|
|
756
|
+
|
|
757
|
+
// Step 3: Run matching algorithm
|
|
758
|
+
const matches = findMatches(bankTransactions, convertedYNABTxns, config);
|
|
759
|
+
|
|
760
|
+
// Step 4: Categorize matches
|
|
761
|
+
const { autoMatches, suggestedMatches, unmatchedBank } = categorizeMatches(matches);
|
|
762
|
+
|
|
763
|
+
// Step 5: Find unmatched YNAB transactions
|
|
764
|
+
const unmatchedYNAB = findUnmatchedYNAB(convertedYNABTxns, matches);
|
|
765
|
+
|
|
766
|
+
let combinationMatches: TransactionMatch[] = [];
|
|
767
|
+
let combinationInsights: ReconciliationInsight[] = [];
|
|
768
|
+
|
|
769
|
+
if (ENABLE_COMBINATION_MATCHING) {
|
|
770
|
+
const combinationResult = findCombinationMatches(unmatchedBank, unmatchedYNAB, config);
|
|
771
|
+
combinationMatches = combinationResult.matches;
|
|
772
|
+
combinationInsights = combinationResult.insights;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const enrichedSuggestedMatches = [...suggestedMatches, ...combinationMatches];
|
|
776
|
+
|
|
777
|
+
// Step 6: Calculate balances
|
|
778
|
+
const balances = calculateBalances(convertedYNABTxns, statementBalance, currency);
|
|
779
|
+
|
|
780
|
+
// Step 7: Generate summary
|
|
781
|
+
const summary = generateSummary(
|
|
782
|
+
bankTransactions,
|
|
783
|
+
convertedYNABTxns,
|
|
784
|
+
autoMatches,
|
|
785
|
+
enrichedSuggestedMatches,
|
|
786
|
+
unmatchedBank,
|
|
787
|
+
unmatchedYNAB,
|
|
788
|
+
balances,
|
|
789
|
+
);
|
|
790
|
+
|
|
791
|
+
// Step 8: Generate next steps
|
|
792
|
+
const nextSteps = generateNextSteps(summary);
|
|
793
|
+
|
|
794
|
+
// Step 9: Detect insights and patterns
|
|
795
|
+
const baseInsights = detectInsights(matches, unmatchedBank, summary, balances, config);
|
|
796
|
+
const insights = mergeInsights(baseInsights, combinationInsights);
|
|
797
|
+
|
|
798
|
+
// Step 10: Build the analysis result
|
|
799
|
+
const analysis: ReconciliationAnalysis = {
|
|
800
|
+
success: true,
|
|
801
|
+
phase: 'analysis',
|
|
802
|
+
summary,
|
|
803
|
+
auto_matches: autoMatches,
|
|
804
|
+
suggested_matches: enrichedSuggestedMatches,
|
|
805
|
+
unmatched_bank: unmatchedBank,
|
|
806
|
+
unmatched_ynab: unmatchedYNAB,
|
|
807
|
+
balance_info: balances,
|
|
808
|
+
next_steps: nextSteps,
|
|
809
|
+
insights,
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
// Step 11: Generate recommendations (if account and budget IDs are provided)
|
|
813
|
+
if (accountId && budgetId) {
|
|
814
|
+
const recommendations = generateRecommendations({
|
|
815
|
+
account_id: accountId,
|
|
816
|
+
budget_id: budgetId,
|
|
817
|
+
analysis,
|
|
818
|
+
matching_config: config,
|
|
819
|
+
});
|
|
820
|
+
analysis.recommendations = recommendations;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
return analysis;
|
|
824
|
+
}
|