@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
@@ -1,12 +1,11 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
  import { analyzeReconciliation } from '../analyzer.js';
3
3
  import type { Transaction as YNABAPITransaction } from 'ynab';
4
- import * as parser from '../../compareTransactions/parser.js';
4
+ import * as csvParser from '../csvParser.js';
5
5
 
6
6
  // Mock the parser module
7
- vi.mock('../../compareTransactions/parser.js', () => ({
8
- parseBankCSV: vi.fn(),
9
- readCSVFile: vi.fn(),
7
+ vi.mock('../csvParser.js', () => ({
8
+ parseCSV: vi.fn(),
10
9
  }));
11
10
 
12
11
  describe('analyzer', () => {
@@ -17,26 +16,36 @@ describe('analyzer', () => {
17
16
  describe('analyzeReconciliation', () => {
18
17
  it('should perform full analysis and return structured results', () => {
19
18
  // Mock CSV parsing
20
- vi.mocked(parser.parseBankCSV).mockReturnValue({
19
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
21
20
  transactions: [
22
21
  {
22
+ id: 'b1',
23
23
  date: '2025-10-15',
24
- amount: -45.23,
24
+ amount: -45230, // milliunits
25
25
  payee: 'Shell Gas',
26
26
  memo: '',
27
+ sourceRow: 1,
28
+ raw: { date: '10/15/2025', amount: '-45.23', description: 'Shell Gas' },
27
29
  },
28
30
  {
31
+ id: 'b2',
29
32
  date: '2025-10-16',
30
- amount: -100.0,
33
+ amount: -100000, // milliunits
31
34
  payee: 'Netflix',
32
35
  memo: '',
36
+ sourceRow: 2,
37
+ raw: { date: '10/16/2025', amount: '-100.00', description: 'Netflix' },
33
38
  },
34
39
  ],
35
- format_detected: 'standard',
36
- delimiter: ',',
37
- total_rows: 2,
38
- valid_rows: 2,
40
+ meta: {
41
+ detectedDelimiter: ',',
42
+ detectedColumns: ['Date', 'Amount', 'Description'],
43
+ totalRows: 2,
44
+ validRows: 2,
45
+ skippedRows: 0,
46
+ },
39
47
  errors: [],
48
+ warnings: [],
40
49
  });
41
50
 
42
51
  const ynabTxns: YNABAPITransaction[] = [
@@ -76,23 +85,33 @@ describe('analyzer', () => {
76
85
  expect(result.unmatched_ynab).toBeDefined();
77
86
  expect(result.balance_info).toBeDefined();
78
87
  expect(result.next_steps).toBeDefined();
88
+
89
+ // Verify auto-matches (exact matches)
90
+ expect(result.auto_matches.length).toBe(2);
79
91
  });
80
92
 
81
93
  it('should categorize high-confidence matches as auto-matches', () => {
82
- vi.mocked(parser.parseBankCSV).mockReturnValue({
94
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
83
95
  transactions: [
84
96
  {
97
+ id: 'b1',
85
98
  date: '2025-10-15',
86
- amount: -50.0,
99
+ amount: -50000,
87
100
  payee: 'Coffee Shop',
88
101
  memo: '',
102
+ sourceRow: 1,
103
+ raw: {} as any,
89
104
  },
90
105
  ],
91
- format_detected: 'standard',
92
- delimiter: ',',
93
- total_rows: 1,
94
- valid_rows: 1,
106
+ meta: {
107
+ detectedDelimiter: ',',
108
+ detectedColumns: [],
109
+ totalRows: 1,
110
+ validRows: 1,
111
+ skippedRows: 0,
112
+ },
95
113
  errors: [],
114
+ warnings: [],
96
115
  });
97
116
 
98
117
  const ynabTxns: YNABAPITransaction[] = [
@@ -114,28 +133,35 @@ describe('analyzer', () => {
114
133
  });
115
134
 
116
135
  it('should categorize medium-confidence matches as suggested', () => {
117
- vi.mocked(parser.parseBankCSV).mockReturnValue({
136
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
118
137
  transactions: [
119
138
  {
139
+ id: 'b1',
120
140
  date: '2025-10-15',
121
- amount: -50.0,
122
- payee: 'Amazon',
141
+ amount: -50000,
142
+ payee: 'Generic Store',
123
143
  memo: '',
144
+ sourceRow: 1,
145
+ raw: {} as any,
124
146
  },
125
147
  ],
126
- format_detected: 'standard',
127
- delimiter: ',',
128
- total_rows: 1,
129
- valid_rows: 1,
148
+ meta: {
149
+ detectedDelimiter: ',',
150
+ detectedColumns: [],
151
+ totalRows: 1,
152
+ validRows: 1,
153
+ skippedRows: 0,
154
+ },
130
155
  errors: [],
156
+ warnings: [],
131
157
  });
132
158
 
133
159
  const ynabTxns: YNABAPITransaction[] = [
134
160
  {
135
161
  id: 'y1',
136
- date: '2025-10-18', // 3 days difference
162
+ date: '2025-10-18', // 3 days difference - date score drops
137
163
  amount: -50000,
138
- payee_name: 'Amazon Prime',
164
+ payee_name: 'Amazon Prime', // Fuzzy match
139
165
  category_name: 'Shopping',
140
166
  cleared: 'uncleared' as const,
141
167
  approved: true,
@@ -144,25 +170,33 @@ describe('analyzer', () => {
144
170
 
145
171
  const result = analyzeReconciliation('csv', undefined, ynabTxns, -50.0);
146
172
 
147
- // Might be medium or low depending on exact scoring
148
- expect(result.suggested_matches.length + result.unmatched_bank.length).toBeGreaterThan(0);
173
+ // Should be suggested (medium)
174
+ expect(result.suggested_matches.length).toBeGreaterThan(0);
175
+ expect(result.suggested_matches[0].confidence).toBe('medium');
149
176
  });
150
177
 
151
178
  it('should identify unmatched bank transactions', () => {
152
- vi.mocked(parser.parseBankCSV).mockReturnValue({
179
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
153
180
  transactions: [
154
181
  {
182
+ id: 'b1',
155
183
  date: '2025-10-15',
156
- amount: -15.99,
184
+ amount: -15990,
157
185
  payee: 'New Store',
158
186
  memo: '',
187
+ sourceRow: 1,
188
+ raw: {} as any,
159
189
  },
160
190
  ],
161
- format_detected: 'standard',
162
- delimiter: ',',
163
- total_rows: 1,
164
- valid_rows: 1,
191
+ meta: {
192
+ detectedDelimiter: ',',
193
+ detectedColumns: [],
194
+ totalRows: 1,
195
+ validRows: 1,
196
+ skippedRows: 0,
197
+ },
165
198
  errors: [],
199
+ warnings: [],
166
200
  });
167
201
 
168
202
  const ynabTxns: YNABAPITransaction[] = [];
@@ -174,13 +208,17 @@ describe('analyzer', () => {
174
208
  });
175
209
 
176
210
  it('should identify unmatched YNAB transactions', () => {
177
- vi.mocked(parser.parseBankCSV).mockReturnValue({
211
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
178
212
  transactions: [],
179
- format_detected: 'standard',
180
- delimiter: ',',
181
- total_rows: 0,
182
- valid_rows: 0,
213
+ meta: {
214
+ detectedDelimiter: ',',
215
+ detectedColumns: [],
216
+ totalRows: 0,
217
+ validRows: 0,
218
+ skippedRows: 0,
219
+ },
183
220
  errors: [],
221
+ warnings: [],
184
222
  });
185
223
 
186
224
  const ynabTxns: YNABAPITransaction[] = [
@@ -198,77 +236,21 @@ describe('analyzer', () => {
198
236
  const result = analyzeReconciliation('csv', undefined, ynabTxns, 0);
199
237
 
200
238
  expect(result.unmatched_ynab.length).toBe(1);
201
- expect(result.unmatched_ynab[0].payee_name).toBe('Restaurant');
202
- });
203
-
204
- it('should surface combination suggestions and insights when totals align', () => {
205
- vi.mocked(parser.parseBankCSV).mockReturnValue({
206
- transactions: [
207
- {
208
- date: '2025-10-20',
209
- amount: -30.0,
210
- payee: 'Evening Out',
211
- memo: '',
212
- },
213
- ],
214
- format_detected: 'standard',
215
- delimiter: ',',
216
- total_rows: 1,
217
- valid_rows: 1,
218
- errors: [],
219
- });
220
-
221
- const ynabTxns: YNABAPITransaction[] = [
222
- {
223
- id: 'y-combo-1',
224
- date: '2025-10-19',
225
- amount: -20000,
226
- payee_name: 'Dinner',
227
- category_name: 'Dining',
228
- cleared: 'uncleared' as const,
229
- approved: true,
230
- } as YNABAPITransaction,
231
- {
232
- id: 'y-combo-2',
233
- date: '2025-10-20',
234
- amount: -10000,
235
- payee_name: 'Drinks',
236
- category_name: 'Dining',
237
- cleared: 'uncleared' as const,
238
- approved: true,
239
- } as YNABAPITransaction,
240
- {
241
- id: 'y-extra',
242
- date: '2025-10-22',
243
- amount: -5000,
244
- payee_name: 'Snacks',
245
- category_name: 'Dining',
246
- cleared: 'uncleared' as const,
247
- approved: true,
248
- } as YNABAPITransaction,
249
- ];
250
-
251
- const result = analyzeReconciliation('csv', undefined, ynabTxns, -30.0);
252
-
253
- const comboMatch = result.suggested_matches.find(
254
- (match) => match.match_reason === 'combination_match',
255
- );
256
- expect(comboMatch).toBeDefined();
257
- expect(comboMatch?.candidates?.length).toBeGreaterThanOrEqual(2);
258
-
259
- const comboInsight = result.insights.find((insight) => insight.id.startsWith('combination-'));
260
- expect(comboInsight).toBeDefined();
261
- expect(comboInsight?.severity).toBe('info');
239
+ expect(result.unmatched_ynab[0].payee).toBe('Restaurant');
262
240
  });
263
241
 
264
242
  it('should calculate balance information correctly', () => {
265
- vi.mocked(parser.parseBankCSV).mockReturnValue({
243
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
266
244
  transactions: [],
267
- format_detected: 'standard',
268
- delimiter: ',',
269
- total_rows: 0,
270
- valid_rows: 0,
245
+ meta: {
246
+ detectedDelimiter: ',',
247
+ detectedColumns: [],
248
+ totalRows: 0,
249
+ validRows: 0,
250
+ skippedRows: 0,
251
+ },
271
252
  errors: [],
253
+ warnings: [],
272
254
  });
273
255
 
274
256
  const ynabTxns: YNABAPITransaction[] = [
@@ -303,16 +285,36 @@ describe('analyzer', () => {
303
285
  });
304
286
 
305
287
  it('should generate appropriate summary', () => {
306
- vi.mocked(parser.parseBankCSV).mockReturnValue({
288
+ vi.mocked(csvParser.parseCSV).mockReturnValue({
307
289
  transactions: [
308
- { date: '2025-10-15', amount: -50.0, payee: 'Store', memo: '' },
309
- { date: '2025-10-20', amount: -30.0, payee: 'Restaurant', memo: '' },
290
+ {
291
+ id: 'b1',
292
+ date: '2025-10-15',
293
+ amount: -50000,
294
+ payee: 'Store',
295
+ memo: '',
296
+ sourceRow: 1,
297
+ raw: {} as any,
298
+ },
299
+ {
300
+ id: 'b2',
301
+ date: '2025-10-20',
302
+ amount: -30000,
303
+ payee: 'Restaurant',
304
+ memo: '',
305
+ sourceRow: 2,
306
+ raw: {} as any,
307
+ },
310
308
  ],
311
- format_detected: 'standard',
312
- delimiter: ',',
313
- total_rows: 2,
314
- valid_rows: 2,
309
+ meta: {
310
+ detectedDelimiter: ',',
311
+ detectedColumns: [],
312
+ totalRows: 2,
313
+ validRows: 2,
314
+ skippedRows: 0,
315
+ },
315
316
  errors: [],
317
+ warnings: [],
316
318
  });
317
319
 
318
320
  const ynabTxns: YNABAPITransaction[] = [
@@ -334,73 +336,5 @@ describe('analyzer', () => {
334
336
  expect(result.summary.statement_date_range).toContain('2025-10-15');
335
337
  expect(result.summary.statement_date_range).toContain('2025-10-20');
336
338
  });
337
-
338
- it('should generate next steps based on analysis', () => {
339
- vi.mocked(parser.parseBankCSV).mockReturnValue({
340
- transactions: [{ date: '2025-10-15', amount: -50.0, payee: 'Store', memo: '' }],
341
- format_detected: 'standard',
342
- delimiter: ',',
343
- total_rows: 1,
344
- valid_rows: 1,
345
- errors: [],
346
- });
347
-
348
- const ynabTxns: YNABAPITransaction[] = [
349
- {
350
- id: 'y1',
351
- date: '2025-10-15',
352
- amount: -50000,
353
- payee_name: 'Store',
354
- category_name: 'Shopping',
355
- cleared: 'uncleared' as const,
356
- approved: true,
357
- } as YNABAPITransaction,
358
- ];
359
-
360
- const result = analyzeReconciliation('csv', undefined, ynabTxns, -50.0);
361
-
362
- expect(result.next_steps).toBeDefined();
363
- expect(Array.isArray(result.next_steps)).toBe(true);
364
- expect(result.next_steps.length).toBeGreaterThan(0);
365
- });
366
-
367
- it('should use file path when provided', () => {
368
- vi.mocked(parser.readCSVFile).mockReturnValue({
369
- transactions: [{ date: '2025-10-15', amount: -50.0, payee: 'Store', memo: '' }],
370
- format_detected: 'standard',
371
- delimiter: ',',
372
- total_rows: 1,
373
- valid_rows: 1,
374
- errors: [],
375
- });
376
-
377
- const ynabTxns: YNABAPITransaction[] = [];
378
-
379
- const result = analyzeReconciliation('', '/path/to/file.csv', ynabTxns, 0);
380
-
381
- expect(vi.mocked(parser.readCSVFile)).toHaveBeenCalledWith('/path/to/file.csv');
382
- expect(result.success).toBe(true);
383
- });
384
-
385
- it('should assign unique IDs to bank transactions', () => {
386
- vi.mocked(parser.parseBankCSV).mockReturnValue({
387
- transactions: [
388
- { date: '2025-10-15', amount: -50.0, payee: 'Store1', memo: '' },
389
- { date: '2025-10-16', amount: -30.0, payee: 'Store2', memo: '' },
390
- ],
391
- format_detected: 'standard',
392
- delimiter: ',',
393
- total_rows: 2,
394
- valid_rows: 2,
395
- errors: [],
396
- });
397
-
398
- const result = analyzeReconciliation('csv', undefined, [], 0);
399
-
400
- expect(result.unmatched_bank.length).toBe(2);
401
- expect(result.unmatched_bank[0].id).toBeDefined();
402
- expect(result.unmatched_bank[1].id).toBeDefined();
403
- expect(result.unmatched_bank[0].id).not.toBe(result.unmatched_bank[1].id);
404
- });
405
339
  });
406
340
  });
@@ -0,0 +1,87 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { parseCSV, ParseCSVOptions } from '../csvParser.js';
3
+
4
+ describe('csvParser', () => {
5
+ describe('Security and Limits', () => {
6
+ it('should throw error if file size exceeds maxBytes', () => {
7
+ const largeContent = 'a'.repeat(1024 * 1024 + 1); // 1MB + 1 byte
8
+ const options: ParseCSVOptions = { maxBytes: 1024 * 1024 }; // 1MB limit
9
+
10
+ expect(() => parseCSV(largeContent, options)).toThrow(/File size exceeds limit/);
11
+ });
12
+
13
+ it('should respect maxRows limit', () => {
14
+ const rows = Array.from(
15
+ { length: 20 },
16
+ (_, i) => `2024-01-${String(i + 1).padStart(2, '0')},Desc ${i},10.00`,
17
+ ).join('\n');
18
+ const content = `Date,Description,Amount\n${rows}`;
19
+ const options: ParseCSVOptions = { maxRows: 10 };
20
+
21
+ const result = parseCSV(content, options);
22
+ expect(result.transactions).toHaveLength(10);
23
+ expect(result.meta.validRows).toBe(10);
24
+ });
25
+
26
+ it('should sanitize control characters from description', () => {
27
+ // ASCII 0x07 (Bell) and 0x1B (Escape) are control characters
28
+ const badDesc = 'Bad\x07Description\x1B';
29
+ const content = `Date,Description,Amount\n2024-01-01,${badDesc},10.00`;
30
+
31
+ const result = parseCSV(content);
32
+ expect(result.transactions[0].payee).toBe('BadDescription');
33
+ });
34
+
35
+ it('should limit description length to 500 characters', () => {
36
+ const longDesc = 'a'.repeat(600);
37
+ const content = `Date,Description,Amount\n2024-01-01,${longDesc},10.00`;
38
+
39
+ const result = parseCSV(content);
40
+ expect(result.transactions[0].payee).toHaveLength(500);
41
+ expect(result.transactions[0].payee).toBe('a'.repeat(500));
42
+ });
43
+ });
44
+
45
+ describe('Timezone Handling', () => {
46
+ it('should parse ISO dates correctly without timezone shift', () => {
47
+ // If parsed as local time in UTC-5 (EST), 2024-01-01 00:00:00 becomes 2023-12-31 19:00:00 UTC
48
+ // But we want 2024-01-01 regardless of local timezone
49
+ const content = `Date,Description,Amount\n2024-01-01,Test,10.00`;
50
+ const result = parseCSV(content);
51
+ expect(result.transactions[0].date).toBe('2024-01-01');
52
+ });
53
+
54
+ it('should parse MDY dates correctly without timezone shift', () => {
55
+ const content = `Date,Description,Amount\n01/01/2024,Test,10.00`;
56
+ const result = parseCSV(content, { dateFormat: 'MDY' });
57
+ expect(result.transactions[0].date).toBe('2024-01-01');
58
+ });
59
+
60
+ it('should parse DMY dates correctly without timezone shift', () => {
61
+ const content = `Date,Description,Amount\n01/01/2024,Test,10.00`;
62
+ const result = parseCSV(content, { dateFormat: 'DMY' });
63
+ expect(result.transactions[0].date).toBe('2024-01-01');
64
+ });
65
+ });
66
+
67
+ describe('Bank Presets', () => {
68
+ it('should detect TD headerless format correctly', () => {
69
+ // TD Pattern: Date, Description, Debit, Credit, Balance
70
+ const content = `
71
+ 01/15/2024,PAYMENT RECEIVED,,100.00,500.00
72
+ 01/16/2024,PURCHASE,50.00,,450.00
73
+ `.trim();
74
+
75
+ // Note: TD pattern detection requires 4+ columns
76
+ // Our parser might need a hint if the csv is very short or malformed
77
+ // But the auto-detect logic checks for date in col 0 and numerics in 2/3
78
+
79
+ const result = parseCSV(content);
80
+ // Should detect TD preset which implies header: false
81
+ expect(result.meta.detectedColumns).toBeDefined();
82
+ expect(result.transactions).toHaveLength(2);
83
+ expect(result.transactions[0].amount).toBe(100000); // Credit is inflow (positive)
84
+ expect(result.transactions[1].amount).toBe(-50000); // Debit is outflow (negative)
85
+ });
86
+ });
87
+ });
@@ -222,7 +222,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
222
222
  initialAccount: accountSnapshot,
223
223
  currencyCode: 'USD',
224
224
  }),
225
- ).rejects.toThrow();
225
+ ).rejects.toMatchObject({ status: expect.any(Number) });
226
226
  }, this);
227
227
  },
228
228
  60000,