@dizzlkheinz/ynab-mcpb 0.12.2 → 0.15.0

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 (262) hide show
  1. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
  2. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
  3. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
  4. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
  5. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
  6. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
  7. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
  8. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
  9. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
  10. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
  11. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
  12. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
  13. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
  14. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
  15. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
  16. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
  17. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
  18. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
  19. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
  20. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
  21. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
  22. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
  23. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
  24. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
  25. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
  26. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
  27. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
  28. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
  29. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
  30. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
  31. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
  32. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
  33. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
  34. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
  35. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
  36. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
  37. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
  38. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
  39. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
  40. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
  41. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
  42. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
  43. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
  44. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
  45. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
  46. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
  47. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
  48. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
  49. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
  50. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
  51. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
  52. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
  53. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
  54. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
  55. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
  56. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
  57. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
  58. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
  59. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
  60. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
  61. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
  62. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
  63. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
  64. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
  65. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
  66. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
  67. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
  68. package/.github/workflows/ci-tests.yml +6 -2
  69. package/.github/workflows/publish.yml +3 -3
  70. package/.github/workflows/release.yml +4 -0
  71. package/CHANGELOG.md +89 -1
  72. package/NUL +1 -1
  73. package/README.md +36 -10
  74. package/dist/bundle/index.cjs +65 -42
  75. package/dist/index.js +9 -20
  76. package/dist/server/YNABMCPServer.d.ts +2 -1
  77. package/dist/server/YNABMCPServer.js +61 -27
  78. package/dist/server/cacheKeys.d.ts +8 -0
  79. package/dist/server/cacheKeys.js +8 -0
  80. package/dist/server/config.d.ts +22 -3
  81. package/dist/server/config.js +16 -17
  82. package/dist/server/errorHandler.d.ts +2 -0
  83. package/dist/server/errorHandler.js +49 -5
  84. package/dist/server/securityMiddleware.js +3 -6
  85. package/dist/server/toolRegistry.js +8 -10
  86. package/dist/tools/accountTools.js +4 -3
  87. package/dist/tools/categoryTools.js +8 -7
  88. package/dist/tools/monthTools.js +2 -1
  89. package/dist/tools/payeeTools.js +2 -1
  90. package/dist/tools/reconcileAdapter.js +10 -5
  91. package/dist/tools/reconciliation/analyzer.d.ts +4 -2
  92. package/dist/tools/reconciliation/analyzer.js +120 -404
  93. package/dist/tools/reconciliation/csvParser.d.ts +51 -0
  94. package/dist/tools/reconciliation/csvParser.js +413 -0
  95. package/dist/tools/reconciliation/executor.d.ts +8 -0
  96. package/dist/tools/reconciliation/executor.js +277 -50
  97. package/dist/tools/reconciliation/index.d.ts +7 -7
  98. package/dist/tools/reconciliation/index.js +115 -39
  99. package/dist/tools/reconciliation/matcher.d.ts +24 -3
  100. package/dist/tools/reconciliation/matcher.js +175 -133
  101. package/dist/tools/reconciliation/recommendationEngine.js +22 -18
  102. package/dist/tools/reconciliation/reportFormatter.js +9 -8
  103. package/dist/tools/reconciliation/signDetector.d.ts +2 -0
  104. package/dist/tools/reconciliation/signDetector.js +54 -0
  105. package/dist/tools/reconciliation/types.d.ts +20 -34
  106. package/dist/tools/reconciliation/types.js +1 -7
  107. package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
  108. package/dist/tools/reconciliation/ynabAdapter.js +15 -0
  109. package/dist/tools/transactionTools.d.ts +3 -17
  110. package/dist/tools/transactionTools.js +5 -17
  111. package/dist/types/reconciliation.d.ts +24 -0
  112. package/dist/types/reconciliation.js +1 -0
  113. package/dist/utils/baseError.d.ts +3 -0
  114. package/dist/utils/baseError.js +7 -0
  115. package/dist/utils/errors.d.ts +13 -0
  116. package/dist/utils/errors.js +15 -0
  117. package/dist/utils/validationError.d.ts +3 -0
  118. package/dist/utils/validationError.js +3 -0
  119. package/docs/guides/ARCHITECTURE.md +12 -129
  120. package/docs/plans/2025-11-20-reloadable-config-token-validation.md +93 -0
  121. package/docs/plans/2025-11-21-fix-transaction-cached-property.md +362 -0
  122. package/docs/plans/2025-11-21-reconciliation-error-handling.md +90 -0
  123. package/docs/plans/2025-11-21-v014-hardening.md +153 -0
  124. package/docs/plans/reconciliation-v2-redesign.md +1571 -0
  125. package/package.json +8 -2
  126. package/scripts/run-throttled-integration-tests.js +9 -3
  127. package/scripts/test-recommendations.ts +1 -1
  128. package/src/__tests__/performance.test.ts +12 -5
  129. package/src/__tests__/testUtils.ts +62 -5
  130. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
  131. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
  132. package/src/__tests__/workflows.e2e.test.ts +33 -0
  133. package/src/index.ts +8 -31
  134. package/src/server/YNABMCPServer.ts +81 -42
  135. package/src/server/__tests__/YNABMCPServer.integration.test.ts +10 -12
  136. package/src/server/__tests__/YNABMCPServer.test.ts +27 -15
  137. package/src/server/__tests__/config.test.ts +76 -152
  138. package/src/server/__tests__/server-startup.integration.test.ts +42 -14
  139. package/src/server/__tests__/toolRegistry.test.ts +1 -1
  140. package/src/server/cacheKeys.ts +8 -0
  141. package/src/server/config.ts +20 -38
  142. package/src/server/errorHandler.ts +52 -5
  143. package/src/server/securityMiddleware.ts +3 -7
  144. package/src/server/toolRegistry.ts +14 -10
  145. package/src/tools/__tests__/categoryTools.test.ts +37 -19
  146. package/src/tools/__tests__/transactionTools.test.ts +58 -2
  147. package/src/tools/accountTools.ts +8 -3
  148. package/src/tools/categoryTools.ts +12 -7
  149. package/src/tools/monthTools.ts +7 -1
  150. package/src/tools/payeeTools.ts +7 -1
  151. package/src/tools/reconcileAdapter.ts +10 -5
  152. package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
  153. package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
  154. package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
  155. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +26 -6
  156. package/src/tools/reconciliation/__tests__/executor.test.ts +133 -60
  157. package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
  158. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
  159. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
  160. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
  161. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
  162. package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
  163. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
  164. package/src/tools/reconciliation/analyzer.ts +174 -545
  165. package/src/tools/reconciliation/csvParser.ts +617 -0
  166. package/src/tools/reconciliation/executor.ts +344 -58
  167. package/src/tools/reconciliation/index.ts +141 -48
  168. package/src/tools/reconciliation/matcher.ts +234 -214
  169. package/src/tools/reconciliation/recommendationEngine.ts +23 -19
  170. package/src/tools/reconciliation/reportFormatter.ts +16 -11
  171. package/src/tools/reconciliation/signDetector.ts +117 -0
  172. package/src/tools/reconciliation/types.ts +39 -61
  173. package/src/tools/reconciliation/ynabAdapter.ts +33 -0
  174. package/src/tools/schemas/outputs/utilityOutputs.ts +1 -1
  175. package/src/tools/transactionTools.ts +7 -18
  176. package/src/types/reconciliation.ts +49 -0
  177. package/src/utils/baseError.ts +7 -0
  178. package/src/utils/errors.ts +21 -0
  179. package/src/utils/validationError.ts +3 -0
  180. package/temp-recon.ts +126 -0
  181. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
  182. package/test_mcp_tools.mjs +75 -0
  183. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
  184. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
  185. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
  186. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
  187. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
  188. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
  189. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
  190. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
  191. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
  192. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
  193. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
  194. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
  195. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
  196. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
  197. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
  198. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
  199. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
  200. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
  201. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
  202. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
  203. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
  204. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
  205. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
  206. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
  207. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
  208. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
  209. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
  210. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
  211. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
  212. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
  213. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
  214. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
  215. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
  216. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
  217. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
  218. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
  219. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
  220. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
  221. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
  222. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
  223. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
  224. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
  225. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
  226. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
  227. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
  228. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
  229. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
  230. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
  231. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
  232. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
  233. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
  234. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
  235. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
  236. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
  237. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
  238. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
  239. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
  240. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
  241. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
  242. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
  243. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
  244. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
  245. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
  246. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
  247. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
  248. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
  249. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
  250. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
  251. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
  252. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
  253. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
  254. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
  255. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
  256. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
  257. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
  258. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
  259. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
  260. package/.gemini/settings.json +0 -8
  261. package/ADOS-2-Module-1-Complete-Manual.md +0 -757
  262. package/WARP.md +0 -245
