@dizzlkheinz/ynab-mcpb 0.13.1 → 0.15.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 (207) 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/publish.yml +3 -3
  69. package/.github/workflows/release.yml +4 -0
  70. package/CHANGELOG.md +75 -0
  71. package/NUL +1 -0
  72. package/dist/bundle/index.cjs +65 -42
  73. package/dist/server/errorHandler.d.ts +2 -0
  74. package/dist/server/errorHandler.js +49 -5
  75. package/dist/tools/reconcileAdapter.js +10 -5
  76. package/dist/tools/reconciliation/analyzer.d.ts +8 -2
  77. package/dist/tools/reconciliation/analyzer.js +127 -409
  78. package/dist/tools/reconciliation/csvParser.d.ts +51 -0
  79. package/dist/tools/reconciliation/csvParser.js +413 -0
  80. package/dist/tools/reconciliation/executor.d.ts +8 -0
  81. package/dist/tools/reconciliation/executor.js +204 -58
  82. package/dist/tools/reconciliation/index.d.ts +7 -7
  83. package/dist/tools/reconciliation/index.js +115 -39
  84. package/dist/tools/reconciliation/matcher.d.ts +24 -3
  85. package/dist/tools/reconciliation/matcher.js +175 -133
  86. package/dist/tools/reconciliation/recommendationEngine.js +22 -18
  87. package/dist/tools/reconciliation/reportFormatter.js +9 -8
  88. package/dist/tools/reconciliation/signDetector.d.ts +2 -0
  89. package/dist/tools/reconciliation/signDetector.js +54 -0
  90. package/dist/tools/reconciliation/types.d.ts +20 -34
  91. package/dist/tools/reconciliation/types.js +1 -7
  92. package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
  93. package/dist/tools/reconciliation/ynabAdapter.js +15 -0
  94. package/dist/types/reconciliation.d.ts +24 -0
  95. package/dist/types/reconciliation.js +1 -0
  96. package/docs/guides/ARCHITECTURE.md +12 -129
  97. package/docs/plans/2025-11-21-v014-hardening.md +153 -0
  98. package/docs/plans/reconciliation-v2-redesign.md +1571 -0
  99. package/package.json +6 -1
  100. package/scripts/test-recommendations.ts +1 -1
  101. package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
  102. package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
  103. package/src/server/errorHandler.ts +52 -5
  104. package/src/tools/reconcileAdapter.ts +10 -5
  105. package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
  106. package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
  107. package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
  108. package/src/tools/reconciliation/__tests__/executor.integration.test.ts +1 -1
  109. package/src/tools/reconciliation/__tests__/executor.test.ts +88 -61
  110. package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
  111. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
  112. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
  113. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
  114. package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
  115. package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
  116. package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
  117. package/src/tools/reconciliation/analyzer.ts +191 -550
  118. package/src/tools/reconciliation/csvParser.ts +617 -0
  119. package/src/tools/reconciliation/executor.ts +249 -66
  120. package/src/tools/reconciliation/index.ts +148 -54
  121. package/src/tools/reconciliation/matcher.ts +234 -214
  122. package/src/tools/reconciliation/recommendationEngine.ts +23 -19
  123. package/src/tools/reconciliation/reportFormatter.ts +16 -11
  124. package/src/tools/reconciliation/signDetector.ts +117 -0
  125. package/src/tools/reconciliation/types.ts +39 -61
  126. package/src/tools/reconciliation/ynabAdapter.ts +33 -0
  127. package/src/types/reconciliation.ts +49 -0
  128. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
  129. package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
  130. package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
  131. package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
  132. package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
  133. package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
  134. package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
  135. package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
  136. package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
  137. package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
  138. package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
  139. package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
  140. package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
  141. package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
  142. package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
  143. package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
  144. package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
  145. package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
  146. package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
  147. package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
  148. package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
  149. package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
  150. package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
  151. package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
  152. package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
  153. package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
  154. package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
  155. package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
  156. package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
  157. package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
  158. package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
  159. package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
  160. package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
  161. package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
  162. package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
  163. package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
  164. package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
  165. package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
  166. package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
  167. package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
  168. package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
  169. package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
  170. package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
  171. package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
  172. package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
  173. package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
  174. package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
  175. package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
  176. package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
  177. package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
  178. package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
  179. package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
  180. package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
  181. package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
  182. package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
  183. package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
  184. package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
  185. package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
  186. package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
  187. package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
  188. package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
  189. package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
  190. package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
  191. package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
  192. package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
  193. package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
  194. package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
  195. package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
  196. package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
  197. package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
  198. package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
  199. package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
  200. package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
  201. package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
  202. package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
  203. package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
  204. package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
  205. package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
  206. package/.gemini/settings.json +0 -8
  207. package/WARP.md +0 -245
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dizzlkheinz/ynab-mcpb",
3
- "version": "0.13.1",
3
+ "version": "0.15.1",
4
4
  "description": "Model Context Protocol server for YNAB (You Need A Budget) integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -67,10 +67,14 @@
