@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,400 @@
1
+ /**
2
+ * Reconciliation tool - Phase 1: Analysis Only
3
+ * Implements guided reconciliation workflow with conservative matching
4
+ */
5
+
6
+ import { z } from 'zod/v4';
7
+ import type * as ynab from 'ynab';
8
+ import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
9
+ import { withToolErrorHandling } from '../../types/index.js';
10
+ import { analyzeReconciliation } from './analyzer.js';
11
+ import type { MatchingConfig } from './types.js';
12
+ import { buildReconciliationPayload } from '../reconcileAdapter.js';
13
+ import {
14
+ executeReconciliation,
15
+ type AccountSnapshot,
16
+ type LegacyReconciliationResult,
17
+ } from './executor.js';
18
+ import { responseFormatter } from '../../server/responseFormatter.js';
19
+ import { extractDateRangeFromCSV, autoDetectCSVFormat } from '../compareTransactions/parser.js';
20
+ import type { DeltaFetcher } from '../deltaFetcher.js';
21
+ import { resolveDeltaFetcherArgs } from '../deltaSupport.js';
22
+
23
+ // Re-export types for external use
24
+ export type * from './types.js';
25
+ export { analyzeReconciliation } from './analyzer.js';
26
+ export { findMatches, findBestMatch } from './matcher.js';
27
+ export { normalizePayee, normalizedMatch, fuzzyMatch, payeeSimilarity } from './payeeNormalizer.js';
28
+
29
+ /**
30
+ * Helper function to determine audit data source based on fetch result
31
+ */
32
+ function getAuditDataSource(
33
+ transactionsResult: { usedDelta?: boolean; wasCached?: boolean },
34
+ forceFullRefresh: boolean,
35
+ ): string {
36
+ if (forceFullRefresh) {
37
+ return 'full_api_fetch_no_delta';
38
+ }
39
+ if (transactionsResult.usedDelta) {
40
+ return 'delta_fetch_with_merge';
41
+ }
42
+ if (transactionsResult.wasCached) {
43
+ return 'delta_fetch_cache_hit';
44
+ }
45
+ return 'delta_fetch_full_refresh';
46
+ }
47
+
48
+ /**
49
+ * Helper function to determine data freshness based on fetch result
50
+ */
51
+ function getDataFreshness(
52
+ transactionsResult: { wasCached?: boolean },
53
+ forceFullRefresh: boolean,
54
+ ): string {
55
+ if (forceFullRefresh) {
56
+ return 'guaranteed_fresh';
57
+ }
58
+ if (transactionsResult.wasCached) {
59
+ return 'cache_validated_via_server_knowledge';
60
+ }
61
+ return 'fresh_via_delta_fetch';
62
+ }
63
+
64
+ /**
65
+ * Schema for reconcile_account tool
66
+ */
67
+ export const ReconcileAccountSchema = z
68
+ .object({
69
+ budget_id: z.string().min(1, 'Budget ID is required'),
70
+ account_id: z.string().min(1, 'Account ID is required'),
71
+
72
+ // CSV input (one required)
73
+ csv_file_path: z.string().optional(),
74
+ csv_data: z.string().optional(),
75
+
76
+ csv_format: z
77
+ .object({
78
+ date_column: z.union([z.string(), z.number()]).optional().default('Date'),
79
+ amount_column: z.union([z.string(), z.number()]).optional(),
80
+ debit_column: z.union([z.string(), z.number()]).optional(),
81
+ credit_column: z.union([z.string(), z.number()]).optional(),
82
+ description_column: z.union([z.string(), z.number()]).optional().default('Description'),
83
+ date_format: z.string().optional().default('MM/DD/YYYY'),
84
+ has_header: z.boolean().optional().default(true),
85
+ delimiter: z.string().optional().default(','),
86
+ })
87
+ .strict()
88
+ .optional()
89
+ .default(() => ({
90
+ date_column: 'Date',
91
+ amount_column: 'Amount',
92
+ description_column: 'Description',
93
+ date_format: 'MM/DD/YYYY',
94
+ has_header: true,
95
+ delimiter: ',',
96
+ })),
97
+
98
+ // Statement information
99
+ statement_balance: z.number({
100
+ message: 'Statement balance is required and must be a number',
101
+ }),
102
+ statement_start_date: z.string().optional(),
103
+ statement_end_date: z.string().optional(),
104
+ statement_date: z.string().optional(),
105
+ expected_bank_balance: z.number().optional(),
106
+ as_of_timezone: z.string().optional(),
107
+
108
+ // Matching configuration (optional)
109
+ date_tolerance_days: z.number().min(0).max(7).optional().default(5),
110
+ amount_tolerance_cents: z.number().min(0).max(100).optional().default(1),
111
+ auto_match_threshold: z.number().min(0).max(100).optional().default(90),
112
+ suggestion_threshold: z.number().min(0).max(100).optional().default(60),
113
+ amount_tolerance: z.number().min(0).max(1).optional(),
114
+
115
+ auto_create_transactions: z.boolean().optional().default(false),
116
+ auto_update_cleared_status: z.boolean().optional().default(false),
117
+ auto_unclear_missing: z.boolean().optional().default(true),
118
+ auto_adjust_dates: z.boolean().optional().default(false),
119
+ invert_bank_amounts: z.boolean().optional(),
120
+ dry_run: z.boolean().optional().default(true),
121
+ balance_verification_mode: z
122
+ .enum(['ANALYSIS_ONLY', 'GUIDED_RESOLUTION', 'AUTO_RESOLVE'])
123
+ .optional()
124
+ .default('ANALYSIS_ONLY'),
125
+ require_exact_match: z.boolean().optional().default(true),
126
+ confidence_threshold: z.number().min(0).max(1).optional().default(0.8),
127
+ max_resolution_attempts: z.number().int().min(1).max(10).optional().default(5),
128
+
129
+ // Response options
130
+ include_structured_data: z.boolean().optional().default(false),
131
+ force_full_refresh: z.boolean().optional().default(true),
132
+ })
133
+ .refine((data) => data.csv_file_path || data.csv_data, {
134
+ message: 'Either csv_file_path or csv_data must be provided',
135
+ path: ['csv_data'],
136
+ });
137
+
138
+ export type ReconcileAccountRequest = z.infer<typeof ReconcileAccountSchema>;
139
+
140
+ /**
141
+ * Handle reconciliation analysis and optional execution
142
+ *
143
+ * Provides intelligent transaction matching, insight detection, and optional
144
+ * execution of reconciliation actions. Returns human-readable narrative and
145
+ * structured JSON data.
146
+ */
147
+ export async function handleReconcileAccount(
148
+ ynabAPI: ynab.API,
149
+ deltaFetcher: DeltaFetcher,
150
+ params: ReconcileAccountRequest,
151
+ ): Promise<CallToolResult>;
152
+ export async function handleReconcileAccount(
153
+ ynabAPI: ynab.API,
154
+ params: ReconcileAccountRequest,
155
+ ): Promise<CallToolResult>;
156
+ export async function handleReconcileAccount(
157
+ ynabAPI: ynab.API,
158
+ deltaFetcherOrParams: DeltaFetcher | ReconcileAccountRequest,
159
+ maybeParams?: ReconcileAccountRequest,
160
+ ): Promise<CallToolResult> {
161
+ const { deltaFetcher, params } = resolveDeltaFetcherArgs(
162
+ ynabAPI,
163
+ deltaFetcherOrParams,
164
+ maybeParams,
165
+ );
166
+ const forceFullRefresh = params.force_full_refresh ?? true;
167
+ return await withToolErrorHandling(
168
+ async () => {
169
+ // Build matching configuration from parameters
170
+ const config: MatchingConfig = {
171
+ dateToleranceDays: params.date_tolerance_days,
172
+ amountToleranceCents: params.amount_tolerance_cents,
173
+ descriptionSimilarityThreshold: 0.8, // Fixed for Phase 1
174
+ autoMatchThreshold: params.auto_match_threshold,
175
+ suggestionThreshold: params.suggestion_threshold,
176
+ };
177
+
178
+ const accountResult = forceFullRefresh
179
+ ? await deltaFetcher.fetchAccountsFull(params.budget_id)
180
+ : await deltaFetcher.fetchAccounts(params.budget_id);
181
+ const accountData = accountResult.data.find((account) => account.id === params.account_id);
182
+ if (!accountData) {
183
+ throw new Error(`Account ${params.account_id} not found in budget ${params.budget_id}`);
184
+ }
185
+ const accountName = accountData.name;
186
+ const accountType = accountData.type;
187
+
188
+ // For liability accounts (credit cards, loans, debts), statement balance should be negative
189
+ // A positive balance on a credit card statement means you OWE that amount
190
+ const accountIsLiability =
191
+ accountType === 'creditCard' ||
192
+ accountType === 'lineOfCredit' ||
193
+ accountType === 'mortgage' ||
194
+ accountType === 'autoLoan' ||
195
+ accountType === 'studentLoan' ||
196
+ accountType === 'personalLoan' ||
197
+ accountType === 'medicalDebt' ||
198
+ accountType === 'otherDebt' ||
199
+ accountType === 'otherLiability';
200
+
201
+ // Determine whether to invert bank amounts
202
+ // If invert_bank_amounts is explicitly set, use that value
203
+ // Otherwise, default to true for liability accounts (legacy behavior)
204
+ // Note: Some banks (e.g., Wealthsimple) show charges as negative already, matching YNAB
205
+ const shouldInvertBankAmounts =
206
+ params.invert_bank_amounts !== undefined ? params.invert_bank_amounts : accountIsLiability;
207
+
208
+ // Negate statement balance for liability accounts
209
+ const adjustedStatementBalance = accountIsLiability
210
+ ? -Math.abs(params.statement_balance)
211
+ : params.statement_balance;
212
+
213
+ const budgetResponse = await ynabAPI.budgets.getBudgetById(params.budget_id);
214
+ const currencyCode = budgetResponse.data.budget?.currency_format?.iso_code ?? 'USD';
215
+
216
+ // Fetch YNAB transactions for the account
217
+ // Auto-detect date range from CSV if not explicitly provided
218
+ let sinceDate: Date;
219
+
220
+ if (params.statement_start_date) {
221
+ // User provided explicit start date
222
+ sinceDate = new Date(params.statement_start_date);
223
+ } else {
224
+ // Auto-detect from CSV content
225
+ try {
226
+ const csvContent = params.csv_data || params.csv_file_path || '';
227
+ const csvFormat = params.csv_format || autoDetectCSVFormat(csvContent);
228
+
229
+ // Convert schema format to parser format
230
+ const parserFormat = {
231
+ date_column: csvFormat.date_column || 'Date',
232
+ amount_column: csvFormat.amount_column,
233
+ debit_column: csvFormat.debit_column,
234
+ credit_column: csvFormat.credit_column,
235
+ description_column: csvFormat.description_column || 'Description',
236
+ date_format: csvFormat.date_format || 'MM/DD/YYYY',
237
+ has_header: csvFormat.has_header ?? true,
238
+ delimiter: csvFormat.delimiter || ',',
239
+ };
240
+
241
+ const { minDate } = extractDateRangeFromCSV(csvContent, parserFormat);
242
+
243
+ // Add 7-day buffer before min date for pending transactions
244
+ const minDateObj = new Date(minDate);
245
+ minDateObj.setDate(minDateObj.getDate() - 7);
246
+ sinceDate = minDateObj;
247
+ } catch {
248
+ // Fallback to 90 days if CSV parsing fails
249
+ sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
250
+ }
251
+ }
252
+
253
+ const sinceDateString = sinceDate.toISOString().split('T')[0];
254
+ const transactionsResult = forceFullRefresh
255
+ ? await deltaFetcher.fetchTransactionsByAccountFull(
256
+ params.budget_id,
257
+ params.account_id,
258
+ sinceDateString,
259
+ )
260
+ : await deltaFetcher.fetchTransactionsByAccount(
261
+ params.budget_id,
262
+ params.account_id,
263
+ sinceDateString,
264
+ );
265
+
266
+ const ynabTransactions = transactionsResult.data;
267
+
268
+ const auditMetadata = {
269
+ data_freshness: getDataFreshness(transactionsResult, forceFullRefresh),
270
+ data_source: getAuditDataSource(transactionsResult, forceFullRefresh),
271
+ server_knowledge: transactionsResult.serverKnowledge,
272
+ fetched_at: new Date().toISOString(),
273
+ accounts_count: accountResult.data.length,
274
+ transactions_count: transactionsResult.data.length,
275
+ cache_status: {
276
+ accounts_cached: accountResult.wasCached,
277
+ transactions_cached: transactionsResult.wasCached,
278
+ delta_merge_applied: transactionsResult.usedDelta,
279
+ },
280
+ };
281
+
282
+ // Perform analysis
283
+ const analysis = analyzeReconciliation(
284
+ params.csv_data || params.csv_file_path || '',
285
+ params.csv_file_path,
286
+ ynabTransactions,
287
+ adjustedStatementBalance,
288
+ config,
289
+ currencyCode,
290
+ params.account_id,
291
+ params.budget_id,
292
+ shouldInvertBankAmounts,
293
+ );
294
+
295
+ const initialAccount: AccountSnapshot = {
296
+ balance: accountData.balance,
297
+ cleared_balance: accountData.cleared_balance,
298
+ uncleared_balance: accountData.uncleared_balance,
299
+ };
300
+
301
+ let executionData: LegacyReconciliationResult | undefined;
302
+ const wantsBalanceVerification = Boolean(params.statement_date);
303
+ const shouldExecute =
304
+ params.auto_create_transactions ||
305
+ params.auto_update_cleared_status ||
306
+ params.auto_unclear_missing ||
307
+ params.auto_adjust_dates ||
308
+ params.balance_verification_mode !== 'ANALYSIS_ONLY' ||
309
+ wantsBalanceVerification;
310
+
311
+ if (shouldExecute) {
312
+ executionData = await executeReconciliation({
313
+ ynabAPI,
314
+ analysis,
315
+ params,
316
+ budgetId: params.budget_id,
317
+ accountId: params.account_id,
318
+ initialAccount,
319
+ currencyCode,
320
+ });
321
+ }
322
+
323
+ const csvFormatForPayload = mapCsvFormatForPayload(params.csv_format);
324
+
325
+ const adapterOptions: Parameters<typeof buildReconciliationPayload>[1] = {
326
+ accountName,
327
+ accountId: params.account_id,
328
+ currencyCode,
329
+ auditMetadata,
330
+ };
331
+ if (csvFormatForPayload !== undefined) {
332
+ adapterOptions.csvFormat = csvFormatForPayload;
333
+ }
334
+
335
+ const payload = buildReconciliationPayload(analysis, adapterOptions, executionData);
336
+
337
+ // Build response payload matching ReconcileAccountOutputSchema
338
+ // Schema expects: { human: string } OR { human: string, structured: object }
339
+ const responseData: Record<string, unknown> = {
340
+ human: payload.human,
341
+ };
342
+
343
+ // Only include structured data if requested (can be very large)
344
+ if (params.include_structured_data) {
345
+ responseData['structured'] = payload.structured;
346
+ }
347
+
348
+ return {
349
+ content: [
350
+ {
351
+ type: 'text',
352
+ text: responseFormatter.format(responseData),
353
+ },
354
+ ],
355
+ };
356
+ },
357
+ 'ynab:reconcile_account',
358
+ 'analyzing account reconciliation',
359
+ );
360
+ }
361
+
362
+ function mapCsvFormatForPayload(format: ReconcileAccountRequest['csv_format'] | undefined):
363
+ | {
364
+ delimiter: string;
365
+ decimal_separator: string;
366
+ thousands_separator: string | null;
367
+ date_format: string;
368
+ header_row: boolean;
369
+ date_column: string | null;
370
+ amount_column: string | null;
371
+ payee_column: string | null;
372
+ }
373
+ | undefined {
374
+ if (!format) {
375
+ return undefined;
376
+ }
377
+
378
+ const coerceString = (value: string | number | undefined | null, fallback?: string) => {
379
+ if (value === undefined || value === null) {
380
+ return fallback ?? null;
381
+ }
382
+ return String(value);
383
+ };
384
+
385
+ const delimiter = coerceString(format.delimiter, ',');
386
+ const decimalSeparator = '.'; // Default decimal separator
387
+ const thousandsSeparator = ','; // Default thousands separator
388
+ const dateFormat = coerceString(format.date_format, 'MM/DD/YYYY');
389
+
390
+ return {
391
+ delimiter: delimiter ?? ',',
392
+ decimal_separator: decimalSeparator,
393
+ thousands_separator: thousandsSeparator,
394
+ date_format: dateFormat ?? 'MM/DD/YYYY',
395
+ header_row: format.has_header ?? true,
396
+ date_column: coerceString(format.date_column, '') ?? null,
397
+ amount_column: coerceString(format.amount_column, '') ?? null,
398
+ payee_column: coerceString(format.description_column, '') ?? null,
399
+ };
400
+ }
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Transaction matching algorithm for reconciliation
3
+ * Implements confidence-based matching with auto-match and suggestion tiers
4
+ */
5
+
6
+ import { normalizedMatch, payeeSimilarity } from './payeeNormalizer.js';
7
+ import { DEFAULT_MATCHING_CONFIG } from './types.js';
8
+ import type {
9
+ BankTransaction,
10
+ YNABTransaction,
11
+ TransactionMatch,
12
+ MatchCandidate,
13
+ MatchingConfig,
14
+ } from './types.js';
15
+
16
+ /**
17
+ * Check if two amounts match within tolerance
18
+ */
19
+ function amountsMatch(bankAmount: number, ynabAmount: number, toleranceCents: number): boolean {
20
+ // Convert YNAB milliunits to dollars
21
+ const ynabDollars = ynabAmount / 1000;
22
+
23
+ // Round to avoid floating point precision issues
24
+ const difference = Math.round(Math.abs(bankAmount - ynabDollars) * 100) / 100;
25
+ const toleranceDollars = toleranceCents / 100;
26
+
27
+ return difference <= toleranceDollars;
28
+ }
29
+
30
+ /**
31
+ * Check if two dates match within tolerance
32
+ */
33
+ function datesMatch(date1: string, date2: string, toleranceDays: number): boolean {
34
+ const d1 = new Date(date1);
35
+ const d2 = new Date(date2);
36
+
37
+ const diffMs = Math.abs(d1.getTime() - d2.getTime());
38
+ const diffDays = diffMs / (1000 * 60 * 60 * 24);
39
+
40
+ return diffDays <= toleranceDays;
41
+ }
42
+
43
+ /**
44
+ * Calculate match confidence score between bank and YNAB transaction
45
+ * Returns score 0-100 and match reasons
46
+ */
47
+ function calculateMatchScore(
48
+ bankTxn: BankTransaction,
49
+ ynabTxn: YNABTransaction,
50
+ config: MatchingConfig,
51
+ ): { score: number; reasons: string[] } {
52
+ const reasons: string[] = [];
53
+ let score = 0;
54
+
55
+ // Amount match (40% weight) - REQUIRED
56
+ const amountMatch = amountsMatch(bankTxn.amount, ynabTxn.amount, config.amountToleranceCents);
57
+ if (!amountMatch) {
58
+ return { score: 0, reasons: ['Amount does not match'] };
59
+ }
60
+ score += 40;
61
+ reasons.push('Amount matches');
62
+
63
+ // Date match (40% weight)
64
+ const dateWithinTolerance = datesMatch(bankTxn.date, ynabTxn.date, config.dateToleranceDays);
65
+ if (dateWithinTolerance) {
66
+ score += 40;
67
+ const daysDiff = Math.abs(
68
+ (new Date(bankTxn.date).getTime() - new Date(ynabTxn.date).getTime()) / (1000 * 60 * 60 * 24),
69
+ );
70
+ if (daysDiff === 0) {
71
+ reasons.push('Exact date match');
72
+ } else {
73
+ reasons.push(`Date within ${Math.round(daysDiff)} days`);
74
+ }
75
+ }
76
+
77
+ // Payee match (20% weight)
78
+ const payeeScore = payeeSimilarity(bankTxn.payee, ynabTxn.payee_name);
79
+
80
+ if (normalizedMatch(bankTxn.payee, ynabTxn.payee_name)) {
81
+ score += 20;
82
+ reasons.push('Payee exact match');
83
+ } else if (payeeScore >= 95) {
84
+ score += 15;
85
+ reasons.push(`Payee highly similar (${Math.round(payeeScore)}%)`);
86
+ } else if (payeeScore >= 80) {
87
+ score += 10;
88
+ reasons.push(`Payee similar (${Math.round(payeeScore)}%)`);
89
+ } else if (payeeScore >= 60) {
90
+ score += 6;
91
+ reasons.push(`Payee somewhat similar (${Math.round(payeeScore)}%)`);
92
+ }
93
+
94
+ return { score: Math.round(score), reasons };
95
+ }
96
+
97
+ /**
98
+ * Priority scoring for YNAB transactions
99
+ * Uncleared transactions get higher priority than cleared ones
100
+ */
101
+ function getPriority(ynabTxn: YNABTransaction): number {
102
+ // Uncleared transactions are expecting bank confirmation
103
+ if (ynabTxn.cleared === 'uncleared') return 10;
104
+ if (ynabTxn.cleared === 'cleared') return 5;
105
+ if (ynabTxn.cleared === 'reconciled') return 1;
106
+ return 0;
107
+ }
108
+
109
+ /**
110
+ * Find all matching candidates for a bank transaction
111
+ */
112
+ function findMatchCandidates(
113
+ bankTxn: BankTransaction,
114
+ ynabTransactions: YNABTransaction[],
115
+ usedIds: Set<string>,
116
+ config: MatchingConfig,
117
+ ): MatchCandidate[] {
118
+ const candidates: MatchCandidate[] = [];
119
+
120
+ for (const ynabTxn of ynabTransactions) {
121
+ // Skip already matched transactions
122
+ if (usedIds.has(ynabTxn.id)) continue;
123
+
124
+ // Skip opposite-signed transactions (refunds vs purchases)
125
+ if (bankTxn.amount > 0 !== ynabTxn.amount > 0) continue;
126
+
127
+ // Calculate match score
128
+ const { score, reasons } = calculateMatchScore(bankTxn, ynabTxn, config);
129
+
130
+ // Only include candidates with minimum score
131
+ if (score >= 30) {
132
+ candidates.push({
133
+ ynab_transaction: ynabTxn,
134
+ confidence: score,
135
+ match_reason: reasons.join(', '),
136
+ explanation: buildExplanation(bankTxn, ynabTxn, score, reasons),
137
+ });
138
+ }
139
+ }
140
+
141
+ // Sort by confidence (desc), then priority (desc), then date proximity
142
+ candidates.sort((a, b) => {
143
+ if (b.confidence !== a.confidence) {
144
+ return b.confidence - a.confidence;
145
+ }
146
+ const priorityDiff = getPriority(b.ynab_transaction) - getPriority(a.ynab_transaction);
147
+ if (priorityDiff !== 0) return priorityDiff;
148
+
149
+ // Date proximity as tiebreaker
150
+ const dateProximityA = Math.abs(
151
+ new Date(bankTxn.date).getTime() - new Date(a.ynab_transaction.date).getTime(),
152
+ );
153
+ const dateProximityB = Math.abs(
154
+ new Date(bankTxn.date).getTime() - new Date(b.ynab_transaction.date).getTime(),
155
+ );
156
+ return dateProximityA - dateProximityB;
157
+ });
158
+
159
+ return candidates;
160
+ }
161
+
162
+ /**
163
+ * Build human-readable explanation for a match
164
+ */
165
+ function buildExplanation(
166
+ _bankTxn: BankTransaction,
167
+ ynabTxn: YNABTransaction,
168
+ score: number,
169
+ reasons: string[],
170
+ ): string {
171
+ const parts: string[] = [];
172
+
173
+ parts.push(`Match confidence: ${score}%`);
174
+ parts.push(reasons.join(', '));
175
+
176
+ if (ynabTxn.cleared === 'uncleared') {
177
+ parts.push('(Uncleared - awaiting confirmation)');
178
+ }
179
+
180
+ return parts.join(' | ');
181
+ }
182
+
183
+ /**
184
+ * Find best match for a single bank transaction
185
+ */
186
+ export function findBestMatch(
187
+ bankTxn: BankTransaction,
188
+ ynabTransactions: YNABTransaction[],
189
+ usedIds: Set<string>,
190
+ config: MatchingConfig,
191
+ ): TransactionMatch {
192
+ const candidates = findMatchCandidates(bankTxn, ynabTransactions, usedIds, config);
193
+
194
+ if (candidates.length === 0) {
195
+ // No match found
196
+ return {
197
+ bank_transaction: bankTxn,
198
+ confidence: 'none',
199
+ confidence_score: 0,
200
+ match_reason: 'No matching transaction found in YNAB',
201
+ action_hint: 'add_to_ynab',
202
+ recommendation: 'This transaction appears on bank statement but not in YNAB',
203
+ };
204
+ }
205
+
206
+ const bestCandidate = candidates[0]!; // Safe: we checked candidates.length > 0
207
+ const bestScore = bestCandidate.confidence;
208
+
209
+ // HIGH confidence: Auto-match candidate (≥90%)
210
+ if (bestScore >= config.autoMatchThreshold) {
211
+ return {
212
+ bank_transaction: bankTxn,
213
+ ynab_transaction: bestCandidate.ynab_transaction,
214
+ confidence: 'high',
215
+ confidence_score: bestScore,
216
+ match_reason: bestCandidate.match_reason,
217
+ };
218
+ }
219
+
220
+ // MEDIUM confidence: Suggested match (60-89%)
221
+ if (bestScore >= config.suggestionThreshold) {
222
+ return {
223
+ bank_transaction: bankTxn,
224
+ ynab_transaction: bestCandidate.ynab_transaction,
225
+ candidates: candidates.slice(0, 3), // Top 3 candidates
226
+ confidence: 'medium',
227
+ confidence_score: bestScore,
228
+ match_reason: bestCandidate.match_reason,
229
+ top_confidence: bestScore,
230
+ action_hint: 'review_and_choose',
231
+ };
232
+ }
233
+
234
+ // LOW confidence: Show as possible match but don't auto-suggest (30-59%)
235
+ return {
236
+ bank_transaction: bankTxn,
237
+ candidates: candidates.slice(0, 3),
238
+ confidence: 'low',
239
+ confidence_score: bestScore,
240
+ match_reason: 'Low confidence match',
241
+ top_confidence: bestScore,
242
+ action_hint: 'review_or_add_new',
243
+ recommendation: 'Consider reviewing candidates or adding as new transaction',
244
+ };
245
+ }
246
+
247
+ /**
248
+ * Find matches for all bank transactions
249
+ */
250
+ export function findMatches(
251
+ bankTransactions: BankTransaction[],
252
+ ynabTransactions: YNABTransaction[],
253
+ config: MatchingConfig = DEFAULT_MATCHING_CONFIG as MatchingConfig,
254
+ ): TransactionMatch[] {
255
+ const matches: TransactionMatch[] = [];
256
+ const usedIds = new Set<string>();
257
+
258
+ for (const bankTxn of bankTransactions) {
259
+ const match = findBestMatch(bankTxn, ynabTransactions, usedIds, config);
260
+ matches.push(match);
261
+
262
+ // Mark high-confidence matches as used to prevent duplicate matching
263
+ if (match.confidence === 'high' && match.ynab_transaction) {
264
+ usedIds.add(match.ynab_transaction.id);
265
+ }
266
+ }
267
+
268
+ return matches;
269
+ }