@@ -1,11 +1,14 @@
1
+ import { promises as fs } from 'fs';
1
2
  import { z } from 'zod/v4';
2
3
  import { withToolErrorHandling } from '../../types/index.js';
3
4
  import { analyzeReconciliation } from './analyzer.js';
4
5
  import { buildReconciliationPayload } from '../reconcileAdapter.js';
5
6
  import { executeReconciliation, } from './executor.js';
6
7
  import { responseFormatter } from '../../server/responseFormatter.js';
7
- import { extractDateRangeFromCSV, autoDetectCSVFormat } from '../compareTransactions/parser.js';
8
+ import { parseCSV } from './csvParser.js';
8
9
  import { resolveDeltaFetcherArgs } from '../deltaSupport.js';
10
+ import { detectSignInversion } from './signDetector.js';
11
+ import { normalizeYNABTransactions } from './ynabAdapter.js';
9
12
  export { analyzeReconciliation } from './analyzer.js';
10
13
  export { findMatches, findBestMatch } from './matcher.js';
11
14
  export { normalizePayee, normalizedMatch, fuzzyMatch, payeeSimilarity } from './payeeNormalizer.js';
@@ -38,25 +41,17 @@ export const ReconcileAccountSchema = z
38
41
  csv_data: z.string().optional(),
