@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.
Files changed (435) hide show
  1. package/.chunkhound.json +11 -0
  2. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +8 -0
  3. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +3 -0
  4. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +3 -0
  5. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +1 -0
  6. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +12 -0
  7. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +1 -0
  8. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +1 -0
  9. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +5 -0
  10. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +1453 -0
  11. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +8 -0
  12. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +9 -0
  13. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +1 -0
  14. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +5 -0
  15. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +81 -0
  16. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +610 -0
  17. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +1 -0
  18. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +1 -0
  19. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +1 -0
  20. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +3 -0
  21. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +8 -0
  22. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +5 -0
  23. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +1 -0
  24. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +9 -0
  25. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +1 -0
  26. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +13 -0
  27. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +14 -0
  28. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +5 -0
  29. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +5 -0
  30. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +5 -0
  31. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +3 -0
  32. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +1 -0
  33. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +1 -0
  34. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +5 -0
  35. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +5 -0
  36. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +8 -0
  37. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +8 -0
  38. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +5 -0
  39. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +1 -0
  40. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +1 -0
  41. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +1 -0
  42. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +1 -0
  43. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +1 -0
  44. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +5 -0
  45. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +1 -0
  46. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +1 -0
  47. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +1 -0
  48. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +3 -0
  49. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +8 -0
  50. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +1 -0
  51. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +1 -0
  52. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +8 -0
  53. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +3 -0
  54. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +5 -0
  55. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +1 -0
  56. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +1 -0
  57. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +5 -0
  58. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +1 -0
  59. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +14 -0
  60. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +1 -0
  61. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +5 -0
  62. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +5 -0
  63. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +11 -0
  64. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +1 -0
  65. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +8 -0
  66. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +1 -0
  67. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +3 -0
  68. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +3 -0
  69. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +8 -0
  70. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +1 -0
  71. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +5 -0
  72. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +1 -0
  73. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +1 -0
  74. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +1 -0
  75. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +1 -0
  76. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +3 -0
  77. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +8 -0
  78. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +1 -0
  79. package/.dxtignore +57 -0
  80. package/.env.example +44 -0
  81. package/.gemini/settings.json +8 -0
  82. package/.github/ISSUE_TEMPLATE/bug_report.md +41 -0
  83. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  84. package/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  85. package/.github/ISSUE_TEMPLATE/release_checklist.md +31 -0
  86. package/.github/pull_request_template.md +41 -0
  87. package/.github/workflows/ci-tests.yml +41 -0
  88. package/.github/workflows/claude-code-review.yml +57 -0
  89. package/.github/workflows/claude.yml +50 -0
  90. package/.github/workflows/full-integration.yml +22 -0
  91. package/.github/workflows/pr-description-check.yml +88 -0
  92. package/.github/workflows/publish.yml +33 -0
  93. package/.github/workflows/release.yml +89 -0
  94. package/.mcpbignore +58 -0
  95. package/.prettierignore +10 -0
  96. package/.prettierrc.json +10 -0
  97. package/ADOS-2-Module-1-Complete-Manual.md +757 -0
  98. package/AGENTS.md +36 -0
  99. package/CHANGELOG.md +187 -0
  100. package/CLAUDE.md +414 -0
  101. package/CODEREVIEW_RESPONSE.md +128 -0
  102. package/LICENSE +17 -0
  103. package/NUL +1 -0
  104. package/README.md +222 -0
  105. package/SCHEMA_IMPROVEMENT_SUMMARY.md +120 -0
  106. package/TESTING_NOTES.md +217 -0
  107. package/WARP.md +245 -0
  108. package/accountactivity-merged.csv +149 -0
  109. package/bin/ynab-mcp-server.cjs +4 -0
  110. package/bin/ynab-mcp-server.js +8 -0
  111. package/bundle-analysis.html +13110 -0
  112. package/dist/bundle/index.cjs +124 -0
  113. package/dist/index.d.ts +2 -0
  114. package/dist/index.js +85 -0
  115. package/dist/server/YNABMCPServer.d.ts +264 -0
  116. package/dist/server/YNABMCPServer.js +845 -0
  117. package/dist/server/budgetResolver.d.ts +15 -0
  118. package/dist/server/budgetResolver.js +99 -0
  119. package/dist/server/cacheManager.d.ts +74 -0
  120. package/dist/server/cacheManager.js +306 -0
  121. package/dist/server/config.d.ts +3 -0
  122. package/dist/server/config.js +19 -0
  123. package/dist/server/deltaCache.d.ts +61 -0
  124. package/dist/server/deltaCache.js +206 -0
  125. package/dist/server/deltaCache.merge.d.ts +9 -0
  126. package/dist/server/deltaCache.merge.js +111 -0
  127. package/dist/server/diagnostics.d.ts +90 -0
  128. package/dist/server/diagnostics.js +163 -0
  129. package/dist/server/errorHandler.d.ts +69 -0
  130. package/dist/server/errorHandler.js +524 -0
  131. package/dist/server/prompts.d.ts +31 -0
  132. package/dist/server/prompts.js +205 -0
  133. package/dist/server/rateLimiter.d.ts +27 -0
  134. package/dist/server/rateLimiter.js +82 -0
  135. package/dist/server/requestLogger.d.ts +62 -0
  136. package/dist/server/requestLogger.js +190 -0
  137. package/dist/server/resources.d.ts +39 -0
  138. package/dist/server/resources.js +85 -0
  139. package/dist/server/responseFormatter.d.ts +14 -0
  140. package/dist/server/responseFormatter.js +42 -0
  141. package/dist/server/securityMiddleware.d.ts +87 -0
  142. package/dist/server/securityMiddleware.js +117 -0
  143. package/dist/server/serverKnowledgeStore.d.ts +11 -0
  144. package/dist/server/serverKnowledgeStore.js +42 -0
  145. package/dist/server/toolRegistry.d.ts +85 -0
  146. package/dist/server/toolRegistry.js +272 -0
  147. package/dist/tools/__tests__/deltaTestUtils.d.ts +18 -0
  148. package/dist/tools/__tests__/deltaTestUtils.js +26 -0
  149. package/dist/tools/accountTools.d.ts +37 -0
  150. package/dist/tools/accountTools.js +175 -0
  151. package/dist/tools/budgetTools.d.ts +10 -0
  152. package/dist/tools/budgetTools.js +68 -0
  153. package/dist/tools/categoryTools.d.ts +27 -0
  154. package/dist/tools/categoryTools.js +232 -0
  155. package/dist/tools/compareTransactions/formatter.d.ts +71 -0
  156. package/dist/tools/compareTransactions/formatter.js +97 -0
  157. package/dist/tools/compareTransactions/index.d.ts +30 -0
  158. package/dist/tools/compareTransactions/index.js +160 -0
  159. package/dist/tools/compareTransactions/matcher.d.ts +12 -0
  160. package/dist/tools/compareTransactions/matcher.js +140 -0
  161. package/dist/tools/compareTransactions/parser.d.ts +14 -0
  162. package/dist/tools/compareTransactions/parser.js +430 -0
  163. package/dist/tools/compareTransactions/types.d.ts +27 -0
  164. package/dist/tools/compareTransactions/types.js +1 -0
  165. package/dist/tools/compareTransactions.d.ts +1 -0
  166. package/dist/tools/compareTransactions.js +1 -0
  167. package/dist/tools/deltaFetcher.d.ts +22 -0
  168. package/dist/tools/deltaFetcher.js +137 -0
  169. package/dist/tools/deltaSupport.d.ts +20 -0
  170. package/dist/tools/deltaSupport.js +176 -0
  171. package/dist/tools/exportTransactions.d.ts +17 -0
  172. package/dist/tools/exportTransactions.js +191 -0
  173. package/dist/tools/monthTools.d.ts +16 -0
  174. package/dist/tools/monthTools.js +107 -0
  175. package/dist/tools/payeeTools.d.ts +17 -0
  176. package/dist/tools/payeeTools.js +82 -0
  177. package/dist/tools/reconcileAdapter.d.ts +25 -0
  178. package/dist/tools/reconcileAdapter.js +167 -0
  179. package/dist/tools/reconciliation/analyzer.d.ts +3 -0
  180. package/dist/tools/reconciliation/analyzer.js +567 -0
  181. package/dist/tools/reconciliation/executor.d.ts +94 -0
  182. package/dist/tools/reconciliation/executor.js +611 -0
  183. package/dist/tools/reconciliation/index.d.ts +54 -0
  184. package/dist/tools/reconciliation/index.js +249 -0
  185. package/dist/tools/reconciliation/matcher.d.ts +3 -0
  186. package/dist/tools/reconciliation/matcher.js +160 -0
  187. package/dist/tools/reconciliation/payeeNormalizer.d.ts +6 -0
  188. package/dist/tools/reconciliation/payeeNormalizer.js +77 -0
  189. package/dist/tools/reconciliation/recommendationEngine.d.ts +2 -0
  190. package/dist/tools/reconciliation/recommendationEngine.js +273 -0
  191. package/dist/tools/reconciliation/reportFormatter.d.ts +13 -0
  192. package/dist/tools/reconciliation/reportFormatter.js +214 -0
  193. package/dist/tools/reconciliation/types.d.ts +172 -0
  194. package/dist/tools/reconciliation/types.js +7 -0
  195. package/dist/tools/schemas/outputs/accountOutputs.d.ts +58 -0
  196. package/dist/tools/schemas/outputs/accountOutputs.js +24 -0
  197. package/dist/tools/schemas/outputs/budgetOutputs.d.ts +48 -0
  198. package/dist/tools/schemas/outputs/budgetOutputs.js +15 -0
  199. package/dist/tools/schemas/outputs/categoryOutputs.d.ts +93 -0
  200. package/dist/tools/schemas/outputs/categoryOutputs.js +37 -0
  201. package/dist/tools/schemas/outputs/comparisonOutputs.d.ts +269 -0
  202. package/dist/tools/schemas/outputs/comparisonOutputs.js +181 -0
  203. package/dist/tools/schemas/outputs/index.d.ts +14 -0
  204. package/dist/tools/schemas/outputs/index.js +14 -0
  205. package/dist/tools/schemas/outputs/monthOutputs.d.ts +122 -0
  206. package/dist/tools/schemas/outputs/monthOutputs.js +51 -0
  207. package/dist/tools/schemas/outputs/payeeOutputs.d.ts +34 -0
  208. package/dist/tools/schemas/outputs/payeeOutputs.js +16 -0
  209. package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +1275 -0
  210. package/dist/tools/schemas/outputs/reconciliationOutputs.js +377 -0
  211. package/dist/tools/schemas/outputs/transactionMutationOutputs.d.ts +717 -0
  212. package/dist/tools/schemas/outputs/transactionMutationOutputs.js +260 -0
  213. package/dist/tools/schemas/outputs/transactionOutputs.d.ts +98 -0
  214. package/dist/tools/schemas/outputs/transactionOutputs.js +49 -0
  215. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +219 -0
  216. package/dist/tools/schemas/outputs/utilityOutputs.js +120 -0
  217. package/dist/tools/schemas/shared/commonOutputs.d.ts +24 -0
  218. package/dist/tools/schemas/shared/commonOutputs.js +27 -0
  219. package/dist/tools/toolCategories.d.ts +32 -0
  220. package/dist/tools/toolCategories.js +32 -0
  221. package/dist/tools/transactionTools.d.ts +315 -0
  222. package/dist/tools/transactionTools.js +1722 -0
  223. package/dist/tools/utilityTools.d.ts +10 -0
  224. package/dist/tools/utilityTools.js +56 -0
  225. package/dist/types/index.d.ts +20 -0
  226. package/dist/types/index.js +16 -0
  227. package/dist/types/toolAnnotations.d.ts +7 -0
  228. package/dist/types/toolAnnotations.js +1 -0
  229. package/dist/utils/amountUtils.d.ts +3 -0
  230. package/dist/utils/amountUtils.js +10 -0
  231. package/dist/utils/dateUtils.d.ts +9 -0
  232. package/dist/utils/dateUtils.js +43 -0
  233. package/dist/utils/money.d.ts +21 -0
  234. package/dist/utils/money.js +51 -0
  235. package/docs/README.md +72 -0
  236. package/docs/assets/examples/reconciliation-with-recommendations.json +68 -0
  237. package/docs/assets/schemas/reconciliation-v2.json +338 -0
  238. package/docs/getting-started/CONFIGURATION.md +175 -0
  239. package/docs/getting-started/INSTALLATION.md +333 -0
  240. package/docs/getting-started/QUICKSTART.md +282 -0
  241. package/docs/guides/ARCHITECTURE.md +650 -0
  242. package/docs/guides/DEPLOYMENT.md +189 -0
  243. package/docs/guides/INTEGRATION_TESTING.md +730 -0
  244. package/docs/guides/TESTING.md +591 -0
  245. package/docs/reconciliation-flow.md +83 -0
  246. package/docs/reference/API.md +1450 -0
  247. package/docs/reference/EXAMPLES.md +946 -0
  248. package/docs/reference/TOOLS.md +348 -0
  249. package/docs/reference/TROUBLESHOOTING.md +481 -0
  250. package/esbuild.config.mjs +68 -0
  251. package/eslint.config.js +49 -0
  252. package/fix-types.sh +17 -0
  253. package/meta.json +12550 -0
  254. package/package.json +105 -0
  255. package/package.json.tmp +105 -0
  256. package/scripts/analyze-bundle.mjs +41 -0
  257. package/scripts/create-pr-description.js +203 -0
  258. package/scripts/generate-mcpb.ps1 +96 -0
  259. package/scripts/run-domain-integration-tests.js +33 -0
  260. package/scripts/run-generate-mcpb.js +29 -0
  261. package/scripts/run-throttled-integration-tests.js +116 -0
  262. package/scripts/test-delta-params.mjs +140 -0
  263. package/scripts/test-recommendations.ts +53 -0
  264. package/scripts/tmpTransaction.ts +48 -0
  265. package/scripts/validate-env.js +122 -0
  266. package/scripts/verify-build.js +105 -0
  267. package/scripts/watch-and-restart.ps1 +50 -0
  268. package/src/__tests__/comprehensive.integration.test.ts +1196 -0
  269. package/src/__tests__/delta.performance.test.ts +80 -0
  270. package/src/__tests__/performance.test.ts +725 -0
  271. package/src/__tests__/setup.ts +449 -0
  272. package/src/__tests__/testRunner.ts +444 -0
  273. package/src/__tests__/testUtils.ts +563 -0
  274. package/src/__tests__/workflows.e2e.test.ts +1675 -0
  275. package/src/index.ts +124 -0
  276. package/src/server/.gitkeep +1 -0
  277. package/src/server/YNABMCPServer.ts +1188 -0
  278. package/src/server/__tests__/YNABMCPServer.integration.test.ts +903 -0
  279. package/src/server/__tests__/YNABMCPServer.test.ts +894 -0
  280. package/src/server/__tests__/budgetResolver.test.ts +425 -0
  281. package/src/server/__tests__/cacheManager.test.ts +880 -0
  282. package/src/server/__tests__/config.test.ts +166 -0
  283. package/src/server/__tests__/deltaCache.merge.test.ts +724 -0
  284. package/src/server/__tests__/deltaCache.swr.test.ts +168 -0
  285. package/src/server/__tests__/deltaCache.test.ts +774 -0
  286. package/src/server/__tests__/diagnostics.test.ts +823 -0
  287. package/src/server/__tests__/errorHandler.integration.test.ts +466 -0
  288. package/src/server/__tests__/errorHandler.test.ts +416 -0
  289. package/src/server/__tests__/prompts.test.ts +354 -0
  290. package/src/server/__tests__/rateLimiter.test.ts +314 -0
  291. package/src/server/__tests__/requestLogger.test.ts +408 -0
  292. package/src/server/__tests__/resources.test.ts +299 -0
  293. package/src/server/__tests__/security.integration.test.ts +426 -0
  294. package/src/server/__tests__/securityMiddleware.test.ts +449 -0
  295. package/src/server/__tests__/server-startup.integration.test.ts +477 -0
  296. package/src/server/__tests__/serverKnowledgeStore.test.ts +174 -0
  297. package/src/server/__tests__/toolRegistry.test.ts +855 -0
  298. package/src/server/budgetResolver.ts +235 -0
  299. package/src/server/cacheManager.ts +503 -0
  300. package/src/server/config.ts +41 -0
  301. package/src/server/deltaCache.merge.ts +149 -0
  302. package/src/server/deltaCache.ts +341 -0
  303. package/src/server/diagnostics.ts +338 -0
  304. package/src/server/errorHandler.ts +756 -0
  305. package/src/server/prompts.ts +291 -0
  306. package/src/server/rateLimiter.ts +156 -0
  307. package/src/server/requestLogger.ts +344 -0
  308. package/src/server/resources.ts +168 -0
  309. package/src/server/responseFormatter.ts +51 -0
  310. package/src/server/securityMiddleware.ts +236 -0
  311. package/src/server/serverKnowledgeStore.ts +91 -0
  312. package/src/server/toolRegistry.ts +489 -0
  313. package/src/tools/.gitkeep +1 -0
  314. package/src/tools/__tests__/accountTools.delta.integration.test.ts +128 -0
  315. package/src/tools/__tests__/accountTools.integration.test.ts +117 -0
  316. package/src/tools/__tests__/accountTools.test.ts +653 -0
  317. package/src/tools/__tests__/budgetTools.delta.integration.test.ts +90 -0
  318. package/src/tools/__tests__/budgetTools.integration.test.ts +134 -0
  319. package/src/tools/__tests__/budgetTools.test.ts +423 -0
  320. package/src/tools/__tests__/categoryTools.delta.integration.test.ts +80 -0
  321. package/src/tools/__tests__/categoryTools.integration.test.ts +295 -0
  322. package/src/tools/__tests__/categoryTools.test.ts +622 -0
  323. package/src/tools/__tests__/compareTransactions/formatter.test.ts +486 -0
  324. package/src/tools/__tests__/compareTransactions/index.test.ts +383 -0
  325. package/src/tools/__tests__/compareTransactions/matcher.test.ts +410 -0
  326. package/src/tools/__tests__/compareTransactions/parser.test.ts +764 -0
  327. package/src/tools/__tests__/compareTransactions.test.ts +342 -0
  328. package/src/tools/__tests__/compareTransactions.window.test.ts +147 -0
  329. package/src/tools/__tests__/deltaFetcher.scheduled.integration.test.ts +76 -0
  330. package/src/tools/__tests__/deltaFetcher.test.ts +270 -0
  331. package/src/tools/__tests__/deltaSupport.test.ts +188 -0
  332. package/src/tools/__tests__/deltaTestUtils.ts +46 -0
  333. package/src/tools/__tests__/exportTransactions.test.ts +213 -0
  334. package/src/tools/__tests__/monthTools.delta.integration.test.ts +80 -0
  335. package/src/tools/__tests__/monthTools.integration.test.ts +174 -0
  336. package/src/tools/__tests__/monthTools.test.ts +523 -0
  337. package/src/tools/__tests__/payeeTools.delta.integration.test.ts +80 -0
  338. package/src/tools/__tests__/payeeTools.integration.test.ts +150 -0
  339. package/src/tools/__tests__/payeeTools.test.ts +445 -0
  340. package/src/tools/__tests__/transactionTools.integration.test.ts +762 -0
  341. package/src/tools/__tests__/transactionTools.test.ts +3521 -0
  342. package/src/tools/__tests__/utilityTools.integration.test.ts +128 -0
  343. package/src/tools/__tests__/utilityTools.test.ts +205 -0
  344. package/src/tools/accountTools.ts +283 -0
  345. package/src/tools/budgetTools.ts +112 -0
  346. package/src/tools/categoryTools.ts +366 -0
  347. package/src/tools/compareTransactions/formatter.ts +163 -0
  348. package/src/tools/compareTransactions/index.ts +228 -0
  349. package/src/tools/compareTransactions/matcher.ts +240 -0
  350. package/src/tools/compareTransactions/parser.ts +557 -0
  351. package/src/tools/compareTransactions/types.ts +60 -0
  352. package/src/tools/compareTransactions.ts +3 -0
  353. package/src/tools/deltaFetcher.ts +278 -0
  354. package/src/tools/deltaSupport.ts +293 -0
  355. package/src/tools/exportTransactions.ts +273 -0
  356. package/src/tools/monthTools.ts +164 -0
  357. package/src/tools/payeeTools.ts +140 -0
  358. package/src/tools/reconcileAdapter.ts +312 -0
  359. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +122 -0
  360. package/src/tools/reconciliation/__tests__/adapter.test.ts +234 -0
  361. package/src/tools/reconciliation/__tests__/analyzer.test.ts +406 -0
  362. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +366 -0
  363. package/src/tools/reconciliation/__tests__/executor.test.ts +779 -0
  364. package/src/tools/reconciliation/__tests__/matcher.test.ts +650 -0
  365. package/src/tools/reconciliation/__tests__/payeeNormalizer.test.ts +278 -0
  366. package/src/tools/reconciliation/__tests__/recommendationEngine.integration.test.ts +658 -0
  367. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +1000 -0
  368. package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +151 -0
  369. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +573 -0
  370. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +78 -0
  371. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +47 -0
  372. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +61 -0
  373. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +49 -0
  374. package/src/tools/reconciliation/analyzer.ts +824 -0
  375. package/src/tools/reconciliation/executor.ts +880 -0
  376. package/src/tools/reconciliation/index.ts +400 -0
  377. package/src/tools/reconciliation/matcher.ts +269 -0
  378. package/src/tools/reconciliation/payeeNormalizer.ts +167 -0
  379. package/src/tools/reconciliation/recommendationEngine.ts +506 -0
  380. package/src/tools/reconciliation/reportFormatter.ts +363 -0
  381. package/src/tools/reconciliation/types.ts +314 -0
  382. package/src/tools/schemas/outputs/__tests__/accountOutputs.test.ts +424 -0
  383. package/src/tools/schemas/outputs/__tests__/budgetOutputs.test.ts +310 -0
  384. package/src/tools/schemas/outputs/__tests__/categoryOutputs.test.ts +448 -0
  385. package/src/tools/schemas/outputs/__tests__/comparisonOutputs.test.ts +519 -0
  386. package/src/tools/schemas/outputs/__tests__/dateValidation.test.ts +155 -0
  387. package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +288 -0
  388. package/src/tools/schemas/outputs/__tests__/monthOutputs.test.ts +478 -0
  389. package/src/tools/schemas/outputs/__tests__/payeeOutputs.test.ts +370 -0
  390. package/src/tools/schemas/outputs/__tests__/reconciliationOutputs.test.ts +401 -0
  391. package/src/tools/schemas/outputs/__tests__/transactionMutationSchemas.test.ts +213 -0
  392. package/src/tools/schemas/outputs/__tests__/transactionOutputs.test.ts +474 -0
  393. package/src/tools/schemas/outputs/__tests__/utilityOutputs.test.ts +333 -0
  394. package/src/tools/schemas/outputs/accountOutputs.ts +137 -0
  395. package/src/tools/schemas/outputs/budgetOutputs.ts +86 -0
  396. package/src/tools/schemas/outputs/categoryOutputs.ts +194 -0
  397. package/src/tools/schemas/outputs/comparisonOutputs.ts +600 -0
  398. package/src/tools/schemas/outputs/index.ts +270 -0
  399. package/src/tools/schemas/outputs/monthOutputs.ts +243 -0
  400. package/src/tools/schemas/outputs/payeeOutputs.ts +105 -0
  401. package/src/tools/schemas/outputs/reconciliationOutputs.ts +796 -0
  402. package/src/tools/schemas/outputs/transactionMutationOutputs.ts +758 -0
  403. package/src/tools/schemas/outputs/transactionOutputs.ts +243 -0
  404. package/src/tools/schemas/outputs/utilityOutputs.ts +411 -0
  405. package/src/tools/schemas/shared/commonOutputs.ts +140 -0
  406. package/src/tools/toolCategories.ts +140 -0
  407. package/src/tools/transactionTools.ts +2509 -0
  408. package/src/tools/utilityTools.ts +90 -0
  409. package/src/types/.gitkeep +1 -0
  410. package/src/types/__tests__/index.test.ts +52 -0
  411. package/src/types/index.ts +67 -0
  412. package/src/types/integration-tests.d.ts +35 -0
  413. package/src/types/toolAnnotations.ts +44 -0
  414. package/src/utils/__tests__/dateUtils.test.ts +170 -0
  415. package/src/utils/__tests__/money.test.ts +189 -0
  416. package/src/utils/amountUtils.ts +32 -0
  417. package/src/utils/dateUtils.ts +108 -0
  418. package/src/utils/money.ts +123 -0
  419. package/test-csv-sample.csv +28 -0
  420. package/test-exports/sample_bank_statement.csv +7 -0
  421. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +23 -0
  422. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +23 -0
  423. package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +44 -0
  424. package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +58 -0
  425. package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +115 -0
  426. package/test-reconcile-autodetect.js +40 -0
  427. package/test-reconcile-tool.js +152 -0
  428. package/test-reconcile-with-csv.cjs +89 -0
  429. package/test-statement.csv +8 -0
  430. package/test_debug.js +47 -0
  431. package/test_simple.mjs +16 -0
  432. package/tsconfig.json +31 -0
  433. package/tsconfig.prod.json +18 -0
  434. package/vitest-reporters/split-json-reporter.ts +211 -0
  435. package/vitest.config.ts +96 -0
