@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,557 @@
|
|
|
1
|
+
import { parse } from 'csv-parse/sync';
|
|
2
|
+
import { parse as parseDateFns } from 'date-fns';
|
|
3
|
+
import { toMilli } from '../../utils/money.js';
|
|
4
|
+
import type { Milli } from '../../utils/money.js';
|
|
5
|
+
import { BankTransaction, CSVFormat } from './types.js';
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse date string using date-fns for better reliability
|
|
10
|
+
*/
|
|
11
|
+
export function parseDate(dateStr: string, format: string): Date {
|
|
12
|
+
const cleanDate = dateStr.trim();
|
|
13
|
+
|
|
14
|
+
// Map our format strings to date-fns format patterns
|
|
15
|
+
const formatMap: Record<string, string> = {
|
|
16
|
+
'MM/DD/YYYY': 'MM/dd/yyyy',
|
|
17
|
+
'M/D/YYYY': 'M/d/yyyy',
|
|
18
|
+
'DD/MM/YYYY': 'dd/MM/yyyy',
|
|
19
|
+
'D/M/YYYY': 'd/M/yyyy',
|
|
20
|
+
'YYYY-MM-DD': 'yyyy-MM-dd',
|
|
21
|
+
'MM-DD-YYYY': 'MM-dd-yyyy',
|
|
22
|
+
'MMM dd, yyyy': 'MMM dd, yyyy',
|
|
23
|
+
'MMM d, yyyy': 'MMM d, yyyy',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const dateFnsFormat = formatMap[format];
|
|
27
|
+
if (dateFnsFormat) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = parseDateFns(cleanDate, dateFnsFormat, new Date());
|
|
30
|
+
if (!isNaN(parsed.getTime())) {
|
|
31
|
+
return parsed;
|
|
32
|
+
}
|
|
33
|
+
} catch {
|
|
34
|
+
// Fall through to generic parsing
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback to native Date parsing for any unrecognized formats
|
|
39
|
+
const parsed = new Date(cleanDate);
|
|
40
|
+
if (isNaN(parsed.getTime())) {
|
|
41
|
+
throw new Error(`Unable to parse date: ${dateStr} with format: ${format}`);
|
|
42
|
+
}
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Convert dollar amount to milliunits
|
|
48
|
+
*/
|
|
49
|
+
export function amountToMilliunits(amountStr: string): Milli {
|
|
50
|
+
const cleaned = amountStr.replace(/[$,\s]/g, '').trim();
|
|
51
|
+
let s = cleaned,
|
|
52
|
+
neg = false;
|
|
53
|
+
if (s.startsWith('(') && s.endsWith(')')) {
|
|
54
|
+
neg = true;
|
|
55
|
+
s = s.slice(1, -1);
|
|
56
|
+
}
|
|
57
|
+
if (s.startsWith('+')) s = s.slice(1);
|
|
58
|
+
|
|
59
|
+
const n = Number(s);
|
|
60
|
+
if (isNaN(n) || !isFinite(n)) {
|
|
61
|
+
throw new Error(`Invalid amount value: "${amountStr}" (cleaned: "${s}")`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return toMilli(neg ? -n : n);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a string looks like a date
|
|
69
|
+
*/
|
|
70
|
+
function isDateLike(str: string): boolean {
|
|
71
|
+
if (!str) return false;
|
|
72
|
+
// Common date patterns
|
|
73
|
+
const datePatterns = [
|
|
74
|
+
/^\d{1,2}\/\d{1,2}\/\d{4}$/, // MM/DD/YYYY
|
|
75
|
+
/^\d{4}-\d{1,2}-\d{1,2}$/, // YYYY-MM-DD
|
|
76
|
+
/^\d{1,2}-\d{1,2}-\d{4}$/, // MM-DD-YYYY
|
|
77
|
+
/^[A-Za-z]{3}\s+\d{1,2},\s+\d{4}$/, // MMM dd, yyyy (e.g., "Sep 18, 2025")
|
|
78
|
+
];
|
|
79
|
+
return datePatterns.some((pattern) => pattern.test(str.trim()));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Detect date format from a sample date string
|
|
84
|
+
*/
|
|
85
|
+
export function detectDateFormat(dateStr: string | undefined): string {
|
|
86
|
+
if (!dateStr) return 'MM/DD/YYYY';
|
|
87
|
+
const cleaned = dateStr.trim();
|
|
88
|
+
|
|
89
|
+
if (cleaned.includes('/')) {
|
|
90
|
+
return 'MM/DD/YYYY';
|
|
91
|
+
} else if (cleaned.includes('-')) {
|
|
92
|
+
if (/^\d{4}-/.test(cleaned)) {
|
|
93
|
+
return 'YYYY-MM-DD';
|
|
94
|
+
} else {
|
|
95
|
+
return 'MM-DD-YYYY';
|
|
96
|
+
}
|
|
97
|
+
} else if (/^[A-Za-z]{3}\s+\d{1,2},\s+\d{4}$/.test(cleaned)) {
|
|
98
|
+
// Detect "Sep 18, 2025" format
|
|
99
|
+
return 'MMM dd, yyyy';
|
|
100
|
+
}
|
|
101
|
+
return 'MM/DD/YYYY';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Detect the most likely delimiter by evaluating candidates across sample lines
|
|
106
|
+
*/
|
|
107
|
+
function detectDelimiter(lines: string[]): string {
|
|
108
|
+
const candidates = [',', ';', '\t', '|'];
|
|
109
|
+
const sampleLines = lines.slice(0, 3).filter((line) => line.trim()); // Use first 2-3 non-empty lines
|
|
110
|
+
|
|
111
|
+
if (sampleLines.length === 0) {
|
|
112
|
+
return ','; // Default fallback
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let bestDelimiter = ',';
|
|
116
|
+
let bestScore = -1;
|
|
117
|
+
|
|
118
|
+
for (const delimiter of candidates) {
|
|
119
|
+
let score = 0;
|
|
120
|
+
const columnCounts: number[] = [];
|
|
121
|
+
let parseFailed = false;
|
|
122
|
+
|
|
123
|
+
for (const line of sampleLines) {
|
|
124
|
+
try {
|
|
125
|
+
const rows = parse(line, {
|
|
126
|
+
delimiter,
|
|
127
|
+
quote: '"',
|
|
128
|
+
escape: '"',
|
|
129
|
+
skip_empty_lines: true,
|
|
130
|
+
trim: true,
|
|
131
|
+
relax_column_count: true,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// rows should be an array with one row (since we're parsing one line)
|
|
135
|
+
if (rows && rows.length > 0 && rows[0]) {
|
|
136
|
+
const columns = Array.isArray(rows[0]) ? rows[0] : Object.values(rows[0]);
|
|
137
|
+
columnCounts.push(columns.length);
|
|
138
|
+
} else {
|
|
139
|
+
// If parsing failed or returned empty, fall back to simple split
|
|
140
|
+
const columns = line.split(delimiter);
|
|
141
|
+
columnCounts.push(columns.length);
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// If csv-parse fails, fall back to simple split method
|
|
145
|
+
parseFailed = true;
|
|
146
|
+
const columns = line.split(delimiter);
|
|
147
|
+
columnCounts.push(columns.length);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check consistency: all lines should have the same column count
|
|
152
|
+
if (columnCounts.length > 1) {
|
|
153
|
+
const firstCount = columnCounts[0];
|
|
154
|
+
if (firstCount === undefined) continue;
|
|
155
|
+
const isConsistent = columnCounts.every((count) => count === firstCount);
|
|
156
|
+
|
|
157
|
+
if (isConsistent && firstCount > 1) {
|
|
158
|
+
// Score based on column count (more columns = better, up to a reasonable limit)
|
|
159
|
+
score = Math.min(firstCount, 10); // Cap at 10 to avoid excessive weight
|
|
160
|
+
|
|
161
|
+
// Bonus points for common delimiters
|
|
162
|
+
if (delimiter === ',') score += 0.5;
|
|
163
|
+
if (delimiter === ';') score += 0.3;
|
|
164
|
+
|
|
165
|
+
// Bonus points if csv-parse succeeded (indicates proper CSV format)
|
|
166
|
+
if (!parseFailed) score += 0.2;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (score > bestScore) {
|
|
171
|
+
bestScore = score;
|
|
172
|
+
bestDelimiter = delimiter;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return bestDelimiter;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Analyze header names to detect column purposes
|
|
181
|
+
*/
|
|
182
|
+
function analyzeHeaders(headers: string[]): {
|
|
183
|
+
dateColumn: string | null;
|
|
184
|
+
amountColumn: string | null;
|
|
185
|
+
descriptionColumn: string | null;
|
|
186
|
+
debitColumn: string | null;
|
|
187
|
+
creditColumn: string | null;
|
|
188
|
+
} {
|
|
189
|
+
const datePattern = /^(date|trans.*date|transaction.*date|post.*date|dt)$/i;
|
|
190
|
+
const amountPattern = /^(amount|amt|dollar.*amount|transaction.*amount)$/i;
|
|
191
|
+
const descriptionPattern = /^(description|desc|memo|transaction.*description|payee|merchant)$/i;
|
|
192
|
+
const debitPattern = /^(debit|debits|withdrawal|withdrawals|out|outgoing)$/i;
|
|
193
|
+
const creditPattern = /^(credit|credits|deposit|deposits|in|incoming)$/i;
|
|
194
|
+
|
|
195
|
+
let dateColumn: string | null = null;
|
|
196
|
+
let amountColumn: string | null = null;
|
|
197
|
+
let descriptionColumn: string | null = null;
|
|
198
|
+
let debitColumn: string | null = null;
|
|
199
|
+
let creditColumn: string | null = null;
|
|
200
|
+
|
|
201
|
+
for (const header of headers) {
|
|
202
|
+
const cleanHeader = header.trim();
|
|
203
|
+
|
|
204
|
+
if (datePattern.test(cleanHeader)) {
|
|
205
|
+
dateColumn = cleanHeader;
|
|
206
|
+
} else if (amountPattern.test(cleanHeader)) {
|
|
207
|
+
amountColumn = cleanHeader;
|
|
208
|
+
} else if (descriptionPattern.test(cleanHeader)) {
|
|
209
|
+
descriptionColumn = cleanHeader;
|
|
210
|
+
} else if (debitPattern.test(cleanHeader)) {
|
|
211
|
+
debitColumn = cleanHeader;
|
|
212
|
+
} else if (creditPattern.test(cleanHeader)) {
|
|
213
|
+
creditColumn = cleanHeader;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return { dateColumn, amountColumn, descriptionColumn, debitColumn, creditColumn };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Auto-detect CSV format by analyzing the first few rows
|
|
222
|
+
*/
|
|
223
|
+
export function autoDetectCSVFormat(csvContent: string): CSVFormat {
|
|
224
|
+
const linesRaw = csvContent.trim().split('\n').slice(0, 3);
|
|
225
|
+
if (linesRaw.length === 0) {
|
|
226
|
+
throw new Error('CSV file is empty');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Safely handle the first line - check if it exists and is not empty after trimming
|
|
230
|
+
const firstLineRaw = linesRaw[0];
|
|
231
|
+
if (!firstLineRaw || !firstLineRaw.trim()) {
|
|
232
|
+
throw new Error('CSV file contains empty first line');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Detect delimiter across sample lines
|
|
236
|
+
const delimiter = detectDelimiter(linesRaw);
|
|
237
|
+
|
|
238
|
+
const firstLine = firstLineRaw.split(delimiter);
|
|
239
|
+
const hasHeader = !isDateLike(firstLine[0] || '');
|
|
240
|
+
|
|
241
|
+
// Check for separate debit/credit columns by looking for empty cells pattern
|
|
242
|
+
let hasDebitCredit = false;
|
|
243
|
+
if (linesRaw.length > 1) {
|
|
244
|
+
const dataLines = hasHeader ? linesRaw.slice(1) : linesRaw;
|
|
245
|
+
hasDebitCredit = dataLines.some((line) => {
|
|
246
|
+
const cols = line.split(delimiter);
|
|
247
|
+
// Look for pattern: amount in col2 OR col3, but not both
|
|
248
|
+
return (
|
|
249
|
+
cols.length >= 4 &&
|
|
250
|
+
((cols[2]?.trim() && !cols[3]?.trim()) || (!cols[2]?.trim() && cols[3]?.trim()))
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (hasHeader) {
|
|
256
|
+
const { dateColumn, amountColumn, descriptionColumn, debitColumn, creditColumn } =
|
|
257
|
+
analyzeHeaders(firstLine);
|
|
258
|
+
|
|
259
|
+
const safe = (v?: string) => (v && v.trim() ? v : undefined);
|
|
260
|
+
|
|
261
|
+
if (hasDebitCredit && debitColumn && creditColumn) {
|
|
262
|
+
const dateCol = safe(dateColumn ?? undefined) ?? safe(firstLine[0]);
|
|
263
|
+
if (!dateCol) throw new Error('Unable to detect date column name from header');
|
|
264
|
+
const descCol = safe(descriptionColumn ?? undefined) ?? safe(firstLine[1]);
|
|
265
|
+
if (!descCol) throw new Error('Unable to detect description column name from header');
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
date_column: dateCol,
|
|
269
|
+
description_column: descCol,
|
|
270
|
+
debit_column: debitColumn,
|
|
271
|
+
credit_column: creditColumn,
|
|
272
|
+
date_format: detectDateFormat(linesRaw[1]?.split(delimiter)[0]),
|
|
273
|
+
has_header: hasHeader,
|
|
274
|
+
delimiter: delimiter,
|
|
275
|
+
};
|
|
276
|
+
} else {
|
|
277
|
+
const dateCol = safe(dateColumn ?? undefined) ?? safe(firstLine[0]);
|
|
278
|
+
if (!dateCol) throw new Error('Unable to detect date column name from header');
|
|
279
|
+
const amountCol = safe(amountColumn ?? undefined) ?? safe(firstLine[1]);
|
|
280
|
+
if (!amountCol) throw new Error('Unable to detect amount column name from header');
|
|
281
|
+
const descCol =
|
|
282
|
+
safe(descriptionColumn ?? undefined) ??
|
|
283
|
+
safe(firstLine.length >= 3 ? firstLine[2] : firstLine[1]);
|
|
284
|
+
if (!descCol) throw new Error('Unable to detect description column name from header');
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
date_column: dateCol,
|
|
288
|
+
amount_column: amountCol,
|
|
289
|
+
description_column: descCol,
|
|
290
|
+
date_format: detectDateFormat(linesRaw[1]?.split(delimiter)[0]),
|
|
291
|
+
has_header: hasHeader,
|
|
292
|
+
delimiter: delimiter,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
if (hasDebitCredit && firstLine.length >= 4) {
|
|
297
|
+
return {
|
|
298
|
+
date_column: 0,
|
|
299
|
+
description_column: 1,
|
|
300
|
+
debit_column: 2,
|
|
301
|
+
credit_column: 3,
|
|
302
|
+
date_format: detectDateFormat(firstLine[0]),
|
|
303
|
+
has_header: hasHeader,
|
|
304
|
+
delimiter: delimiter,
|
|
305
|
+
};
|
|
306
|
+
} else {
|
|
307
|
+
return {
|
|
308
|
+
date_column: 0,
|
|
309
|
+
amount_column: 1,
|
|
310
|
+
description_column: firstLine.length >= 3 ? 2 : 1,
|
|
311
|
+
date_format: detectDateFormat(firstLine[0]),
|
|
312
|
+
has_header: hasHeader,
|
|
313
|
+
delimiter: delimiter,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Automatically fix common CSV issues like unquoted dates with commas
|
|
321
|
+
*/
|
|
322
|
+
function preprocessCSV(csvContent: string, format: CSVFormat): string {
|
|
323
|
+
// Check if we're dealing with MMM dd, yyyy format dates that might need quoting
|
|
324
|
+
if (format.date_format?.includes('MMM') && format.date_format?.includes(',')) {
|
|
325
|
+
const lines = csvContent.split('\n');
|
|
326
|
+
const fixedLines = lines.map((line, index) => {
|
|
327
|
+
// Skip header row
|
|
328
|
+
if (format.has_header && index === 0) return line;
|
|
329
|
+
if (!line.trim()) return line;
|
|
330
|
+
|
|
331
|
+
// Check if this line has unquoted dates (more commas than expected)
|
|
332
|
+
const parts = line.split(format.delimiter);
|
|
333
|
+
const expectedColumns = format.has_header ? lines[0]?.split(format.delimiter).length || 3 : 3;
|
|
334
|
+
|
|
335
|
+
if (parts.length > expectedColumns) {
|
|
336
|
+
// Check if we have a date pattern split across first two parts (like "Sep 18, 2025")
|
|
337
|
+
const potentialDate = parts.slice(0, 2).join(',');
|
|
338
|
+
if (/^[A-Za-z]{3}\s+\d{1,2},\s+\d{4}/.test(potentialDate)) {
|
|
339
|
+
// This looks like "Sep 18, 2025" - quote it
|
|
340
|
+
const dateField = parts.slice(0, 2).join(','); // "Sep 18, 2025"
|
|
341
|
+
const remainingFields = parts.slice(2);
|
|
342
|
+
return `"${dateField}"${format.delimiter}${remainingFields.join(format.delimiter)}`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return line;
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
return fixedLines.join('\n');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return csvContent;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Parse CSV data into bank transactions
|
|
357
|
+
*/
|
|
358
|
+
export function parseBankCSV(
|
|
359
|
+
csvContent: string,
|
|
360
|
+
format: CSVFormat,
|
|
361
|
+
options: { debug?: boolean } = {},
|
|
362
|
+
): BankTransaction[] {
|
|
363
|
+
// Preprocess CSV to fix common issues like unquoted dates
|
|
364
|
+
const processedCSV = preprocessCSV(csvContent, format);
|
|
365
|
+
|
|
366
|
+
const records = parse(processedCSV, {
|
|
367
|
+
delimiter: format.delimiter,
|
|
368
|
+
columns: format.has_header,
|
|
369
|
+
skip_empty_lines: true,
|
|
370
|
+
trim: true,
|
|
371
|
+
// Enhanced CSV parsing options for robust handling
|
|
372
|
+
quote: '"', // Handle quoted fields (for dates with commas)
|
|
373
|
+
escape: '"', // Handle escaped quotes within fields
|
|
374
|
+
relax_column_count: true, // Handle varying column counts
|
|
375
|
+
// Removed deprecated auto_parse and auto_parse_date options
|
|
376
|
+
// Removed relax_quotes as it may not be supported in current csv-parse version
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const transactions: BankTransaction[] = [];
|
|
380
|
+
|
|
381
|
+
for (let i = 0; i < records.length; i++) {
|
|
382
|
+
const record = records[i];
|
|
383
|
+
const rowNumber = format.has_header ? i + 2 : i + 1; // Account for header row
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
let rawDate: string;
|
|
387
|
+
let rawAmount: string;
|
|
388
|
+
let description: string;
|
|
389
|
+
|
|
390
|
+
if (format.has_header) {
|
|
391
|
+
// Record is an object when using headers
|
|
392
|
+
const recordObj = record as unknown as Record<string, string>;
|
|
393
|
+
rawDate = recordObj[format.date_column as string] || '';
|
|
394
|
+
|
|
395
|
+
if (format.amount_column) {
|
|
396
|
+
rawAmount = recordObj[format.amount_column as string] || '';
|
|
397
|
+
} else if (format.debit_column !== undefined && format.credit_column !== undefined) {
|
|
398
|
+
const debitVal = recordObj[format.debit_column as string] || '';
|
|
399
|
+
const creditVal = recordObj[format.credit_column as string] || '';
|
|
400
|
+
// Convert: debits negative, credits positive
|
|
401
|
+
// Check if debit has a value and is non-zero
|
|
402
|
+
const debitNum = parseFloat(debitVal.replace(/[^\d.-]/g, ''));
|
|
403
|
+
const creditNum = parseFloat(creditVal.replace(/[^\d.-]/g, ''));
|
|
404
|
+
if (!isNaN(debitNum) && debitNum !== 0) {
|
|
405
|
+
rawAmount = `-${debitVal}`;
|
|
406
|
+
} else if (!isNaN(creditNum) && creditNum !== 0) {
|
|
407
|
+
rawAmount = creditVal;
|
|
408
|
+
} else {
|
|
409
|
+
rawAmount = '0';
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
throw new Error('No amount column configuration found');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
description = recordObj[format.description_column as string] || '';
|
|
416
|
+
} else {
|
|
417
|
+
// Record is an array when not using headers, so use column indices
|
|
418
|
+
const recordArray = record as string[];
|
|
419
|
+
const dateIndex =
|
|
420
|
+
typeof format.date_column === 'number'
|
|
421
|
+
? format.date_column
|
|
422
|
+
: parseInt(format.date_column, 10);
|
|
423
|
+
const descIndex =
|
|
424
|
+
typeof format.description_column === 'number'
|
|
425
|
+
? format.description_column
|
|
426
|
+
: parseInt(format.description_column, 10);
|
|
427
|
+
|
|
428
|
+
// Validate indices are valid numbers (fallback to defaults if invalid)
|
|
429
|
+
const safeDateIndex = isNaN(dateIndex) ? 0 : dateIndex;
|
|
430
|
+
const safeDescIndex = isNaN(descIndex) ? 2 : descIndex;
|
|
431
|
+
|
|
432
|
+
rawDate = recordArray[safeDateIndex] || '';
|
|
433
|
+
|
|
434
|
+
if (format.amount_column !== undefined) {
|
|
435
|
+
const amountIndex =
|
|
436
|
+
typeof format.amount_column === 'number'
|
|
437
|
+
? format.amount_column
|
|
438
|
+
: parseInt(format.amount_column, 10);
|
|
439
|
+
const safeAmountIndex = isNaN(amountIndex) ? 1 : amountIndex;
|
|
440
|
+
rawAmount = recordArray[safeAmountIndex] || '';
|
|
441
|
+
} else if (format.debit_column !== undefined && format.credit_column !== undefined) {
|
|
442
|
+
const debitIndex =
|
|
443
|
+
typeof format.debit_column === 'number'
|
|
444
|
+
? format.debit_column
|
|
445
|
+
: parseInt(format.debit_column, 10);
|
|
446
|
+
const creditIndex =
|
|
447
|
+
typeof format.credit_column === 'number'
|
|
448
|
+
? format.credit_column
|
|
449
|
+
: parseInt(format.credit_column, 10);
|
|
450
|
+
|
|
451
|
+
const debitVal = recordArray[debitIndex] || '';
|
|
452
|
+
const creditVal = recordArray[creditIndex] || '';
|
|
453
|
+
|
|
454
|
+
// Convert: debits negative, credits positive
|
|
455
|
+
// Check if debit has a value and is non-zero
|
|
456
|
+
const debitNum = parseFloat(debitVal.replace(/[^\d.-]/g, ''));
|
|
457
|
+
const creditNum = parseFloat(creditVal.replace(/[^\d.-]/g, ''));
|
|
458
|
+
if (!isNaN(debitNum) && debitNum !== 0) {
|
|
459
|
+
rawAmount = `-${debitVal}`;
|
|
460
|
+
} else if (!isNaN(creditNum) && creditNum !== 0) {
|
|
461
|
+
rawAmount = creditVal;
|
|
462
|
+
} else {
|
|
463
|
+
rawAmount = '0';
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
throw new Error('No amount column configuration found');
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
description = recordArray[safeDescIndex] || '';
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!rawDate || !rawAmount) {
|
|
473
|
+
if (options.debug) {
|
|
474
|
+
console.warn(`Skipping row ${rowNumber}: missing date or amount`);
|
|
475
|
+
}
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const date = parseDate(rawDate, format.date_format);
|
|
480
|
+
let amount: Milli;
|
|
481
|
+
try {
|
|
482
|
+
amount = amountToMilliunits(rawAmount);
|
|
483
|
+
} catch (error) {
|
|
484
|
+
if (options.debug) {
|
|
485
|
+
console.warn(
|
|
486
|
+
`Skipping row ${rowNumber}: ${error instanceof Error ? error.message : 'Invalid amount'}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
transactions.push({
|
|
493
|
+
date,
|
|
494
|
+
amount,
|
|
495
|
+
description: description.trim(),
|
|
496
|
+
raw_amount: rawAmount,
|
|
497
|
+
raw_date: rawDate,
|
|
498
|
+
row_number: rowNumber,
|
|
499
|
+
});
|
|
500
|
+
} catch (error) {
|
|
501
|
+
if (options.debug) {
|
|
502
|
+
console.warn(`Error parsing row ${rowNumber}:`, error);
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return transactions;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Read CSV file safely with error handling
|
|
513
|
+
*/
|
|
514
|
+
export function readCSVFile(filePath: string): string {
|
|
515
|
+
try {
|
|
516
|
+
return readFileSync(filePath, 'utf-8');
|
|
517
|
+
} catch (error) {
|
|
518
|
+
throw new Error(
|
|
519
|
+
`Unable to read CSV file: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Extract date range from CSV bank statement
|
|
526
|
+
* Returns min and max dates in YYYY-MM-DD format
|
|
527
|
+
*/
|
|
528
|
+
export function extractDateRangeFromCSV(
|
|
529
|
+
csvContent: string,
|
|
530
|
+
format: CSVFormat,
|
|
531
|
+
): { minDate: string; maxDate: string } {
|
|
532
|
+
const transactions = parseBankCSV(csvContent, format);
|
|
533
|
+
|
|
534
|
+
if (transactions.length === 0) {
|
|
535
|
+
throw new Error('No transactions found in CSV');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Extract all dates (already Date objects from parseBankCSV)
|
|
539
|
+
const dates = transactions.map((txn) => txn.date.getTime());
|
|
540
|
+
|
|
541
|
+
// Find min and max
|
|
542
|
+
const minDateObj = new Date(Math.min(...dates));
|
|
543
|
+
const maxDateObj = new Date(Math.max(...dates));
|
|
544
|
+
|
|
545
|
+
// Convert to YYYY-MM-DD format
|
|
546
|
+
const toYYYYMMDD = (date: Date): string => {
|
|
547
|
+
const year = date.getFullYear();
|
|
548
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
549
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
550
|
+
return `${year}-${month}-${day}`;
|
|
551
|
+
};
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
minDate: toYYYYMMDD(minDateObj),
|
|
555
|
+
maxDate: toYYYYMMDD(maxDateObj),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type * as ynab from 'ynab';
|
|
2
|
+
import { z } from 'zod/v4';
|
|
3
|
+
import type { CompareTransactionsSchema } from './index.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents a bank transaction from CSV
|
|
7
|
+
*/
|
|
8
|
+
export interface BankTransaction {
|
|
9
|
+
/** Parsed date of the transaction */
|
|
10
|
+
date: Date;
|
|
11
|
+
/** Transaction amount in milliunits (YNAB format) */
|
|
12
|
+
amount: number;
|
|
13
|
+
/** Transaction description from CSV */
|
|
14
|
+
description: string;
|
|
15
|
+
/** Original amount string from CSV */
|
|
16
|
+
raw_amount: string;
|
|
17
|
+
/** Original date string from CSV */
|
|
18
|
+
raw_date: string;
|
|
19
|
+
/** Row number in CSV file for reference */
|
|
20
|
+
row_number: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Represents a YNAB transaction for comparison
|
|
25
|
+
*/
|
|
26
|
+
export interface YNABTransaction {
|
|
27
|
+
/** YNAB transaction ID */
|
|
28
|
+
id: string;
|
|
29
|
+
/** Transaction date */
|
|
30
|
+
date: Date;
|
|
31
|
+
/** Transaction amount in milliunits */
|
|
32
|
+
amount: number;
|
|
33
|
+
/** Payee name (nullable) */
|
|
34
|
+
payee_name: string | null | undefined;
|
|
35
|
+
/** Transaction memo (nullable) */
|
|
36
|
+
memo: string | null | undefined;
|
|
37
|
+
/** Transaction cleared status */
|
|
38
|
+
cleared: string;
|
|
39
|
+
/** Original YNAB transaction detail object */
|
|
40
|
+
original: ynab.TransactionDetail;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Represents a matched pair of bank and YNAB transactions
|
|
45
|
+
*/
|
|
46
|
+
export interface TransactionMatch {
|
|
47
|
+
/** Bank transaction from CSV */
|
|
48
|
+
bank_transaction: BankTransaction;
|
|
49
|
+
/** Matched YNAB transaction */
|
|
50
|
+
ynab_transaction: YNABTransaction;
|
|
51
|
+
/** Match score (0-100, higher is better) */
|
|
52
|
+
match_score: number;
|
|
53
|
+
/** Reasons for the match with explanations */
|
|
54
|
+
match_reasons: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* CSV format configuration type derived from zod schema for consistency
|
|
59
|
+
*/
|
|
60
|
+
export type CSVFormat = z.infer<typeof CompareTransactionsSchema>['csv_format'];
|