39
42
  csv_format: z
40
43
  .object({
41
- date_column: z.union([z.string(), z.number()]).optional().default('Date'),
44
+ date_column: z.union([z.string(), z.number()]).optional(),
42
45
  amount_column: z.union([z.string(), z.number()]).optional(),
43
46
  debit_column: z.union([z.string(), z.number()]).optional(),
44
47
  credit_column: z.union([z.string(), z.number()]).optional(),
45
- description_column: z.union([z.string(), z.number()]).optional().default('Description'),
46
- date_format: z.string().optional().default('MM/DD/YYYY'),
47
- has_header: z.boolean().optional().default(true),
48
- delimiter: z.string().optional().default(','),
48
+ description_column: z.union([z.string(), z.number()]).optional(),
49
+ date_format: z.string().optional(),
50
+ has_header: z.boolean().optional(),
51
+ delimiter: z.string().optional(),
49
52
  })
50
53
  .strict()
51
- .optional()
52
- .default(() => ({
53
- date_column: 'Date',
54
- amount_column: 'Amount',
55
- description_column: 'Description',
56
- date_format: 'MM/DD/YYYY',
57
- has_header: true,
58
- delimiter: ',',
59
- })),
54
+ .optional(),
60
55
  statement_balance: z.number({
61
56
  message: 'Statement balance is required and must be a number',
62
57
  }),
@@ -65,9 +60,9 @@ export const ReconcileAccountSchema = z
65
60
  statement_date: z.string().optional(),
66
61
  expected_bank_balance: z.number().optional(),
67
62
  as_of_timezone: z.string().optional(),
68
- date_tolerance_days: z.number().min(0).max(7).optional().default(5),
63
+ date_tolerance_days: z.number().min(0).max(7).optional().default(7),
69
64
  amount_tolerance_cents: z.number().min(0).max(100).optional().default(1),
70
- auto_match_threshold: z.number().min(0).max(100).optional().default(90),
65
+ auto_match_threshold: z.number().min(0).max(100).optional().default(85),
71
66
  suggestion_threshold: z.number().min(0).max(100).optional().default(60),
72
67
  amount_tolerance: z.number().min(0).max(1).optional(),
73
68
  auto_create_transactions: z.boolean().optional().default(false),
@@ -95,11 +90,19 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
95
90
  const forceFullRefresh = params.force_full_refresh ?? true;
