@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.
- package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
- package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
- package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
- package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
- package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
- package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
- package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
- package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
- package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
- package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
- package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
- package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
- package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
- package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
- package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
- package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
- package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
- package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
- package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
- package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
- package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
- package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
- package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
- package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
- package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
- package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
- package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
- package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
- package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
- package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
- package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
- package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
- package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
- package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
- package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
- package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
- package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
- package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
- package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
- package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
- package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
- package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
- package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
- package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
- package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
- package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
- package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
- package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
- package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
- package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
- package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
- package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
- package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
- package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
- package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
- package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
- package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
- package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
- package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
- package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
- package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
- package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
- package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
- package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
- package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
- package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
- package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/release.yml +4 -0
- package/CHANGELOG.md +75 -0
- package/NUL +1 -0
- package/dist/bundle/index.cjs +65 -42
- package/dist/server/errorHandler.d.ts +2 -0
- package/dist/server/errorHandler.js +49 -5
- package/dist/tools/reconcileAdapter.js +10 -5
- package/dist/tools/reconciliation/analyzer.d.ts +8 -2
- package/dist/tools/reconciliation/analyzer.js +127 -409
- package/dist/tools/reconciliation/csvParser.d.ts +51 -0
- package/dist/tools/reconciliation/csvParser.js +413 -0
- package/dist/tools/reconciliation/executor.d.ts +8 -0
- package/dist/tools/reconciliation/executor.js +204 -58
- package/dist/tools/reconciliation/index.d.ts +7 -7
- package/dist/tools/reconciliation/index.js +115 -39
- package/dist/tools/reconciliation/matcher.d.ts +24 -3
- package/dist/tools/reconciliation/matcher.js +175 -133
- package/dist/tools/reconciliation/recommendationEngine.js +22 -18
- package/dist/tools/reconciliation/reportFormatter.js +9 -8
- package/dist/tools/reconciliation/signDetector.d.ts +2 -0
- package/dist/tools/reconciliation/signDetector.js +54 -0
- package/dist/tools/reconciliation/types.d.ts +20 -34
- package/dist/tools/reconciliation/types.js +1 -7
- package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
- package/dist/tools/reconciliation/ynabAdapter.js +15 -0
- package/dist/types/reconciliation.d.ts +24 -0
- package/dist/types/reconciliation.js +1 -0
- package/docs/guides/ARCHITECTURE.md +12 -129
- package/docs/plans/2025-11-21-v014-hardening.md +153 -0
- package/docs/plans/reconciliation-v2-redesign.md +1571 -0
- package/package.json +6 -1
- package/scripts/test-recommendations.ts +1 -1
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
- package/src/server/errorHandler.ts +52 -5
- package/src/tools/reconcileAdapter.ts +10 -5
- package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +1 -1
- package/src/tools/reconciliation/__tests__/executor.test.ts +88 -61
- package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
- package/src/tools/reconciliation/analyzer.ts +191 -550
- package/src/tools/reconciliation/csvParser.ts +617 -0
- package/src/tools/reconciliation/executor.ts +249 -66
- package/src/tools/reconciliation/index.ts +148 -54
- package/src/tools/reconciliation/matcher.ts +234 -214
- package/src/tools/reconciliation/recommendationEngine.ts +23 -19
- package/src/tools/reconciliation/reportFormatter.ts +16 -11
- package/src/tools/reconciliation/signDetector.ts +117 -0
- package/src/tools/reconciliation/types.ts +39 -61
- package/src/tools/reconciliation/ynabAdapter.ts +33 -0
- package/src/types/reconciliation.ts +49 -0
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
- package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
- package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
- package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
- package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
- package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
- package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
- package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
- package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
- package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
- package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
- package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
- package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
- package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
- package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
- package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
- package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
- package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
- package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
- package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
- package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
- package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
- package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
- package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
- package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
- package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
- package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
- package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
- package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
- package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
- package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
- package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
- package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
- package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
- package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
- package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
- package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
- package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
- package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
- package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
- package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
- package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
- package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
- package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
- package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
- package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
- package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
- package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
- package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
- package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
- package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
- package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
- package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
- package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
- package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
- package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
- package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
- package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
- package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
- package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
- package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
- package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
- package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
- package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
- package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
- package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
- package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
- package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
- package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
- package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
- package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
- package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
- package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
- package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
- package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
- package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
- package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
- package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
- package/.gemini/settings.json +0 -8
- package/WARP.md +0 -245
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dizzlkheinz/ynab-mcpb",
|
|
3
|
-
"version": "0.
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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'
|
|
613
|
+
if (!error || typeof error !== 'object') {
|
|
575
614
|
return null;
|
|
576
615
|
}
|
|
577
616
|
|
|
578
|
-
|
|
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:
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
37
|
+
bankTransaction: {
|
|
38
38
|
id: 'bank-1',
|
|
39
39
|
date: '2025-10-15',
|
|
40
|
-
amount: -
|
|
40
|
+
amount: -45230,
|
|
41
41
|
payee: 'Shell Gas',
|
|
42
|
-
|
|
42
|
+
sourceRow: 2,
|
|
43
|
+
raw: { date: '2025-10-15', amount: '-45.23', description: 'Shell Gas' },
|
|
43
44
|
},
|
|
44
|
-
|
|
45
|
+
ynabTransaction: {
|
|
45
46
|
id: 'ynab-1',
|
|
46
47
|
date: '2025-10-14',
|
|
47
48
|
amount: -45230,
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
65
|
+
bankTransaction: {
|
|
64
66
|
id: 'bank-2',
|
|
65
67
|
date: '2025-10-20',
|
|
66
|
-
amount: -
|
|
68
|
+
amount: -60000,
|
|
67
69
|
payee: 'Amazon',
|
|
68
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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:
|
|
100
|
+
amount: 22220,
|
|
97
101
|
payee: 'EvoCarShare',
|
|
98
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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: {
|