67
67
  "license": "AGPL-3.0",
68
68
  "dependencies": {
69
69
  "@modelcontextprotocol/sdk": "^1.22.0",
70
+ "chrono-node": "^2.9.0",
70
71
  "csv-parse": "^6.1.0",
71
72
  "d3-array": "^3.2.4",
72
73
  "date-fns": "^4.1.0",
74
+ "dayjs": "^1.11.19",
73
75
  "dotenv": "^17.2.1",
76
+ "fuzzball": "^2.2.3",
77
+ "papaparse": "^5.5.3",
74
78
  "ynab": "^2.9.0",
75
79
  "zod": "^4.1.11",
76
80
  "zod-validation-error": "^5.0.0"
@@ -79,6 +83,7 @@
79
83
  "@eslint/js": "^9.35.0",
80
84
  "@types/d3-array": "^3.2.1",
81
85
  "@types/node": "^24.5.2",
86
+ "@types/papaparse": "^5.5.0",
82
87
  "@vitest/coverage-v8": "^3.2.4",
83
88
  "@vitest/ui": "^3.2.4",
84
89
  "esbuild": "^0.25.10",
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { analyzeReconciliation } from '../src/tools/reconciliation/analyzer.js';
7
- import { DEFAULT_MATCHING_CONFIG } from '../src/tools/reconciliation/types.js';
7
+ import { DEFAULT_CONFIG as DEFAULT_MATCHING_CONFIG } from '../src/tools/reconciliation/matcher.js';
8
8
 
9
9
  // Test data from user's scenario
10
10
  const csvContent = `Date,Description,Amount
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseCSV } from '../../../tools/reconciliation/csvParser.js';
3
+ import { findMatches } from '../../../tools/reconciliation/matcher.js';
4
+ import { normalizeYNABTransaction } from '../../../tools/reconciliation/ynabAdapter.js';
5
+
6
+ describe('CSV Parser Integration Tests', () => {
7
+ describe('TD Bank CSV', () => {
8
+ const tdCSV = `Date,Description,Amount
9
+ 09/15/2025,SHELL STATION 1234 TORONTO ON,-45.23
10
+ 09/16/2025,AMZN MKTP CA*1A2B3C4,-127.99
11
+ 09/17/2025,PAYROLL DEPOSIT ABC CORP,2500.00`;
12
+
13
+ it('should parse TD CSV correctly', () => {
14
+ const result = parseCSV(tdCSV, { preset: 'td' });
15
+
16
+ expect(result.errors).toHaveLength(0);
17
+ expect(result.transactions).toHaveLength(3);
18
+ expect(result.transactions[0].amount).toBe(-45230); // Milliunits!
19
+ expect(result.transactions[0].payee).toBe('SHELL STATION 1234 TORONTO ON');
20
+ expect(result.transactions[0].date).toBe('2025-09-15');
21
+ });
22
+ });
23
+
24
+ describe('RBC Debit/Credit CSV', () => {
25
+ const rbcCSV = `Transaction Date,Description 1,Debit,Credit
26
+ 2025-09-15,SHELL GAS,45.23,
27
+ 2025-09-16,TRANSFER FROM SAVINGS,,500.00`;
28
+
29
+ it('should parse RBC CSV with debit/credit columns', () => {
30
+ const result = parseCSV(rbcCSV, { preset: 'rbc' });
31
+
32
+ expect(result.errors).toHaveLength(0);
33
+ expect(result.transactions).toHaveLength(2);
34
+ expect(result.transactions[0].amount).toBe(-45230); // Debit = negative milliunits
35
+ expect(result.transactions[1].amount).toBe(500000); // Credit = positive milliunits
36
+ });
37
+ });
38
+
39
+ describe('Ambiguous Debit/Credit Warning', () => {
40
+ const ambiguousCSV = `Transaction Date,Description,Debit,Credit
41
+ 2025-09-15,WEIRD TXN,50.00,25.00`;
42
+
43
+ it('should warn when both debit and credit have values', () => {
44
+ const result = parseCSV(ambiguousCSV, { preset: 'rbc' });
45
+
46
+ expect(result.warnings).toHaveLength(1);
47
+ expect(result.warnings[0].message).toContain('Both Debit');
48
+ expect(result.transactions[0].amount).toBe(-50000); // Uses debit
49
+ });
50
+ });
51
+
52
+ describe('European Number Format', () => {
53
+ const euroCSV = `Date,Amount,Description
54
+ 15/09/2025,"1.234,56",Big Purchase`;
55
+
56
+ it('should handle European number format', () => {
57
+ const result = parseCSV(euroCSV);
58
+
59
+ expect(result.transactions[0].amount).toBe(1234560); // 1234.56 in milliunits
60
+ });
61
+ });
62
+ });
63
+
64
+ describe('Matcher Integration Tests', () => {
65
+ const mockYNABTransactions = [
66
+ {
67
+ id: 'y1',
68
+ date: '2025-09-15',
69
+ amount: -45230,
70
+ payee_name: 'Shell',
71
+ category_name: 'Gas',
72
+ cleared: 'uncleared',
73
+ approved: true,
74
+ },
75
+ {
76
+ id: 'y2',
77
+ date: '2025-09-17',
78
+ amount: -127990,
79
+ payee_name: 'Amazon',
80
+ category_name: 'Shopping',
81
+ cleared: 'uncleared',
82
+ approved: true,
83
+ },
84
+ ].map((t) => normalizeYNABTransaction(t as any));
85
+
86
+ it('should achieve high confidence matches with exact integer comparison', () => {
87
+ const bankCSV = `Date,Description,Amount
88
+ 09/15/2025,SHELL STATION 1234,-45.23
89
+ 09/16/2025,AMZN MKTP CA*ABC123,-127.99`;
90
+
91
+ const parsed = parseCSV(bankCSV);
92
+ const matches = findMatches(parsed.transactions, mockYNABTransactions);
93
+
94
+ // Shell: exact amount match (both -45230 milliunits)
95
+ expect(matches[0].confidence).toBe('high');
96
+ expect(matches[0].bestMatch?.scores.amount).toBe(100);
97
+
98
+ // Amazon: exact amount match (both -127990 milliunits)
99
+ expect(matches[1].confidence).toBe('high');
100
+ expect(matches[1].bestMatch?.scores.amount).toBe(100);
101
+ });
102
+
103
+ it('should use exact integer comparison (no float precision issues)', () => {
104
+ // Both are now integers - no floating point comparison needed!
105
+ const bankTxn = {
106
+ id: 'b1',
107
+ date: '2025-09-15',
108
+ amount: -45230, // Integer milliunits
109
+ payee: 'Shell',
110
+ sourceRow: 2,
111
+ raw: { date: '09/15/2025', amount: '-45.23', description: 'Shell' },
112
+ };
113
+
114
+ const ynabTxn = {
115
+ id: 'y1',
116
+ date: '2025-09-15',
117
+ amount: -45230, // Integer milliunits - direct from YNAB API
118
+ payee: 'Shell',
119
+ memo: null,
120
+ categoryName: 'Gas',
121
+ cleared: 'uncleared' as const,
122
+ approved: true,
123
+ };
124
+
125
+ const matches = findMatches([bankTxn], [ynabTxn]);
126
+ // Exact match because integers compare exactly: -45230 === -45230
127
+ expect(matches[0].bestMatch?.scores.amount).toBe(100);
128
+ });
129
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseCSV } from '../../../tools/reconciliation/csvParser.js';
3
+
4
+ describe('Real World CSV Validation (Simulated)', () => {
5
+ describe('Wealthsimple', () => {
6
+ // Simulated content based on real structure
7
+ const wsContent = `Date,Payee,Amount
8
+ 2025-11-21,"Amazon.Ca*B03Vv2Ss0",-42.68
9
+ 2025-11-21,"Amazon.Ca*B06I19Si0",-33.56
10
+ 2025-11-21,"Amzn Mktp Ca*B07U50Um2",-37.79`;
11
+
12
+ it('should parse Wealthsimple export correctly', () => {
13
+ const result = parseCSV(wsContent);
14
+
15
+ expect(result.errors).toHaveLength(0);
16
+ expect(result.transactions.length).toBe(3);
17
+
18
+ const first = result.transactions[0];
19
+ expect(first.date).toBe('2025-11-21');
20
+ expect(first.payee).toContain('Amazon');
21
+ expect(first.amount).toBe(-42680); // -42.68
22
+ });
23
+ });
24
+
25
+ describe('TD Canada Trust', () => {
26
+ // Simulated content based on real structure (Headerless)
27
+ // Date, Desc, Debit, Credit, Balance
28
+ const tdContent = `11/21/2025,EvoCarShare,23.87,,132.91
29
+ 11/19/2025,KOODO MOBILE PAC,43.68,,109.04
30
+ 11/14/2025,PAYMENT - THANK YOU,,1378.31,0.00`;
31
+
32
+ it('should parse TD export correctly using preset', () => {
33
+ // TD export is headerless
34
+ // We expect the 'td' preset to handle header: false and index mappings
35
+ const result = parseCSV(tdContent, { preset: 'td' });
36
+
37
+ expect(result.errors).toHaveLength(0);
38
+ expect(result.transactions.length).toBe(3);
39
+
40
+ const first = result.transactions[0];
41
+ // 11/21/2025
42
+ expect(first.date).toBe('2025-11-21');
43
+ expect(first.payee).toBe('EvoCarShare');
44
+ // 23.87 Debit = Outflow = Negative
45
+ expect(first.amount).toBe(-23870);
46
+
47
+ const third = result.transactions[2];
48
+ // 1378.31 Credit = Inflow = Positive
49
+ expect(third.payee).toBe('PAYMENT - THANK YOU');
50
+ expect(third.amount).toBe(1378310);
51
+ });
52
+ });
53
+ });
@@ -12,6 +12,7 @@ interface ErrorResponseFormatter {
12
12
  */
13
13
 
14
14
  export const enum YNABErrorCode {
15
+ BAD_REQUEST = 400,
15
16
  UNAUTHORIZED = 401,
16
17
  FORBIDDEN = 403,
17
18
  NOT_FOUND = 404,
@@ -54,6 +55,11 @@ export class YNABAPIError extends Error {
54
55
  this.code = code;
55
56
  this.originalError = originalError;
56
57
  }
58
+
59
+ // Expose status as an alias for code for backward compatibility with tests
60
+ get status(): YNABErrorCode {
61
+ return this.code;
62
+ }
57
63
  }
58
64
 
59
65
  export class ValidationError extends Error {
@@ -106,7 +112,12 @@ export class ErrorHandler {
106
112
  formattedText = this.formatter.format(errorResponse);
107
113
  } catch {
108
114
  // Fallback to JSON.stringify if formatter fails
109
- formattedText = JSON.stringify(errorResponse, null, 2);
115
+ try {
116
+ formattedText = JSON.stringify(errorResponse, null, 2);
117
+ } catch {
118
+ // Final fallback if JSON serialization fails (e.g. circular references)
119
+ formattedText = `Error processing request: ${this.getGenericErrorMessage(context)}`;
120
+ }
110
121
  }
111
122
 
112
123
  return {
@@ -135,7 +146,9 @@ export class ErrorHandler {
135
146
  private createErrorResponse(error: unknown, context: string): ErrorResponse {
136
147
  // Handle custom error types
137
148
  if (error instanceof YNABAPIError) {
138
- const sanitizedDetails = this.sanitizeErrorDetails(error.originalError);
149
+ const ynabDetails = this.extractYNABApiError(error.originalError);
150
+ const detailsToSanitize = ynabDetails?.details || error.originalError;
151
+ const sanitizedDetails = this.sanitizeErrorDetails(detailsToSanitize);
139
152
  return {
140
153
  error: {
141
154
  code: error.code,
@@ -216,7 +229,22 @@ export class ErrorHandler {
216
229
 
217
230
  // Fallback for unknown errors
218
231
  // Preserve the original error message for debugging while sanitizing sensitive data
219
- const errorMessage = error instanceof Error ? error.message : String(error);
232
+ let errorMessage: string;
233
+ if (error instanceof Error) {
234
+ errorMessage = error.message;
235
+ } else if (typeof error === 'string') {
236
+ errorMessage = error;
237
+ } else if (error && typeof error === 'object') {
238
+ // Handle plain objects (e.g., YNAB SDK errors that aren't Error instances)
239
+ try {
240
+ errorMessage = JSON.stringify(error, null, 2);
241
+ } catch {
242
+ // Circular reference or other JSON issue
243
+ errorMessage = Object.prototype.toString.call(error);
244
+ }
245
+ } else {
246
+ errorMessage = String(error);
247
+ }
220
248
  const sanitizedDetails = this.sanitizeErrorDetails(errorMessage);
221
249
 
222
250
  return {
@@ -264,6 +292,8 @@ export class ErrorHandler {
264
292
  */
265
293
  private getUserFriendlyMessage(code: YNABErrorCode | SecurityErrorCode, context: string): string {
266
294
  switch (code) {
295
+ case YNABErrorCode.BAD_REQUEST:
296
+ return 'The request was invalid. Please check your input data.';
267
297
  case YNABErrorCode.UNAUTHORIZED:
268
298
  return 'Your YNAB access token is invalid or has expired. Please check your token and try again.';
269
299
  case YNABErrorCode.FORBIDDEN:
@@ -288,6 +318,12 @@ export class ErrorHandler {
288
318
  */
289
319
  private getErrorSuggestions(code: YNABErrorCode | SecurityErrorCode, context: string): string[] {
290
320
  switch (code) {
321
+ case YNABErrorCode.BAD_REQUEST:
322
+ return [
323
+ 'Check that all required fields are correct',
324
+ 'Verify that dates are in the correct format (ISO 8601)',
325
+ 'Ensure amounts are valid numbers',
326
+ ];
291
327
  case YNABErrorCode.UNAUTHORIZED:
292
328
  return [
293
329
  'Go to https://app.youneedabudget.com/settings/developer to generate a new access token',
@@ -401,6 +437,8 @@ export class ErrorHandler {
401
437
  */
402
438
  private getErrorMessage(code: YNABErrorCode, context: string): string {
403
439
  switch (code) {
440
+ case YNABErrorCode.BAD_REQUEST:
441
+ return 'Bad request - invalid parameters';
404
442
  case YNABErrorCode.UNAUTHORIZED:
405
443
  return 'Invalid or expired YNAB access token';
406
444
  case YNABErrorCode.FORBIDDEN:
@@ -535,6 +573,7 @@ export class ErrorHandler {
535
573
  */
536
574
  private mapHttpStatusToErrorCode(status: number): YNABErrorCode | null {
537
575
  switch (status) {
576
+ case YNABErrorCode.BAD_REQUEST:
538
577
  case YNABErrorCode.UNAUTHORIZED:
539
578
  case YNABErrorCode.FORBIDDEN:
540
579
  case YNABErrorCode.NOT_FOUND:
@@ -571,11 +610,19 @@ export class ErrorHandler {
571
610
  * Extracts structured YNAB API error information
572
611
  */
573
612
  private extractYNABApiError(error: unknown): { code: YNABErrorCode; details?: string } | null {
574
- if (!error || typeof error !== 'object' || !('error' in (error as Record<string, unknown>))) {
613
+ if (!error || typeof error !== 'object') {
575
614
  return null;
576
615
  }
577
616
 
578
- const payload = (error as { error?: unknown }).error;
617
+ let payload = (error as { error?: unknown }).error;
618
+
619
+ if (!payload) {
620
+ const responseData = (error as { response?: { data?: unknown } }).response?.data;
621
+ if (responseData && typeof responseData === 'object') {
622
+ payload = (responseData as { error?: unknown }).error;
623
+ }
624
+ }
625
+
579
626
  if (!payload || typeof payload !== 'object') {
580
627
  return null;
581
628
  }
@@ -76,7 +76,7 @@ interface LegacyBalanceReconciliation {
76
76
 
77
77
  const toBankTransactionView = (txn: BankTransaction, currency: string) => ({
78
78
  ...txn,
79
- amount_money: toMoneyValueFromDecimal(txn.amount, currency),
79
+ amount_money: toMoneyValue(txn.amount, currency),
80
80
  });
81
81
 
82
82
  const toYNABTransactionView = (txn: YNABTransaction, currency: string) => ({
@@ -85,15 +85,20 @@ const toYNABTransactionView = (txn: YNABTransaction, currency: string) => ({
85
85
  });
86
86
 
87
87
  const convertMatch = (match: TransactionMatch, currency: string) => ({
88
- ...match,
89
- bank_transaction: toBankTransactionView(match.bank_transaction, currency),
90
- ynab_transaction: match.ynab_transaction
91
- ? toYNABTransactionView(match.ynab_transaction, currency)
88
+ bank_transaction: toBankTransactionView(match.bankTransaction, currency),
89
+ ynab_transaction: match.ynabTransaction
90
+ ? toYNABTransactionView(match.ynabTransaction, currency)
92
91
  : undefined,
93
92
  candidates: match.candidates?.map((candidate) => ({
94
93
  ...candidate,
95
94
  ynab_transaction: toYNABTransactionView(candidate.ynab_transaction, currency),
96
95
  })),
96
+ confidence: match.confidence,
97
+ confidence_score: match.confidenceScore,
98
+ match_reason: match.matchReason,
99
+ top_confidence: match.topConfidence,
100
+ action_hint: match.actionHint,
101
+ recommendation: match.recommendation,
97
102
  });
98
103
 
99
104
  const convertInsight = (insight: ReconciliationInsight) => ({
@@ -34,38 +34,41 @@ const buildAnalysis = (): ReconciliationAnalysis => ({
34
34
  },
35
35
  auto_matches: [
36
36
  {
37
- bank_transaction: {
37
+ bankTransaction: {
38
38
  id: 'bank-1',
39
39
  date: '2025-10-15',
40
- amount: -45.23,
40
+ amount: -45230,
41
41
  payee: 'Shell Gas',
42
- original_csv_row: 2,
42
+ sourceRow: 2,
43
+ raw: { date: '2025-10-15', amount: '-45.23', description: 'Shell Gas' },
43
44
  },
44
- ynab_transaction: {
45
+ ynabTransaction: {
45
46
  id: 'ynab-1',
46
47
  date: '2025-10-14',
47
48
  amount: -45230,
48
- payee_name: 'Shell',
49
- category_name: 'Auto',
49
+ payee: 'Shell',
50
+ categoryName: 'Auto',
50
51
  cleared: 'uncleared',
51
52
  approved: true,
53
+ memo: null,
52
54
  },
53
55
  candidates: [],
54
56
  confidence: 'high',
55
- confidence_score: 97,
56
- match_reason: 'exact_amount_and_date',
57
- top_confidence: 97,
58
- action_hint: 'mark_cleared',
57
+ confidenceScore: 97,
58
+ matchReason: 'exact_amount_and_date',
59
+ topConfidence: 97,
60
+ actionHint: 'mark_cleared',
59
61
  },
60
62
  ],
61
63
  suggested_matches: [
62
64
  {
63
- bank_transaction: {
65
+ bankTransaction: {
64
66
  id: 'bank-2',
65
67
  date: '2025-10-20',
66
- amount: -60,
68
+ amount: -60000,
67
69
  payee: 'Amazon',
68
- original_csv_row: 5,
70
+ sourceRow: 5,
71
+ raw: { date: '2025-10-20', amount: '-60.00', description: 'Amazon' },
69
72
  },
70
73
  candidates: [
71
74
  {
@@ -73,10 +76,11 @@ const buildAnalysis = (): ReconciliationAnalysis => ({
73
76
  id: 'ynab-2',
74
77
  date: '2025-10-19',
75
78
  amount: -60000,
76
- payee_name: 'Amazon Online',
77
- category_name: 'Shopping',
79
+ payee: 'Amazon Online',
80
+ categoryName: 'Shopping',
78
81
  cleared: 'uncleared',
79
82
  approved: true,
83
+ memo: null,
80
84
  },
81
85
  confidence: 75,
82
86
  match_reason: 'amount_and_date_fuzzy_payee',
@@ -84,18 +88,19 @@ const buildAnalysis = (): ReconciliationAnalysis => ({
84
88
  },
85
89
  ],
86
90
  confidence: 'medium',
87
- confidence_score: 75,
88
- match_reason: 'amount_and_date_fuzzy_payee',
89
- top_confidence: 75,
91
+ confidenceScore: 75,
92
+ matchReason: 'amount_and_date_fuzzy_payee',
93
+ topConfidence: 75,
90
94
  },
91
95
  ],
92
96
  unmatched_bank: [
93
97
  {
94
98
  id: 'bank-3',
95
99
  date: '2025-10-25',
96
- amount: 22.22,
100
+ amount: 22220,
97
101
  payee: 'EvoCarShare',
98
- original_csv_row: 7,
102
+ sourceRow: 7,
103
+ raw: { date: '2025-10-25', amount: '22.22', description: 'EvoCarShare' },
99
104
  },
100
105
  ],
101
106
  unmatched_ynab: [
@@ -103,10 +108,11 @@ const buildAnalysis = (): ReconciliationAnalysis => ({
103
108
  id: 'ynab-3',
104
109
  date: '2025-10-26',
105
110
  amount: -15000,
106
- payee_name: 'Coffee Shop',
107
- category_name: 'Dining',
111
+ payee: 'Coffee Shop',
112
+ categoryName: 'Dining',
108
113
  cleared: 'cleared',
109
114
  approved: true,
115
+ memo: null,
110
116
  },
111
117
  ],
112
118
  balance_info: {