96
91
  return await withToolErrorHandling(async () => {
97
92
  const config = {
98
- dateToleranceDays: params.date_tolerance_days,
99
- amountToleranceCents: params.amount_tolerance_cents,
100
- descriptionSimilarityThreshold: 0.8,
101
- autoMatchThreshold: params.auto_match_threshold,
102
- suggestionThreshold: params.suggestion_threshold,
93
+ weights: {
94
+ amount: 0.5,
95
+ date: 0.15,
96
+ payee: 0.35,
97
+ },
98
+ dateToleranceDays: params.date_tolerance_days ?? 5,
99
+ amountToleranceMilliunits: (params.amount_tolerance_cents ?? 1) * 10,
100
+ autoMatchThreshold: params.auto_match_threshold ?? 90,
101
+ suggestedMatchThreshold: params.suggestion_threshold ?? 60,
102
+ minimumCandidateScore: 40,
103
+ exactAmountBonus: 10,
104
+ exactDateBonus: 5,
105
+ exactPayeeBonus: 10,
103
106
  };
104
107
  const accountResult = forceFullRefresh
105
108
  ? await deltaFetcher.fetchAccountsFull(params.budget_id)
@@ -125,28 +128,70 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
125
128
  : params.statement_balance;
126
129
  const budgetResponse = await ynabAPI.budgets.getBudgetById(params.budget_id);
127
130
  const currencyCode = budgetResponse.data.budget?.currency_format?.iso_code ?? 'USD';
131
+ const dateFormat = mapCsvDateFormatToHint(params.csv_format?.date_format);
132
+ const csvOptions = {
133
+ columns: {
134
+ ...(params.csv_format?.date_column !== undefined && {
135
+ date: String(params.csv_format.date_column),
136
+ }),
137
+ ...(params.csv_format?.amount_column !== undefined && {
138
+ amount: String(params.csv_format.amount_column),
139
+ }),
140
+ ...(params.csv_format?.debit_column !== undefined && {
141
+ debit: String(params.csv_format.debit_column),
142
+ }),
143
+ ...(params.csv_format?.credit_column !== undefined && {
144
+ credit: String(params.csv_format.credit_column),
145
+ }),
146
+ ...(params.csv_format?.description_column !== undefined && {
147
+ description: String(params.csv_format.description_column),
148
+ }),
149
+ },
150
+ ...(dateFormat && { dateFormat }),
151
+ ...(params.csv_format?.has_header !== undefined && {
152
+ header: params.csv_format.has_header,
153
+ }),
154
+ };
155
+ let csvContent = params.csv_data ?? '';
156
+ if (!csvContent && params.csv_file_path) {
157
+ try {
158
+ csvContent = await fs.readFile(params.csv_file_path, 'utf8');
159
+ }
160
+ catch (error) {
161
+ const message = error instanceof Error && error.message
162
+ ? error.message
163
+ : 'Unknown error while reading CSV file';
164
+ throw new Error(`Failed to read CSV file at path ${params.csv_file_path}: ${message}`);
165
+ }
166
+ }
128
167
  let sinceDate;
168
+ let parseResult;
129
169
  if (params.statement_start_date) {
130
170
  sinceDate = new Date(params.statement_start_date);
131
171
  }
132
172
  else {
133
173
  try {
134
- const csvContent = params.csv_data || params.csv_file_path || '';
135
- const csvFormat = params.csv_format || autoDetectCSVFormat(csvContent);
136
- const parserFormat = {
137
- date_column: csvFormat.date_column || 'Date',
138
- amount_column: csvFormat.amount_column,
139
- debit_column: csvFormat.debit_column,
140
- credit_column: csvFormat.credit_column,
141
- description_column: csvFormat.description_column || 'Description',
142
- date_format: csvFormat.date_format || 'MM/DD/YYYY',
143
- has_header: csvFormat.has_header ?? true,
144
- delimiter: csvFormat.delimiter || ',',
145
- };
146
- const { minDate } = extractDateRangeFromCSV(csvContent, parserFormat);
147
- const minDateObj = new Date(minDate);
148
- minDateObj.setDate(minDateObj.getDate() - 7);
149
- sinceDate = minDateObj;
174
+ parseResult = parseCSV(csvContent, {
175
+ ...csvOptions,
176
+ invertAmounts: shouldInvertBankAmounts,
177
+ });
178
+ if (parseResult.transactions.length > 0) {
179
+ const dates = parseResult.transactions
180
+ .map((t) => new Date(t.date).getTime())
181
+ .filter((t) => !isNaN(t));
182
+ if (dates.length > 0) {
183
+ const minTime = Math.min(...dates);
184
+ const minDateObj = new Date(minTime);
185
+ minDateObj.setDate(minDateObj.getDate() - 7);
186
+ sinceDate = minDateObj;
187
+ }
188
+ else {
189
+ sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
190
+ }
191
+ }
192
+ else {
193
+ sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
194
+ }
150
195
  }
151
196
  catch {
152
197
  sinceDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
@@ -157,6 +202,21 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
157
202
  ? await deltaFetcher.fetchTransactionsByAccountFull(params.budget_id, params.account_id, sinceDateString)
158
203
  : await deltaFetcher.fetchTransactionsByAccount(params.budget_id, params.account_id, sinceDateString);
159
204
  const ynabTransactions = transactionsResult.data;
205
+ let finalInvertAmounts = shouldInvertBankAmounts;
206
+ if (params.invert_bank_amounts === undefined && csvContent) {
207
+ const rawParseResult = parseCSV(csvContent, {
208
+ ...csvOptions,
209
+ invertAmounts: false,
210
+ });
211
+ if (rawParseResult.transactions.length > 0 && ynabTransactions.length > 0) {
212
+ const normalizedYNAB = normalizeYNABTransactions(ynabTransactions);
213
+ const needsInversion = detectSignInversion(rawParseResult.transactions, normalizedYNAB);
214
+ finalInvertAmounts = needsInversion;
215
+ if (needsInversion !== shouldInvertBankAmounts && parseResult) {
216
+ parseResult = undefined;
217
+ }
218
+ }
219
+ }
160
220
  const auditMetadata = {
161
221
  data_freshness: getDataFreshness(transactionsResult, forceFullRefresh),
162
222
  data_source: getAuditDataSource(transactionsResult, forceFullRefresh),
@@ -170,7 +230,7 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
170
230
  delta_merge_applied: transactionsResult.usedDelta,
171
231
  },
172
232
  };
173
- const analysis = analyzeReconciliation(params.csv_data || params.csv_file_path || '', params.csv_file_path, ynabTransactions, adjustedStatementBalance, config, currencyCode, params.account_id, params.budget_id, shouldInvertBankAmounts);
233
+ const analysis = analyzeReconciliation(parseResult ?? csvContent, params.csv_file_path, ynabTransactions, adjustedStatementBalance, config, currencyCode, params.account_id, params.budget_id, finalInvertAmounts, csvOptions);
174
234
  const initialAccount = {
175
235
  balance: accountData.balance,
176
236
  cleared_balance: accountData.cleared_balance,
@@ -222,6 +282,22 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
222
282
  };
223
283
  }, 'ynab:reconcile_account', 'analyzing account reconciliation');
224
284
  }
