@dizzlkheinz/ynab-mcpb 0.12.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/ci-tests.yml +6 -2
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/release.yml +4 -0
- package/CHANGELOG.md +89 -1
- package/NUL +1 -1
- package/README.md +36 -10
- package/dist/bundle/index.cjs +65 -42
- package/dist/index.js +9 -20
- package/dist/server/YNABMCPServer.d.ts +2 -1
- package/dist/server/YNABMCPServer.js +61 -27
- package/dist/server/cacheKeys.d.ts +8 -0
- package/dist/server/cacheKeys.js +8 -0
- package/dist/server/config.d.ts +22 -3
- package/dist/server/config.js +16 -17
- package/dist/server/errorHandler.d.ts +2 -0
- package/dist/server/errorHandler.js +49 -5
- package/dist/server/securityMiddleware.js +3 -6
- package/dist/server/toolRegistry.js +8 -10
- package/dist/tools/accountTools.js +4 -3
- package/dist/tools/categoryTools.js +8 -7
- package/dist/tools/monthTools.js +2 -1
- package/dist/tools/payeeTools.js +2 -1
- package/dist/tools/reconcileAdapter.js +10 -5
- package/dist/tools/reconciliation/analyzer.d.ts +4 -2
- package/dist/tools/reconciliation/analyzer.js +120 -404
- 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 +277 -50
- 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/tools/transactionTools.d.ts +3 -17
- package/dist/tools/transactionTools.js +5 -17
- package/dist/types/reconciliation.d.ts +24 -0
- package/dist/types/reconciliation.js +1 -0
- package/dist/utils/baseError.d.ts +3 -0
- package/dist/utils/baseError.js +7 -0
- package/dist/utils/errors.d.ts +13 -0
- package/dist/utils/errors.js +15 -0
- package/dist/utils/validationError.d.ts +3 -0
- package/dist/utils/validationError.js +3 -0
- package/docs/guides/ARCHITECTURE.md +12 -129
- package/docs/plans/2025-11-20-reloadable-config-token-validation.md +93 -0
- package/docs/plans/2025-11-21-fix-transaction-cached-property.md +362 -0
- package/docs/plans/2025-11-21-reconciliation-error-handling.md +90 -0
- package/docs/plans/2025-11-21-v014-hardening.md +153 -0
- package/docs/plans/reconciliation-v2-redesign.md +1571 -0
- package/package.json +8 -2
- package/scripts/run-throttled-integration-tests.js +9 -3
- package/scripts/test-recommendations.ts +1 -1
- package/src/__tests__/performance.test.ts +12 -5
- package/src/__tests__/testUtils.ts +62 -5
- 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/__tests__/workflows.e2e.test.ts +33 -0
- package/src/index.ts +8 -31
- package/src/server/YNABMCPServer.ts +81 -42
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +10 -12
- package/src/server/__tests__/YNABMCPServer.test.ts +27 -15
- package/src/server/__tests__/config.test.ts +76 -152
- package/src/server/__tests__/server-startup.integration.test.ts +42 -14
- package/src/server/__tests__/toolRegistry.test.ts +1 -1
- package/src/server/cacheKeys.ts +8 -0
- package/src/server/config.ts +20 -38
- package/src/server/errorHandler.ts +52 -5
- package/src/server/securityMiddleware.ts +3 -7
- package/src/server/toolRegistry.ts +14 -10
- package/src/tools/__tests__/categoryTools.test.ts +37 -19
- package/src/tools/__tests__/transactionTools.test.ts +58 -2
- package/src/tools/accountTools.ts +8 -3
- package/src/tools/categoryTools.ts +12 -7
- package/src/tools/monthTools.ts +7 -1
- package/src/tools/payeeTools.ts +7 -1
- 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 +26 -6
- package/src/tools/reconciliation/__tests__/executor.test.ts +133 -60
- 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 +174 -545
- package/src/tools/reconciliation/csvParser.ts +617 -0
- package/src/tools/reconciliation/executor.ts +344 -58
- package/src/tools/reconciliation/index.ts +141 -48
- 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/tools/schemas/outputs/utilityOutputs.ts +1 -1
- package/src/tools/transactionTools.ts +7 -18
- package/src/types/reconciliation.ts +49 -0
- package/src/utils/baseError.ts +7 -0
- package/src/utils/errors.ts +21 -0
- package/src/utils/validationError.ts +3 -0
- package/temp-recon.ts +126 -0
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
- package/test_mcp_tools.mjs +75 -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/ADOS-2-Module-1-Complete-Manual.md +0 -757
- 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
|
|
4
|
+
import * as csvParser from '../csvParser.js';
|
|
5
5
|
|
|
6
6
|
// Mock the parser module
|
|
7
|
-
vi.mock('
|
|
8
|
-
|
|
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(
|
|
19
|
+
vi.mocked(csvParser.parseCSV).mockReturnValue({
|
|
21
20
|
transactions: [
|
|
22
21
|
{
|
|
22
|
+
id: 'b1',
|
|
23
23
|
date: '2025-10-15',
|
|
24
|
-
amount: -
|
|
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: -
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
94
|
+
vi.mocked(csvParser.parseCSV).mockReturnValue({
|
|
83
95
|
transactions: [
|
|
84
96
|
{
|
|
97
|
+
id: 'b1',
|
|
85
98
|
date: '2025-10-15',
|
|
86
|
-
amount: -
|
|
99
|
+
amount: -50000,
|
|
87
100
|
payee: 'Coffee Shop',
|
|
88
101
|
memo: '',
|
|
102
|
+
sourceRow: 1,
|
|
103
|
+
raw: {} as any,
|
|
89
104
|
},
|
|
90
105
|
],
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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(
|
|
136
|
+
vi.mocked(csvParser.parseCSV).mockReturnValue({
|
|
118
137
|
transactions: [
|
|
119
138
|
{
|
|
139
|
+
id: 'b1',
|
|
120
140
|
date: '2025-10-15',
|
|
121
|
-
amount: -
|
|
122
|
-
payee: '
|
|
141
|
+
amount: -50000,
|
|
142
|
+
payee: 'Generic Store',
|
|
123
143
|
memo: '',
|
|
144
|
+
sourceRow: 1,
|
|
145
|
+
raw: {} as any,
|
|
124
146
|
},
|
|
125
147
|
],
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
//
|
|
148
|
-
expect(result.suggested_matches.length
|
|
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(
|
|
179
|
+
vi.mocked(csvParser.parseCSV).mockReturnValue({
|
|
153
180
|
transactions: [
|
|
154
181
|
{
|
|
182
|
+
id: 'b1',
|
|
155
183
|
date: '2025-10-15',
|
|
156
|
-
amount: -
|
|
184
|
+
amount: -15990,
|
|
157
185
|
payee: 'New Store',
|
|
158
186
|
memo: '',
|
|
187
|
+
sourceRow: 1,
|
|
188
|
+
raw: {} as any,
|
|
159
189
|
},
|
|
160
190
|
],
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
211
|
+
vi.mocked(csvParser.parseCSV).mockReturnValue({
|
|
178
212
|
transactions: [],
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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].
|
|
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(
|
|
243
|
+
vi.mocked(csvParser.parseCSV).mockReturnValue({
|
|
266
244
|
transactions: [],
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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(
|
|
288
|
+
vi.mocked(csvParser.parseCSV).mockReturnValue({
|
|
307
289
|
transactions: [
|
|
308
|
-
{
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
+
});
|
|
@@ -40,7 +40,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
|
|
|
40
40
|
await ynabAPI.transactions.deleteTransaction(budgetId, transactionId);
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
});
|
|
43
|
+
}, 60000); // 60 second timeout for cleanup of bulk transactions
|
|
44
44
|
|
|
45
45
|
it(
|
|
46
46
|
'creates 10 transactions via bulk mode',
|
|
@@ -67,6 +67,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
|
|
|
67
67
|
this,
|
|
68
68
|
);
|
|
69
69
|
if (!result) return;
|
|
70
|
+
if (containsRateLimitFailure(result)) return;
|
|
70
71
|
|
|
71
72
|
trackCreatedTransactions(result);
|
|
72
73
|
expect(result.summary.transactions_created).toBe(10);
|
|
@@ -117,6 +118,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
|
|
|
117
118
|
this,
|
|
118
119
|
);
|
|
119
120
|
if (!duplicateAttempt) return;
|
|
121
|
+
if (containsRateLimitFailure(duplicateAttempt)) return;
|
|
120
122
|
|
|
121
123
|
const duplicateActions = duplicateAttempt.actions_taken.filter(
|
|
122
124
|
(action) => action.duplicate === true,
|
|
@@ -153,6 +155,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
|
|
|
153
155
|
this,
|
|
154
156
|
);
|
|
155
157
|
if (!result) return;
|
|
158
|
+
if (containsRateLimitFailure(result)) return;
|
|
156
159
|
trackCreatedTransactions(result);
|
|
157
160
|
|
|
158
161
|
expect(result.summary.transactions_created).toBe(150);
|
|
@@ -187,6 +190,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
|
|
|
187
190
|
this,
|
|
188
191
|
);
|
|
189
192
|
if (!result) return;
|
|
193
|
+
if (containsRateLimitFailure(result)) return;
|
|
190
194
|
trackCreatedTransactions(result);
|
|
191
195
|
|
|
192
196
|
const duration = Date.now() - start;
|
|
@@ -218,7 +222,7 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
|
|
|
218
222
|
initialAccount: accountSnapshot,
|
|
219
223
|
currencyCode: 'USD',
|
|
220
224
|
}),
|
|
221
|
-
).rejects.
|
|
225
|
+
).rejects.toMatchObject({ status: expect.any(Number) });
|
|
222
226
|
}, this);
|
|
223
227
|
},
|
|
224
228
|
60000,
|
|
@@ -249,6 +253,17 @@ describeIntegration('Reconciliation Executor - Bulk Create Integration', () => {
|
|
|
249
253
|
}
|
|
250
254
|
}
|
|
251
255
|
}
|
|
256
|
+
|
|
257
|
+
function containsRateLimitFailure(result: Awaited<ReturnType<typeof executeReconciliation>>) {
|
|
258
|
+
return result.actions_taken.some((action) => {
|
|
259
|
+
const reason = typeof action.reason === 'string' ? action.reason.toLowerCase() : '';
|
|
260
|
+
return (
|
|
261
|
+
reason.includes('429') ||
|
|
262
|
+
reason.includes('too many requests') ||
|
|
263
|
+
reason.includes('rate limit')
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
252
267
|
});
|
|
253
268
|
|
|
254
269
|
async function resolveDefaultBudgetId(api: ynab.API): Promise<string> {
|
|
@@ -291,7 +306,12 @@ function buildIntegrationAnalysis(
|
|
|
291
306
|
const clearedDollars = snapshot.cleared_balance / 1000;
|
|
292
307
|
const totalDelta = transactionAmount * count;
|
|
293
308
|
const statementBalance = clearedDollars + totalDelta;
|
|
294
|
-
|
|
309
|
+
|
|
310
|
+
// Choose a base date safely in the past so YNAB accepts the transactions (no future dates),
|
|
311
|
+
// and include a nonce in payee names to avoid duplicate collisions across test runs.
|
|
312
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
313
|
+
const baseDate = Date.now() - (count + 1) * dayMs;
|
|
314
|
+
const runNonce = Date.now().toString();
|
|
295
315
|
|
|
296
316
|
return {
|
|
297
317
|
success: true,
|
|
@@ -312,12 +332,12 @@ function buildIntegrationAnalysis(
|
|
|
312
332
|
auto_matches: [],
|
|
313
333
|
suggested_matches: [],
|
|
314
334
|
unmatched_bank: Array.from({ length: count }, (_, index) => {
|
|
315
|
-
const date = new Date(baseDate + index *
|
|
335
|
+
const date = new Date(baseDate + index * dayMs);
|
|
316
336
|
return {
|
|
317
|
-
id: `integration-bank-${index}`,
|
|
337
|
+
id: `integration-bank-${index}-${runNonce}`,
|
|
318
338
|
date: date.toISOString().slice(0, 10),
|
|
319
339
|
amount: transactionAmount,
|
|
320
|
-
payee: `Integration Payee ${index}`,
|
|
340
|
+
payee: `Integration Payee ${index}-${runNonce}`,
|
|
321
341
|
memo: `Integration memo ${index}`,
|
|
322
342
|
original_csv_row: index + 1,
|
|
323
343
|
};
|