@@ -0,0 +1,228 @@
1
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
2
+ import * as ynab from 'ynab';
3
+ import { z } from 'zod/v4';
4
+ import { withToolErrorHandling } from '../../types/index.js';
5
+ import { parseBankCSV, readCSVFile, autoDetectCSVFormat } from './parser.js';
6
+ import { findMatches } from './matcher.js';
7
+ import { buildComparisonResult } from './formatter.js';
8
+ import type { YNABTransaction } from './types.js';
9
+
10
+ // Re-export core types for consumers
11
+ export type { BankTransaction, YNABTransaction, TransactionMatch } from './types.js';
12
+
13
+ /**
14
+ * Schema for ynab:compare_transactions tool parameters
15
+ */
16
+ export const CompareTransactionsSchema = z
17
+ .object({
18
+ budget_id: z.string().min(1, 'Budget ID is required'),
19
+ account_id: z.string().min(1, 'Account ID is required'),
20
+ csv_file_path: z.string().optional(),
21
+ csv_data: z.string().optional(),
22
+ /** @deprecated Currently unused. Originally intended to limit the date range when CSV dates span a very long period. */
23
+ date_range_days: z.number().min(1).max(365).optional().default(30),
24
+ amount_tolerance: z.number().min(0).max(1).optional().default(0.01),
25
+ date_tolerance_days: z.number().min(0).max(7).optional().default(5),
26
+ enable_chronology_bonus: z.boolean().optional().default(false),
27
+ statement_start_date: z.string().optional(),
28
+ statement_date: z.string().optional(),
29
+ auto_detect_format: z.boolean().optional().default(false),
30
+ debug: z.boolean().optional().default(false),
31
+ csv_format: z
32
+ .object({
33
+ date_column: z.union([z.string(), z.number()]).optional().default('Date'),
34
+ amount_column: z.union([z.string(), z.number()]).optional(),
35
+ debit_column: z.union([z.string(), z.number()]).optional(),
36
+ credit_column: z.union([z.string(), z.number()]).optional(),
37
+ description_column: z.union([z.string(), z.number()]).optional().default('Description'),
38
+ date_format: z.string().optional().default('MM/DD/YYYY'),
39
+ has_header: z.boolean().optional().default(true),
40
+ delimiter: z.string().optional().default(','),
41
+ })
42
+ .strict()
43
+ .optional()
44
+ .default(() => ({
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
+ .strict()
54
+ .refine((data) => data.csv_file_path || data.csv_data, {
55
+ message: 'Either csv_file_path or csv_data must be provided',
56
+ });
57
+
58
+ export type CompareTransactionsParams = z.infer<typeof CompareTransactionsSchema>;
59
+
60
+ /**
61
+ * Handles the ynab:compare_transactions tool call
62
+ */
63
+ export async function handleCompareTransactions(
64
+ ynabAPI: ynab.API,
65
+ params: CompareTransactionsParams,
66
+ ): Promise<CallToolResult> {
67
+ return await withToolErrorHandling(
68
+ async () => {
69
+ // Parse and apply defaults/validation
70
+ const parsed = CompareTransactionsSchema.parse(params);
71
+
72
+ const payeesResponse = await ynabAPI.payees.getPayees(parsed.budget_id);
73
+ const payees = payeesResponse.data.payees;
74
+
75
+ // Get CSV data
76
+ let csvContent: string;
77
+ if (parsed.csv_file_path) {
78
+ csvContent = readCSVFile(parsed.csv_file_path);
79
+ } else {
80
+ csvContent = parsed.csv_data!;
81
+ }
82
+
83
+ // Auto-detect format if requested
84
+ let csvFormat = parsed.csv_format;
85
+ if (parsed.auto_detect_format) {
86
+ try {
87
+ csvFormat = autoDetectCSVFormat(csvContent);
88
+ if (parsed.debug) {
89
+ console.warn('Auto-detected CSV format:', csvFormat);
90
+ }
91
+ } catch (error) {
92
+ if (parsed.debug) {
93
+ console.warn('Auto-detection failed, using provided format:', error);
94
+ }
95
+ }
96
+ }
97
+
98
+ // Parse bank transactions from CSV
99
+ const bankTransactions = parseBankCSV(csvContent, csvFormat, { debug: parsed.debug });
100
+
101
+ if (bankTransactions.length === 0) {
102
+ throw new Error(
103
+ 'No valid transactions found in CSV data. ' +
104
+ 'Check your csv_format parameters or try auto_detect_format: true. ' +
105
+ `CSV has ${csvContent.split('\n').length} lines.`,
106
+ );
107
+ }
108
+
109
+ // Calculate date range for YNAB query
110
+ const bankDates = bankTransactions.map((t) => t.date);
111
+ const minDate = new Date(Math.min(...bankDates.map((d) => d.getTime())));
112
+ const maxDate = new Date(Math.max(...bankDates.map((d) => d.getTime())));
113
+
114
+ // Add tolerance to date range
115
+ const startDate = new Date(minDate);
116
+ startDate.setDate(startDate.getDate() - parsed.date_tolerance_days!);
117
+ const endDate = new Date(maxDate);
118
+ endDate.setDate(endDate.getDate() + parsed.date_tolerance_days!);
119
+
120
+ // Get YNAB transactions for the account in the date range
121
+ const sinceDate = startDate.toISOString().split('T')[0];
122
+ const response = await ynabAPI.transactions.getTransactionsByAccount(
123
+ parsed.budget_id,
124
+ parsed.account_id,
125
+ sinceDate,
126
+ );
127
+
128
+ // Filter YNAB transactions to the extended date range and convert for comparison
129
+ const ynabTransactions: YNABTransaction[] = response.data.transactions
130
+ .filter((txn) => {
131
+ const txnDate = new Date(txn.date);
132
+ return txnDate >= startDate && txnDate <= endDate && !txn.deleted;
133
+ })
134
+ .map((txn) => ({
135
+ id: txn.id,
136
+ date: new Date(txn.date),
137
+ amount: txn.amount,
138
+ payee_name: txn.payee_name,
139
+ memo: txn.memo,
140
+ cleared: txn.cleared,
141
+ original: txn,
142
+ }));
143
+
144
+ // Filter candidates to statement window if provided
145
+ let filteredBankTransactions = bankTransactions;
146
+ let filteredYnabTransactions = ynabTransactions;
147
+
148
+ if (parsed.statement_start_date || parsed.statement_date) {
149
+ filteredBankTransactions = bankTransactions.filter((t) => {
150
+ const dateStr = t.date.toISOString().split('T')[0]!;
151
+ if (parsed.statement_start_date && dateStr < parsed.statement_start_date) {
152
+ return false;
153
+ }
154
+ if (parsed.statement_date && dateStr > parsed.statement_date) {
155
+ return false;
156
+ }
157
+ return true;
158
+ });
159
+ filteredYnabTransactions = ynabTransactions.filter((t) => {
160
+ const dateStr = t.date.toISOString().split('T')[0]!;
161
+ if (parsed.statement_start_date && dateStr < parsed.statement_start_date) {
162
+ return false;
163
+ }
164
+ if (parsed.statement_date && dateStr > parsed.statement_date) {
165
+ return false;
166
+ }
167
+ return true;
168
+ });
169
+ }
170
+
171
+ // Find matches
172
+ const matchResults = findMatches(
173
+ filteredBankTransactions,
174
+ filteredYnabTransactions,
175
+ parsed.amount_tolerance!,
176
+ parsed.date_tolerance_days!,
177
+ parsed.enable_chronology_bonus!,
178
+ );
179
+
180
+ // Build comparison result - compute date range from filtered transactions when statement window is applied
181
+ let dateRange: { start: string; end: string };
182
+ if (parsed.statement_start_date || parsed.statement_date) {
183
+ // Use filtered bank transactions for date range when statement window filtering is applied
184
+ const filteredBankDates = filteredBankTransactions.map((t) => t.date);
185
+ if (filteredBankDates.length > 0) {
186
+ const filteredMinDate = new Date(Math.min(...filteredBankDates.map((d) => d.getTime())));
187
+ const filteredMaxDate = new Date(Math.max(...filteredBankDates.map((d) => d.getTime())));
188
+ dateRange = {
189
+ start: filteredMinDate.toISOString().split('T')[0] as string,
190
+ end: filteredMaxDate.toISOString().split('T')[0] as string,
191
+ };
192
+ } else {
193
+ // Fallback to statement window if no filtered transactions
194
+ dateRange = {
195
+ start: (parsed.statement_start_date ||
196
+ parsed.statement_date ||
197
+ minDate.toISOString().split('T')[0]) as string,
198
+ end: (parsed.statement_date ||
199
+ parsed.statement_start_date ||
200
+ maxDate.toISOString().split('T')[0]) as string,
201
+ };
202
+ }
203
+ } else {
204
+ // Use original unfiltered date range when no statement window filtering
205
+ dateRange = {
206
+ start: minDate.toISOString().split('T')[0] as string,
207
+ end: maxDate.toISOString().split('T')[0] as string,
208
+ };
209
+ }
210
+
211
+ const parameters = {
212
+ amount_tolerance: parsed.amount_tolerance,
213
+ date_tolerance_days: parsed.date_tolerance_days,
214
+ };
215
+
216
+ return buildComparisonResult(
217
+ matchResults,
218
+ filteredBankTransactions,
219
+ filteredYnabTransactions,
220
+ payees,
221
+ parameters,
222
+ dateRange,
223
+ );
224
+ },
225
+ 'ynab:compare_transactions',
226
+ 'comparing bank and YNAB transactions',
227
+ );
228
+ }
@@ -0,0 +1,240 @@
1
+ import { BankTransaction, YNABTransaction, TransactionMatch } from './types.js';
2
+
3
+ /**
4
+ * Calculate match score between bank and YNAB transactions
5
+ */
6
+ export function calculateMatchScore(
7
+ bankTxn: BankTransaction,
8
+ ynabTxn: YNABTransaction,
9
+ amountTolerance: number,
10
+ dateTolerance: number,
11
+ ): { score: number; reasons: string[] } {
12
+ const reasons: string[] = [];
13
+ let score = 0;
14
+
15
+ // Date matching (40 points max)
16
+ const dateDiff = Math.abs(bankTxn.date.getTime() - ynabTxn.date.getTime());
17
+ const daysDiff = dateDiff / (1000 * 60 * 60 * 24);
18
+
19
+ if (daysDiff === 0) {
20
+ score += 40;
21
+ reasons.push('Exact date match');
22
+ } else if (daysDiff <= dateTolerance) {
23
+ score += Math.max(20, 40 - daysDiff * 10);
24
+ reasons.push(`Date within ${daysDiff.toFixed(1)} days`);
25
+ }
26
+
27
+ // Amount matching (50 points max)
28
+ const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
29
+ const amountDiffPercent = amountDiff / Math.abs(bankTxn.amount);
30
+
31
+ if (amountDiff === 0) {
32
+ score += 50;
33
+ reasons.push('Exact amount match');
34
+ } else if (amountDiffPercent <= amountTolerance) {
35
+ score += Math.max(25, 50 - amountDiffPercent * 1000);
36
+ reasons.push(`Amount within ${(amountDiffPercent * 100).toFixed(2)}% tolerance`);
37
+ }
38
+
39
+ // Description/payee matching (10 points max)
40
+ const bankDesc = bankTxn.description.toLowerCase();
41
+ const ynabPayee = (ynabTxn.payee_name || '').toLowerCase();
42
+ const ynabMemo = (ynabTxn.memo || '').toLowerCase();
43
+
44
+ if (bankDesc && ynabPayee && (ynabPayee.includes(bankDesc) || bankDesc.includes(ynabPayee))) {
45
+ score += 10;
46
+ reasons.push('Payee name similarity');
47
+ } else if (bankDesc && ynabMemo && (ynabMemo.includes(bankDesc) || bankDesc.includes(ynabMemo))) {
48
+ score += 5;
49
+ reasons.push('Memo similarity');
50
+ }
51
+
52
+ return { score, reasons };
53
+ }
54
+
55
+ /**
56
+ * Group transactions by amount to detect duplicates
57
+ */
58
+ export function groupTransactionsByAmount(
59
+ transactions: (BankTransaction | YNABTransaction)[],
60
+ ): Map<number, (BankTransaction | YNABTransaction)[]> {
61
+ const groups = new Map<number, (BankTransaction | YNABTransaction)[]>();
62
+
63
+ for (const txn of transactions) {
64
+ const amount = txn.amount;
65
+ if (!groups.has(amount)) {
66
+ groups.set(amount, []);
67
+ }
68
+ groups.get(amount)!.push(txn);
69
+ }
70
+
71
+ return groups;
72
+ }
73
+
74
+ /**
75
+ * Match duplicate amounts using sequential date-based approach
76
+ */
77
+ export function matchDuplicateAmounts(
78
+ bankTxns: BankTransaction[],
79
+ ynabTxns: YNABTransaction[],
80
+ _amount: number,
81
+ amountTolerance: number,
82
+ dateTolerance: number,
83
+ enableChronologyBonus: boolean = false,
84
+ ): TransactionMatch[] {
85
+ // Sort both arrays by date for sequential matching
86
+ const sortedBank = [...bankTxns].sort((a, b) => a.date.getTime() - b.date.getTime());
87
+ const sortedYnab = [...ynabTxns].sort((a, b) => a.date.getTime() - b.date.getTime());
88
+
89
+ const matches: TransactionMatch[] = [];
90
+ const usedYnabIds = new Set<string>();
91
+
92
+ // For each bank transaction, find the best available YNAB transaction
93
+ // considering both score and chronological order
94
+ for (const bankTxn of sortedBank) {
95
+ let bestMatch: { ynab: YNABTransaction; score: number; reasons: string[] } | null = null;
96
+
97
+ for (const ynabTxn of sortedYnab) {
98
+ if (usedYnabIds.has(ynabTxn.id)) continue;
99
+
100
+ const { score, reasons } = calculateMatchScore(
101
+ bankTxn,
102
+ ynabTxn,
103
+ amountTolerance,
104
+ dateTolerance,
105
+ );
106
+
107
+ // Apply chronology bonus if enabled (for duplicates, heavily prefer chronological order)
108
+ let chronologyBonus = 0;
109
+ if (enableChronologyBonus) {
110
+ const daysDiff =
111
+ Math.abs(bankTxn.date.getTime() - ynabTxn.date.getTime()) / (1000 * 60 * 60 * 24);
112
+ chronologyBonus = daysDiff <= 1 ? 15 : daysDiff <= 3 ? 10 : 0;
113
+ }
114
+ const adjustedScore = score + chronologyBonus;
115
+
116
+ if (adjustedScore >= 30 && (!bestMatch || adjustedScore > bestMatch.score)) {
117
+ const enhancedReasons = [...reasons];
118
+ if (chronologyBonus > 0) {
119
+ enhancedReasons.push(`Chronological order bonus (+${chronologyBonus})`);
120
+ }
121
+ bestMatch = { ynab: ynabTxn, score: adjustedScore, reasons: enhancedReasons };
122
+ }
123
+ }
124
+
125
+ if (bestMatch) {
126
+ matches.push({
127
+ bank_transaction: bankTxn,
128
+ ynab_transaction: bestMatch.ynab,
129
+ match_score: bestMatch.score,
130
+ match_reasons: bestMatch.reasons,
131
+ });
132
+ usedYnabIds.add(bestMatch.ynab.id);
133
+ }
134
+ }
135
+
136
+ return matches;
137
+ }
138
+
139
+ /**
140
+ * Find the best matches between bank and YNAB transactions
141
+ */
142
+ export function findMatches(
143
+ bankTransactions: BankTransaction[],
144
+ ynabTransactions: YNABTransaction[],
145
+ amountTolerance: number,
146
+ dateTolerance: number,
147
+ enableChronologyBonus: boolean = false,
148
+ ): {
149
+ matches: TransactionMatch[];
150
+ unmatched_bank: BankTransaction[];
151
+ unmatched_ynab: YNABTransaction[];
152
+ } {
153
+ const matches: TransactionMatch[] = [];
154
+ const usedYnabIds = new Set<string>();
155
+ const usedBankIndices = new Set<number>();
156
+
157
+ // Group transactions by amount to detect duplicates
158
+ const bankByAmount = groupTransactionsByAmount(bankTransactions);
159
+ const ynabByAmount = groupTransactionsByAmount(ynabTransactions);
160
+
161
+ // Find amounts that appear multiple times (duplicates)
162
+ const duplicateAmounts = new Set<number>();
163
+ for (const [amount, txns] of bankByAmount) {
164
+ if (txns.length > 1 || (ynabByAmount.get(amount)?.length || 0) > 1) {
165
+ duplicateAmounts.add(amount);
166
+ }
167
+ }
168
+
169
+ // Handle duplicate amounts with special sequential matching
170
+ for (const amount of duplicateAmounts) {
171
+ const bankDuplicates =
172
+ bankByAmount
173
+ .get(amount)
174
+ ?.filter(
175
+ (txn): txn is BankTransaction =>
176
+ 'raw_amount' in txn && !usedBankIndices.has(bankTransactions.indexOf(txn)),
177
+ ) || [];
178
+ const ynabDuplicates =
179
+ ynabByAmount
180
+ .get(amount)
181
+ ?.filter((txn): txn is YNABTransaction => 'id' in txn && !usedYnabIds.has(txn.id)) || [];
182
+
183
+ if (bankDuplicates.length > 0 && ynabDuplicates.length > 0) {
184
+ const duplicateMatches = matchDuplicateAmounts(
185
+ bankDuplicates,
186
+ ynabDuplicates,
187
+ amount,
188
+ amountTolerance,
189
+ dateTolerance,
190
+ enableChronologyBonus,
191
+ );
192
+
193
+ for (const match of duplicateMatches) {
194
+ matches.push(match);
195
+ usedYnabIds.add(match.ynab_transaction.id);
196
+ usedBankIndices.add(bankTransactions.indexOf(match.bank_transaction));
197
+ }
198
+ }
199
+ }
200
+
201
+ // Handle non-duplicate amounts with original algorithm
202
+ for (let i = 0; i < bankTransactions.length; i++) {
203
+ const bankTxn = bankTransactions[i];
204
+ if (!bankTxn || usedBankIndices.has(i) || duplicateAmounts.has(bankTxn.amount)) continue;
205
+
206
+ let bestMatch: { ynab: YNABTransaction; score: number; reasons: string[] } | null = null;
207
+
208
+ for (const ynabTxn of ynabTransactions) {
209
+ if (usedYnabIds.has(ynabTxn.id) || duplicateAmounts.has(ynabTxn.amount)) continue;
210
+
211
+ const { score, reasons } = calculateMatchScore(
212
+ bankTxn,
213
+ ynabTxn,
214
+ amountTolerance,
215
+ dateTolerance,
216
+ );
217
+
218
+ if (score >= 30 && (!bestMatch || score > bestMatch.score)) {
219
+ bestMatch = { ynab: ynabTxn, score, reasons };
220
+ }
221
+ }
222
+
223
+ if (bestMatch) {
224
+ matches.push({
225
+ bank_transaction: bankTxn,
226
+ ynab_transaction: bestMatch.ynab,
227
+ match_score: bestMatch.score,
228
+ match_reasons: bestMatch.reasons,
229
+ });
230
+ usedYnabIds.add(bestMatch.ynab.id);
231
+ usedBankIndices.add(i);
232
+ }
233
+ }
234
+
235
+ // Collect unmatched transactions
236
+ const unmatched_bank = bankTransactions.filter((_, i) => !usedBankIndices.has(i));
237
+ const unmatched_ynab = ynabTransactions.filter((txn) => !usedYnabIds.has(txn.id));
238
+
239
+ return { matches, unmatched_bank, unmatched_ynab };
240
+ }