285
+ function mapCsvDateFormatToHint(format) {
286
+ if (!format) {
287
+ return undefined;
288
+ }
289
+ const normalized = format.toUpperCase().replace(/[^YMD]/g, '');
290
+ if (normalized === 'YYYYMMDD' || normalized === 'YYMMDD' || normalized === 'YMD') {
291
+ return 'YMD';
292
+ }
293
+ if (normalized === 'MMDDYYYY' || normalized === 'MDY') {
294
+ return 'MDY';
295
+ }
296
+ if (normalized === 'DDMMYYYY' || normalized === 'DMY') {
297
+ return 'DMY';
298
+ }
299
+ return undefined;
300
+ }
225
301
  function mapCsvFormatForPayload(format) {
226
302
  if (!format) {
227
303
  return undefined;
@@ -1,3 +1,24 @@
1
- import type { BankTransaction, YNABTransaction, TransactionMatch, MatchingConfig } from './types.js';
2
- export declare function findBestMatch(bankTxn: BankTransaction, ynabTransactions: YNABTransaction[], usedIds: Set<string>, config: MatchingConfig): TransactionMatch;
3
- export declare function findMatches(bankTransactions: BankTransaction[], ynabTransactions: YNABTransaction[], config?: MatchingConfig): TransactionMatch[];
1
+ import type { BankTransaction as CanonicalBankTransaction, NormalizedYNABTransaction } from '../../types/reconciliation.js';
2
+ import { type MatchingConfig } from './types.js';
3
+ export type { MatchingConfig };
4
+ export interface MatchCandidate {
5
+ ynabTransaction: NormalizedYNABTransaction;
6
+ scores: {
7
+ amount: number;
8
+ date: number;
9
+ payee: number;
10
+ combined: number;
11
+ };
12
+ matchReasons: string[];
13
+ }
14
+ export interface MatchResult {
15
+ bankTransaction: CanonicalBankTransaction;
16
+ bestMatch: MatchCandidate | null;
17
+ candidates: MatchCandidate[];
18
+ confidence: 'high' | 'medium' | 'low' | 'none';
19
+ confidenceScore: number;
20
+ }
21
+ export declare const DEFAULT_CONFIG: MatchingConfig;
22
+ export declare function normalizeConfig(config?: MatchingConfig): MatchingConfig;
23
+ export declare function findMatches(bankTransactions: CanonicalBankTransaction[], ynabTransactions: NormalizedYNABTransaction[], config?: MatchingConfig): MatchResult[];
24
+ export declare function findBestMatch(bankTransaction: CanonicalBankTransaction, ynabTransactions: NormalizedYNABTransaction[], usedYnabIds?: Set<string>, config?: MatchingConfig): MatchResult;
@@ -1,160 +1,202 @@
1
- import { normalizedMatch, payeeSimilarity } from './payeeNormalizer.js';
2
- import { DEFAULT_MATCHING_CONFIG } from './types.js';
3
- function amountsMatch(bankAmount, ynabAmount, toleranceCents) {
4
- const ynabDollars = ynabAmount / 1000;
5
- const difference = Math.round(Math.abs(bankAmount - ynabDollars) * 100) / 100;
6
- const toleranceDollars = toleranceCents / 100;
7
- return difference <= toleranceDollars;
8
- }
9
- function datesMatch(date1, date2, toleranceDays) {
10
- const d1 = new Date(date1);
11
- const d2 = new Date(date2);
12
- const diffMs = Math.abs(d1.getTime() - d2.getTime());
13
- const diffDays = diffMs / (1000 * 60 * 60 * 24);
14
- return diffDays <= toleranceDays;
15
- }
16
- function calculateMatchScore(bankTxn, ynabTxn, config) {
17
- const reasons = [];
18
- let score = 0;
19
- const amountMatch = amountsMatch(bankTxn.amount, ynabTxn.amount, config.amountToleranceCents);
20
- if (!amountMatch) {
21
- return { score: 0, reasons: ['Amount does not match'] };
22
- }
23
- score += 40;
24
- reasons.push('Amount matches');
25
- const dateWithinTolerance = datesMatch(bankTxn.date, ynabTxn.date, config.dateToleranceDays);
26
- if (dateWithinTolerance) {
27
- score += 40;
28
- const daysDiff = Math.abs((new Date(bankTxn.date).getTime() - new Date(ynabTxn.date).getTime()) / (1000 * 60 * 60 * 24));
29
- if (daysDiff === 0) {
30
- reasons.push('Exact date match');
31
- }
32
- else {
33
- reasons.push(`Date within ${Math.round(daysDiff)} days`);
34
- }
1
+ import * as fuzz from 'fuzzball';
2
+ export const DEFAULT_CONFIG = {
3
+ weights: {
4
+ amount: 0.5,
5
+ date: 0.15,
6
+ payee: 0.35,
7
+ },
8
+ amountToleranceMilliunits: 10,
9
+ dateToleranceDays: 7,
10
+ autoMatchThreshold: 85,
11
+ suggestedMatchThreshold: 60,
12
+ minimumCandidateScore: 40,
13
+ exactAmountBonus: 10,
14
+ exactDateBonus: 5,
15
+ exactPayeeBonus: 10,
16
+ };
17
+ export function normalizeConfig(config) {
18
+ if (!config) {
19
+ return { ...DEFAULT_CONFIG };
35
20
  }
36
- const payeeScore = payeeSimilarity(bankTxn.payee, ynabTxn.payee_name);
37
- if (normalizedMatch(bankTxn.payee, ynabTxn.payee_name)) {
38
- score += 20;
39
- reasons.push('Payee exact match');
21
+ return {
22
+ weights: config.weights ?? DEFAULT_CONFIG.weights,
23
+ amountToleranceMilliunits: config.amountToleranceMilliunits ?? DEFAULT_CONFIG.amountToleranceMilliunits,
24
+ dateToleranceDays: config.dateToleranceDays ?? DEFAULT_CONFIG.dateToleranceDays,
25
+ autoMatchThreshold: config.autoMatchThreshold ?? DEFAULT_CONFIG.autoMatchThreshold,
26
+ suggestedMatchThreshold: config.suggestedMatchThreshold ?? DEFAULT_CONFIG.suggestedMatchThreshold,
27
+ minimumCandidateScore: config.minimumCandidateScore ?? DEFAULT_CONFIG.minimumCandidateScore,
28
+ exactAmountBonus: config.exactAmountBonus ?? DEFAULT_CONFIG.exactAmountBonus,
29
+ exactDateBonus: config.exactDateBonus ?? DEFAULT_CONFIG.exactDateBonus,
30
+ exactPayeeBonus: config.exactPayeeBonus ?? DEFAULT_CONFIG.exactPayeeBonus,
31
+ };
32
+ }
33
+ function matchSingle(bankTxn, ynabTransactions, usedIds, configInput) {
34
+ const config = normalizeConfig(configInput);
35
+ const candidates = findCandidates(bankTxn, ynabTransactions, usedIds, config);
36
+ const bestMatch = candidates.length > 0 ? candidates[0] : null;
37
+ const confidenceScore = bestMatch?.scores.combined ?? 0;
38
+ let confidence;
39
+ if (confidenceScore >= config.autoMatchThreshold) {
40
+ confidence = 'high';
41
+ if (bestMatch)
42
+ usedIds.add(bestMatch.ynabTransaction.id);
40
43
  }
41
- else if (payeeScore >= 95) {
42
- score += 15;
43
- reasons.push(`Payee highly similar (${Math.round(payeeScore)}%)`);
44
+ else if (confidenceScore >= config.suggestedMatchThreshold) {
45
+ confidence = 'medium';
44
46
  }
45
- else if (payeeScore >= 80) {
46
- score += 10;
47
- reasons.push(`Payee similar (${Math.round(payeeScore)}%)`);
47
+ else if (confidenceScore >= config.minimumCandidateScore) {
48
+ confidence = 'low';
48
49
  }
49
- else if (payeeScore >= 60) {
50
- score += 6;
51
- reasons.push(`Payee somewhat similar (${Math.round(payeeScore)}%)`);
50
+ else {
51
+ confidence = 'none';
52
52
  }
53
- return { score: Math.round(score), reasons };
53
+ return {
54
+ bankTransaction: bankTxn,
55
+ bestMatch,
56
+ candidates: candidates.slice(0, 3),
57
+ confidence,
58
+ confidenceScore,
59
+ };
54
60
  }
55
- function getPriority(ynabTxn) {
56
- if (ynabTxn.cleared === 'uncleared')
57
- return 10;
58
- if (ynabTxn.cleared === 'cleared')
59
- return 5;
60
- if (ynabTxn.cleared === 'reconciled')
61
- return 1;
62
- return 0;
61
+ export function findMatches(bankTransactions, ynabTransactions, config) {
62
+ const usedYnabIds = new Set();
63
+ const results = [];
64
+ for (const bankTxn of bankTransactions) {
65
+ results.push(matchSingle(bankTxn, ynabTransactions, usedYnabIds, config));
66
+ }
67
+ return results;
63
68
  }
64
- function findMatchCandidates(bankTxn, ynabTransactions, usedIds, config) {
69
+ function findCandidates(bankTxn, ynabTransactions, usedIds, config) {
65
70
  const candidates = [];
66
71
  for (const ynabTxn of ynabTransactions) {
67
72
  if (usedIds.has(ynabTxn.id))
68
73
  continue;
69
- if (bankTxn.amount > 0 !== ynabTxn.amount > 0)
74
+ const bankSign = Math.sign(bankTxn.amount);
75
+ const ynabSign = Math.sign(ynabTxn.amount);
76
+ if (bankSign !== ynabSign && bankSign !== 0 && ynabSign !== 0) {
77
+ continue;
78
+ }
79
+ const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
80
+ if (amountDiff > config.amountToleranceMilliunits) {
70
81
  continue;
71
- const { score, reasons } = calculateMatchScore(bankTxn, ynabTxn, config);
72
- if (score >= 30) {
82
+ }
83
+ const scores = calculateScores(bankTxn, ynabTxn, config);
84
+ if (scores.combined >= config.minimumCandidateScore) {
73
85
  candidates.push({
74
- ynab_transaction: ynabTxn,
75
- confidence: score,
76
- match_reason: reasons.join(', '),
77
- explanation: buildExplanation(bankTxn, ynabTxn, score, reasons),
86
+ ynabTransaction: ynabTxn,
87
+ scores,
88
+ matchReasons: buildMatchReasons(scores, config),
78
89
  });
79
90
  }
80
91
  }
81
92
  candidates.sort((a, b) => {
82
- if (b.confidence !== a.confidence) {
83
- return b.confidence - a.confidence;
93
+ const scoreDiff = b.scores.combined - a.scores.combined;
94
+ if (scoreDiff !== 0) {
95
+ return scoreDiff;
96
+ }
97
+ const aUncleared = a.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
98
+ const bUncleared = b.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
99
+ if (aUncleared !== bUncleared) {
100
+ return bUncleared - aUncleared;
101
+ }
102
+ const bankTime = new Date(bankTxn.date).getTime();
103
+ const aDiff = Math.abs(bankTime - new Date(a.ynabTransaction.date).getTime());
104
+ const bDiff = Math.abs(bankTime - new Date(b.ynabTransaction.date).getTime());
105
+ if (aDiff !== bDiff) {
106
+ return aDiff - bDiff;
84
107
  }
85
- const priorityDiff = getPriority(b.ynab_transaction) - getPriority(a.ynab_transaction);
86
- if (priorityDiff !== 0)
87
- return priorityDiff;
88
- const dateProximityA = Math.abs(new Date(bankTxn.date).getTime() - new Date(a.ynab_transaction.date).getTime());
89
- const dateProximityB = Math.abs(new Date(bankTxn.date).getTime() - new Date(b.ynab_transaction.date).getTime());
90
- return dateProximityA - dateProximityB;
108
+ return 0;
91
109
  });
92
110
  return candidates;
93
111
  }
94
- function buildExplanation(_bankTxn, ynabTxn, score, reasons) {
95
- const parts = [];
96
- parts.push(`Match confidence: ${score}%`);
97
- parts.push(reasons.join(', '));
98
- if (ynabTxn.cleared === 'uncleared') {
99
- parts.push('(Uncleared - awaiting confirmation)');
100
- }
101
- return parts.join(' | ');
102
- }
103
- export function findBestMatch(bankTxn, ynabTransactions, usedIds, config) {
104
- const candidates = findMatchCandidates(bankTxn, ynabTransactions, usedIds, config);
105
- if (candidates.length === 0) {
106
- return {
107
- bank_transaction: bankTxn,
108
- confidence: 'none',
109
- confidence_score: 0,
110
- match_reason: 'No matching transaction found in YNAB',
111
- action_hint: 'add_to_ynab',
112
- recommendation: 'This transaction appears on bank statement but not in YNAB',
113
- };
114
- }
115
- const bestCandidate = candidates[0];
116
- const bestScore = bestCandidate.confidence;
117
- if (bestScore >= config.autoMatchThreshold) {
118
- return {
119
- bank_transaction: bankTxn,
120
- ynab_transaction: bestCandidate.ynab_transaction,
121
- confidence: 'high',
122
- confidence_score: bestScore,
123
- match_reason: bestCandidate.match_reason,
124
- };
125
- }
126
- if (bestScore >= config.suggestionThreshold) {
127
- return {
128
- bank_transaction: bankTxn,
129
- ynab_transaction: bestCandidate.ynab_transaction,
130
- candidates: candidates.slice(0, 3),
131
- confidence: 'medium',
132
- confidence_score: bestScore,
133
- match_reason: bestCandidate.match_reason,
134
- top_confidence: bestScore,
135
- action_hint: 'review_and_choose',
136
- };
112
+ function calculateScores(bankTxn, ynabTxn, config) {
113
+ const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
114
+ let amountScore;
115
+ if (amountDiff === 0) {
116
+ amountScore = 100;
117
+ }
118
+ else if (amountDiff <= config.amountToleranceMilliunits) {
119
+ amountScore = 95;
137
120
  }
121
+ else if (amountDiff <= 1000) {
122
+ amountScore = 80 - (amountDiff / 1000) * 20;
123
+ }
124
+ else {
125
+ amountScore = Math.max(0, 60 - (amountDiff / 1000) * 5);
126
+ }
127
+ const bankDate = new Date(bankTxn.date);
128
+ const ynabDate = new Date(ynabTxn.date);
129
+ const daysDiff = Math.abs(bankDate.getTime() - ynabDate.getTime()) / (1000 * 60 * 60 * 24);
130
+ let dateScore;
131
+ if (daysDiff < 0.5) {
132
+ dateScore = 100;
133
+ }
134
+ else if (daysDiff <= 1) {
135
+ dateScore = 95;
136
+ }
137
+ else if (daysDiff <= config.dateToleranceDays) {
138
+ dateScore = 90 - (daysDiff - 1) * (40 / config.dateToleranceDays);
139
+ }
140
+ else {
141
+ dateScore = Math.max(0, 50 - (daysDiff - config.dateToleranceDays) * 5);
142
+ }
143
+ const payeeScore = calculatePayeeScore(bankTxn.payee, ynabTxn.payee);
144
+ let combined = amountScore * config.weights.amount +
145
+ dateScore * config.weights.date +
146
+ payeeScore * config.weights.payee;
147
+ if (amountScore === 100)
148
+ combined += config.exactAmountBonus;
149
+ if (dateScore === 100)
150
+ combined += config.exactDateBonus;
151
+ if (payeeScore >= 95)
152
+ combined += config.exactPayeeBonus;
153
+ combined = Math.min(100, combined);
138
154
  return {
139
- bank_transaction: bankTxn,
140
- candidates: candidates.slice(0, 3),
141
- confidence: 'low',
142
- confidence_score: bestScore,
143
- match_reason: 'Low confidence match',
144
- top_confidence: bestScore,
145
- action_hint: 'review_or_add_new',
146
- recommendation: 'Consider reviewing candidates or adding as new transaction',
155
+ amount: Math.round(amountScore),
156
+ date: Math.round(dateScore),
157
+ payee: Math.round(payeeScore),
158
+ combined: Math.round(combined),
147
159
  };
148
160
  }
149
- export function findMatches(bankTransactions, ynabTransactions, config = DEFAULT_MATCHING_CONFIG) {
150
- const matches = [];
151
- const usedIds = new Set();
152
- for (const bankTxn of bankTransactions) {
153
- const match = findBestMatch(bankTxn, ynabTransactions, usedIds, config);
154
- matches.push(match);
155
- if (match.confidence === 'high' && match.ynab_transaction) {
156
- usedIds.add(match.ynab_transaction.id);
157
- }
161
+ function calculatePayeeScore(bankPayee, ynabPayee) {
162
+ if (!ynabPayee)
163
+ return 30;
164
+ const scores = [
165
+ fuzz.token_set_ratio(bankPayee, ynabPayee),
166
+ fuzz.token_sort_ratio(bankPayee, ynabPayee),
167
+ fuzz.partial_ratio(bankPayee, ynabPayee),
168
+ fuzz.WRatio(bankPayee, ynabPayee),
169
+ ];
170
+ return Math.max(...scores);
171
+ }
172
+ function buildMatchReasons(scores, config) {
173
+ const reasons = [];
174
+ if (scores.amount === 100) {
175
+ reasons.push('Exact amount match');
176
+ }
177
+ else if (scores.amount >= 95) {
178
+ reasons.push('Amount within tolerance');
179
+ }
180
+ if (scores.date === 100) {
181
+ reasons.push('Same date');
182
+ }
183
+ else if (scores.date >= 90) {
184
+ reasons.push('Date within 1-2 days');
185
+ }
186
+ else if (scores.date >= 50) {
187
+ reasons.push(`Date within ${config.dateToleranceDays} days`);
158
188
  }
159
- return matches;
189
+ if (scores.payee >= 95) {
190
+ reasons.push('Payee exact match');
191
+ }
192
+ else if (scores.payee >= 80) {
193
+ reasons.push('Payee highly similar');
194
+ }
195
+ else if (scores.payee >= 60) {
196
+ reasons.push('Payee somewhat similar');
197
+ }
198
+ return reasons;
199
+ }
200
+ export function findBestMatch(bankTransaction, ynabTransactions, usedYnabIds = new Set(), config) {
201
+ return matchSingle(bankTransaction, ynabTransactions, usedYnabIds, config);
160
202
  }