@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
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { generateRecommendations } from '../recommendationEngine.js';
|
|
3
|
+
import type {
|
|
4
|
+
RecommendationContext,
|
|
5
|
+
ReconciliationAnalysis,
|
|
6
|
+
BankTransaction,
|
|
7
|
+
YNABTransaction,
|
|
8
|
+
MoneyValue,
|
|
9
|
+
ReconciliationInsight,
|
|
10
|
+
CreateTransactionRecommendation,
|
|
11
|
+
UpdateClearedRecommendation,
|
|
12
|
+
ReviewDuplicateRecommendation,
|
|
13
|
+
ManualReviewRecommendation,
|
|
14
|
+
TransactionMatch,
|
|
15
|
+
} from '../types.js';
|
|
16
|
+
import { toMoneyValueFromDecimal } from '../../../utils/money.js';
|
|
17
|
+
|
|
18
|
+
// Helper to create MoneyValue objects
|
|
19
|
+
const makeMoney = (value: number, currency = 'USD'): MoneyValue => {
|
|
20
|
+
return toMoneyValueFromDecimal(value, currency);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// Helper to create minimal mock context
|
|
24
|
+
const createMockContext = (overrides?: Partial<RecommendationContext>): RecommendationContext => {
|
|
25
|
+
const defaultAnalysis: ReconciliationAnalysis = {
|
|
26
|
+
success: true,
|
|
27
|
+
phase: 'analysis',
|
|
28
|
+
matches: [],
|
|
29
|
+
auto_matches: [],
|
|
30
|
+
suggested_matches: [],
|
|
31
|
+
unmatched_bank: [],
|
|
32
|
+
unmatched_ynab: [],
|
|
33
|
+
balance_info: {
|
|
34
|
+
current_cleared: makeMoney(100),
|
|
35
|
+
current_uncleared: makeMoney(0),
|
|
36
|
+
current_total: makeMoney(100),
|
|
37
|
+
target_statement: makeMoney(100),
|
|
38
|
+
discrepancy: makeMoney(0),
|
|
39
|
+
on_track: true,
|
|
40
|
+
},
|
|
41
|
+
summary: {
|
|
42
|
+
statement_date_range: '2024-01-01 to 2024-01-31',
|
|
43
|
+
bank_transactions_count: 0,
|
|
44
|
+
ynab_transactions_count: 0,
|
|
45
|
+
auto_matched: 0,
|
|
46
|
+
suggested_matches: 0,
|
|
47
|
+
unmatched_bank: 0,
|
|
48
|
+
unmatched_ynab: 0,
|
|
49
|
+
current_cleared_balance: makeMoney(100),
|
|
50
|
+
target_statement_balance: makeMoney(100),
|
|
51
|
+
discrepancy: makeMoney(0),
|
|
52
|
+
discrepancy_explanation: 'Balanced',
|
|
53
|
+
},
|
|
54
|
+
insights: [],
|
|
55
|
+
next_steps: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
account_id: 'test-account-id',
|
|
60
|
+
budget_id: 'test-budget-id',
|
|
61
|
+
analysis: defaultAnalysis,
|
|
62
|
+
matching_config: {
|
|
63
|
+
amountToleranceCents: 1,
|
|
64
|
+
dateToleranceDays: 2,
|
|
65
|
+
descriptionSimilarityThreshold: 0.8,
|
|
66
|
+
autoMatchThreshold: 90,
|
|
67
|
+
suggestionThreshold: 60,
|
|
68
|
+
},
|
|
69
|
+
...overrides,
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Helper to create mock bank transaction
|
|
74
|
+
const createBankTransaction = (overrides?: Partial<BankTransaction>): BankTransaction => ({
|
|
75
|
+
id: 'bank-txn-1',
|
|
76
|
+
date: '2024-01-15',
|
|
77
|
+
amount: -50.0,
|
|
78
|
+
payee: 'Test Store',
|
|
79
|
+
memo: 'Test memo',
|
|
80
|
+
original_csv_row: 1,
|
|
81
|
+
...overrides,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Helper to create mock YNAB transaction
|
|
85
|
+
const createYNABTransaction = (overrides?: Partial<YNABTransaction>): YNABTransaction => ({
|
|
86
|
+
id: 'ynab-txn-1',
|
|
87
|
+
date: '2024-01-15',
|
|
88
|
+
amount: -50000,
|
|
89
|
+
payee_name: 'Test Store',
|
|
90
|
+
category_name: 'Shopping',
|
|
91
|
+
cleared: 'uncleared',
|
|
92
|
+
approved: true,
|
|
93
|
+
memo: 'Test memo',
|
|
94
|
+
...overrides,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Helper to create mock insight
|
|
98
|
+
const createInsight = (
|
|
99
|
+
type: ReconciliationInsight['type'],
|
|
100
|
+
severity: ReconciliationInsight['severity'] = 'info',
|
|
101
|
+
): ReconciliationInsight => ({
|
|
102
|
+
id: `insight-${type}-1`,
|
|
103
|
+
type,
|
|
104
|
+
severity,
|
|
105
|
+
title: `Test ${type} insight`,
|
|
106
|
+
description: `This is a test ${type} insight`,
|
|
107
|
+
evidence: {},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('recommendationEngine', () => {
|
|
111
|
+
describe('generateRecommendations', () => {
|
|
112
|
+
describe('empty context scenarios', () => {
|
|
113
|
+
it('should return empty array for completely empty context', () => {
|
|
114
|
+
const context = createMockContext();
|
|
115
|
+
const recommendations = generateRecommendations(context);
|
|
116
|
+
expect(recommendations).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should return empty array with no insights and no unmatched transactions', () => {
|
|
120
|
+
const context = createMockContext({
|
|
121
|
+
analysis: {
|
|
122
|
+
...createMockContext().analysis,
|
|
123
|
+
insights: [],
|
|
124
|
+
unmatched_bank: [],
|
|
125
|
+
unmatched_ynab: [],
|
|
126
|
+
suggested_matches: [],
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const recommendations = generateRecommendations(context);
|
|
130
|
+
expect(recommendations).toEqual([]);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe('insight processing', () => {
|
|
135
|
+
it('should process near_match insights', () => {
|
|
136
|
+
const insight = createInsight('near_match', 'warning');
|
|
137
|
+
const context = createMockContext({
|
|
138
|
+
analysis: {
|
|
139
|
+
...createMockContext().analysis,
|
|
140
|
+
insights: [insight],
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const recommendations = generateRecommendations(context);
|
|
145
|
+
|
|
146
|
+
expect(recommendations).toHaveLength(1);
|
|
147
|
+
const rec = recommendations[0] as ManualReviewRecommendation;
|
|
148
|
+
expect(rec.action_type).toBe('manual_review');
|
|
149
|
+
expect(rec.priority).toBe('medium');
|
|
150
|
+
expect(rec.confidence).toBe(0.7); // CONFIDENCE.NEAR_MATCH_REVIEW
|
|
151
|
+
expect(rec.source_insight_id).toBe(insight.id);
|
|
152
|
+
expect(rec.parameters.issue_type).toBe('complex_match');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should process repeat_amount insights', () => {
|
|
156
|
+
const insight = createInsight('repeat_amount', 'info');
|
|
157
|
+
const context = createMockContext({
|
|
158
|
+
analysis: {
|
|
159
|
+
...createMockContext().analysis,
|
|
160
|
+
insights: [insight],
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const recommendations = generateRecommendations(context);
|
|
165
|
+
|
|
166
|
+
expect(recommendations).toHaveLength(1);
|
|
167
|
+
const rec = recommendations[0] as ManualReviewRecommendation;
|
|
168
|
+
expect(rec.action_type).toBe('manual_review');
|
|
169
|
+
expect(rec.priority).toBe('medium');
|
|
170
|
+
expect(rec.confidence).toBe(0.75); // CONFIDENCE.REPEAT_AMOUNT
|
|
171
|
+
expect(rec.source_insight_id).toBe(insight.id);
|
|
172
|
+
expect(rec.message).toContain('recurring pattern');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('should process anomaly insights', () => {
|
|
176
|
+
const insight = createInsight('anomaly', 'warning');
|
|
177
|
+
const context = createMockContext({
|
|
178
|
+
analysis: {
|
|
179
|
+
...createMockContext().analysis,
|
|
180
|
+
insights: [insight],
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const recommendations = generateRecommendations(context);
|
|
185
|
+
|
|
186
|
+
expect(recommendations).toHaveLength(1);
|
|
187
|
+
const rec = recommendations[0] as ManualReviewRecommendation;
|
|
188
|
+
expect(rec.action_type).toBe('manual_review');
|
|
189
|
+
expect(rec.priority).toBe('low');
|
|
190
|
+
expect(rec.confidence).toBe(0.5); // CONFIDENCE.ANOMALY_REVIEW
|
|
191
|
+
expect(rec.source_insight_id).toBe(insight.id);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('should process critical anomaly insights with higher severity', () => {
|
|
195
|
+
const insight = createInsight('anomaly', 'critical');
|
|
196
|
+
const context = createMockContext({
|
|
197
|
+
analysis: {
|
|
198
|
+
...createMockContext().analysis,
|
|
199
|
+
insights: [insight],
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const recommendations = generateRecommendations(context);
|
|
204
|
+
|
|
205
|
+
expect(recommendations).toHaveLength(1);
|
|
206
|
+
const rec = recommendations[0] as ManualReviewRecommendation;
|
|
207
|
+
expect(rec.action_type).toBe('manual_review');
|
|
208
|
+
expect(rec.parameters.issue_type).toBe('large_discrepancy');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should process multiple insights', () => {
|
|
212
|
+
const insights = [
|
|
213
|
+
createInsight('near_match', 'warning'),
|
|
214
|
+
createInsight('repeat_amount', 'info'),
|
|
215
|
+
createInsight('anomaly', 'warning'),
|
|
216
|
+
];
|
|
217
|
+
const context = createMockContext({
|
|
218
|
+
analysis: {
|
|
219
|
+
...createMockContext().analysis,
|
|
220
|
+
insights,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const recommendations = generateRecommendations(context);
|
|
225
|
+
|
|
226
|
+
expect(recommendations).toHaveLength(3);
|
|
227
|
+
expect(recommendations.map((r) => r.action_type)).toEqual([
|
|
228
|
+
'manual_review',
|
|
229
|
+
'manual_review',
|
|
230
|
+
'manual_review',
|
|
231
|
+
]);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
describe('unmatched bank transactions', () => {
|
|
236
|
+
it('should create create_transaction recommendation for unmatched bank transaction', () => {
|
|
237
|
+
const bankTxn = createBankTransaction({
|
|
238
|
+
id: 'bank-1',
|
|
239
|
+
amount: -75.5,
|
|
240
|
+
payee: 'Coffee Shop',
|
|
241
|
+
date: '2024-01-20',
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const context = createMockContext({
|
|
245
|
+
analysis: {
|
|
246
|
+
...createMockContext().analysis,
|
|
247
|
+
unmatched_bank: [bankTxn],
|
|
248
|
+
},
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const recommendations = generateRecommendations(context);
|
|
252
|
+
|
|
253
|
+
expect(recommendations).toHaveLength(1);
|
|
254
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
255
|
+
expect(rec.action_type).toBe('create_transaction');
|
|
256
|
+
expect(rec.priority).toBe('medium');
|
|
257
|
+
expect(rec.confidence).toBe(0.8); // CONFIDENCE.UNMATCHED_BANK
|
|
258
|
+
expect(rec.parameters.account_id).toBe('test-account-id');
|
|
259
|
+
expect(rec.parameters.date).toBe('2024-01-20');
|
|
260
|
+
expect(rec.parameters.amount).toBe(-75500); // In milliunits
|
|
261
|
+
expect(rec.parameters.payee_name).toBe('Coffee Shop');
|
|
262
|
+
expect(rec.parameters.cleared).toBe('cleared');
|
|
263
|
+
expect(rec.parameters.approved).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should include memo if present in bank transaction', () => {
|
|
267
|
+
const bankTxn = createBankTransaction({
|
|
268
|
+
memo: 'Business expense',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const context = createMockContext({
|
|
272
|
+
analysis: {
|
|
273
|
+
...createMockContext().analysis,
|
|
274
|
+
unmatched_bank: [bankTxn],
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
const recommendations = generateRecommendations(context);
|
|
279
|
+
|
|
280
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
281
|
+
expect(rec.parameters.memo).toBe('Business expense');
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('should not include memo if not present in bank transaction', () => {
|
|
285
|
+
const bankTxn = createBankTransaction();
|
|
286
|
+
delete bankTxn.memo;
|
|
287
|
+
|
|
288
|
+
const context = createMockContext({
|
|
289
|
+
analysis: {
|
|
290
|
+
...createMockContext().analysis,
|
|
291
|
+
unmatched_bank: [bankTxn],
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const recommendations = generateRecommendations(context);
|
|
296
|
+
|
|
297
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
298
|
+
expect(rec.parameters.memo).toBeUndefined();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should create recommendations for multiple unmatched bank transactions', () => {
|
|
302
|
+
const bankTxns = [
|
|
303
|
+
createBankTransaction({ id: 'b1', amount: -10.0 }),
|
|
304
|
+
createBankTransaction({ id: 'b2', amount: -20.0 }),
|
|
305
|
+
createBankTransaction({ id: 'b3', amount: -30.0 }),
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
const context = createMockContext({
|
|
309
|
+
analysis: {
|
|
310
|
+
...createMockContext().analysis,
|
|
311
|
+
unmatched_bank: bankTxns,
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
const recommendations = generateRecommendations(context);
|
|
316
|
+
|
|
317
|
+
expect(recommendations).toHaveLength(3);
|
|
318
|
+
expect(recommendations.every((r) => r.action_type === 'create_transaction')).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
describe('unmatched YNAB transactions', () => {
|
|
323
|
+
it('should create update_cleared recommendation for uncleared YNAB transaction', () => {
|
|
324
|
+
const ynabTxn = createYNABTransaction({
|
|
325
|
+
id: 'ynab-1',
|
|
326
|
+
cleared: 'uncleared',
|
|
327
|
+
payee_name: 'Restaurant',
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const context = createMockContext({
|
|
331
|
+
analysis: {
|
|
332
|
+
...createMockContext().analysis,
|
|
333
|
+
unmatched_ynab: [ynabTxn],
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
const recommendations = generateRecommendations(context);
|
|
338
|
+
|
|
339
|
+
expect(recommendations).toHaveLength(1);
|
|
340
|
+
const rec = recommendations[0] as UpdateClearedRecommendation;
|
|
341
|
+
expect(rec.action_type).toBe('update_cleared');
|
|
342
|
+
expect(rec.priority).toBe('low');
|
|
343
|
+
expect(rec.confidence).toBe(0.6); // CONFIDENCE.UPDATE_CLEARED
|
|
344
|
+
expect(rec.parameters.transaction_id).toBe('ynab-1');
|
|
345
|
+
expect(rec.parameters.cleared).toBe('cleared');
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
it('should not create recommendation for already cleared YNAB transaction', () => {
|
|
349
|
+
const ynabTxn = createYNABTransaction({
|
|
350
|
+
cleared: 'cleared',
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const context = createMockContext({
|
|
354
|
+
analysis: {
|
|
355
|
+
...createMockContext().analysis,
|
|
356
|
+
unmatched_ynab: [ynabTxn],
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const recommendations = generateRecommendations(context);
|
|
361
|
+
|
|
362
|
+
expect(recommendations).toHaveLength(0);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should not create recommendation for reconciled YNAB transaction', () => {
|
|
366
|
+
const ynabTxn = createYNABTransaction({
|
|
367
|
+
cleared: 'reconciled',
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
const context = createMockContext({
|
|
371
|
+
analysis: {
|
|
372
|
+
...createMockContext().analysis,
|
|
373
|
+
unmatched_ynab: [ynabTxn],
|
|
374
|
+
},
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const recommendations = generateRecommendations(context);
|
|
378
|
+
|
|
379
|
+
expect(recommendations).toHaveLength(0);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should handle null payee_name in YNAB transaction', () => {
|
|
383
|
+
const ynabTxn = createYNABTransaction({
|
|
384
|
+
cleared: 'uncleared',
|
|
385
|
+
payee_name: null,
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const context = createMockContext({
|
|
389
|
+
analysis: {
|
|
390
|
+
...createMockContext().analysis,
|
|
391
|
+
unmatched_ynab: [ynabTxn],
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
const recommendations = generateRecommendations(context);
|
|
396
|
+
|
|
397
|
+
expect(recommendations).toHaveLength(1);
|
|
398
|
+
const rec = recommendations[0] as UpdateClearedRecommendation;
|
|
399
|
+
expect(rec.message).toContain('Unknown');
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
describe('suggested matches', () => {
|
|
404
|
+
it('should create review_duplicate recommendation for suggested match', () => {
|
|
405
|
+
const bankTxn = createBankTransaction();
|
|
406
|
+
const ynabTxn = createYNABTransaction();
|
|
407
|
+
|
|
408
|
+
const suggestedMatch: TransactionMatch = {
|
|
409
|
+
bank_transaction: bankTxn,
|
|
410
|
+
ynab_transaction: ynabTxn,
|
|
411
|
+
confidence: 'medium',
|
|
412
|
+
confidence_score: 75,
|
|
413
|
+
match_reason: 'Fuzzy payee match',
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const context = createMockContext({
|
|
417
|
+
analysis: {
|
|
418
|
+
...createMockContext().analysis,
|
|
419
|
+
suggested_matches: [suggestedMatch],
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const recommendations = generateRecommendations(context);
|
|
424
|
+
|
|
425
|
+
expect(recommendations).toHaveLength(1);
|
|
426
|
+
const rec = recommendations[0] as ReviewDuplicateRecommendation;
|
|
427
|
+
expect(rec.action_type).toBe('review_duplicate');
|
|
428
|
+
expect(rec.priority).toBe('high');
|
|
429
|
+
expect(rec.confidence).toBe(0.75); // confidence_score / 100
|
|
430
|
+
expect(rec.parameters.candidate_ids).toContain(ynabTxn.id);
|
|
431
|
+
expect(rec.parameters.suggested_match_id).toBe(ynabTxn.id);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should create create_transaction for suggested match with no YNAB transaction', () => {
|
|
435
|
+
const bankTxn = createBankTransaction({ amount: -45.0 });
|
|
436
|
+
|
|
437
|
+
const suggestedMatch: TransactionMatch = {
|
|
438
|
+
bank_transaction: bankTxn,
|
|
439
|
+
confidence: 'none',
|
|
440
|
+
confidence_score: 0,
|
|
441
|
+
match_reason: 'No match found',
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const context = createMockContext({
|
|
445
|
+
analysis: {
|
|
446
|
+
...createMockContext().analysis,
|
|
447
|
+
suggested_matches: [suggestedMatch],
|
|
448
|
+
},
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const recommendations = generateRecommendations(context);
|
|
452
|
+
|
|
453
|
+
expect(recommendations).toHaveLength(1);
|
|
454
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
455
|
+
expect(rec.action_type).toBe('create_transaction');
|
|
456
|
+
expect(rec.priority).toBe('high');
|
|
457
|
+
expect(rec.confidence).toBe(0.95); // CONFIDENCE.CREATE_EXACT_MATCH
|
|
458
|
+
expect(rec.parameters.amount).toBe(-45000); // In milliunits
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should create manual_review for combination match with multiple candidates', () => {
|
|
462
|
+
const bankTxn = createBankTransaction({ amount: -100.0, payee: 'Split Payment' });
|
|
463
|
+
const ynabTxn1 = createYNABTransaction({
|
|
464
|
+
id: 'y1',
|
|
465
|
+
amount: -50000,
|
|
466
|
+
payee_name: 'Vendor A',
|
|
467
|
+
});
|
|
468
|
+
const ynabTxn2 = createYNABTransaction({
|
|
469
|
+
id: 'y2',
|
|
470
|
+
amount: -50000,
|
|
471
|
+
payee_name: 'Vendor B',
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const suggestedMatch: TransactionMatch = {
|
|
475
|
+
bank_transaction: bankTxn,
|
|
476
|
+
candidates: [
|
|
477
|
+
{
|
|
478
|
+
ynab_transaction: ynabTxn1,
|
|
479
|
+
confidence: 60,
|
|
480
|
+
match_reason: 'Partial amount match',
|
|
481
|
+
explanation: 'Amount matches half of bank transaction',
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
ynab_transaction: ynabTxn2,
|
|
485
|
+
confidence: 60,
|
|
486
|
+
match_reason: 'Partial amount match',
|
|
487
|
+
explanation: 'Amount matches half of bank transaction',
|
|
488
|
+
},
|
|
489
|
+
],
|
|
490
|
+
confidence: 'medium',
|
|
491
|
+
confidence_score: 60,
|
|
492
|
+
match_reason: 'combination_match',
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const context = createMockContext({
|
|
496
|
+
analysis: {
|
|
497
|
+
...createMockContext().analysis,
|
|
498
|
+
suggested_matches: [suggestedMatch],
|
|
499
|
+
},
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const recommendations = generateRecommendations(context);
|
|
503
|
+
|
|
504
|
+
expect(recommendations).toHaveLength(1);
|
|
505
|
+
const rec = recommendations[0] as ManualReviewRecommendation;
|
|
506
|
+
expect(rec.action_type).toBe('manual_review');
|
|
507
|
+
expect(rec.priority).toBe('medium');
|
|
508
|
+
expect(rec.confidence).toBe(0.7); // CONFIDENCE.NEAR_MATCH_REVIEW
|
|
509
|
+
expect(rec.parameters.issue_type).toBe('complex_match');
|
|
510
|
+
expect(rec.parameters.related_transactions).toHaveLength(3); // 1 bank + 2 YNAB
|
|
511
|
+
|
|
512
|
+
// Verify related transactions structure
|
|
513
|
+
const relatedTxns = rec.parameters.related_transactions!;
|
|
514
|
+
expect(relatedTxns[0]?.source).toBe('bank');
|
|
515
|
+
expect(relatedTxns[0]?.id).toBe(bankTxn.id);
|
|
516
|
+
expect(relatedTxns[0]?.description).toBe('Split Payment');
|
|
517
|
+
expect(relatedTxns[1]?.source).toBe('ynab');
|
|
518
|
+
expect(relatedTxns[1]?.id).toBe('y1');
|
|
519
|
+
expect(relatedTxns[2]?.source).toBe('ynab');
|
|
520
|
+
expect(relatedTxns[2]?.id).toBe('y2');
|
|
521
|
+
|
|
522
|
+
// Verify enhanced metadata
|
|
523
|
+
expect(rec.metadata?.bank_transaction_amount).toBeDefined();
|
|
524
|
+
expect(rec.metadata?.bank_transaction_amount.value).toBe(-100.0);
|
|
525
|
+
expect(rec.metadata?.candidate_total_amount).toBeDefined();
|
|
526
|
+
expect(rec.metadata?.candidate_total_amount.value).toBe(-100.0); // -50 + -50
|
|
527
|
+
expect(rec.metadata?.candidate_count).toBe(2);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('amount sign preservation (CRITICAL)', () => {
|
|
532
|
+
it('should preserve negative amounts for expenses in create_transaction', () => {
|
|
533
|
+
const bankTxn = createBankTransaction({
|
|
534
|
+
amount: -123.45,
|
|
535
|
+
payee: 'Grocery Store',
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const context = createMockContext({
|
|
539
|
+
analysis: {
|
|
540
|
+
...createMockContext().analysis,
|
|
541
|
+
unmatched_bank: [bankTxn],
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const recommendations = generateRecommendations(context);
|
|
546
|
+
|
|
547
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
548
|
+
expect(rec.parameters.amount).toBe(-123450); // In milliunits
|
|
549
|
+
expect(rec.parameters.amount).toBeLessThan(0);
|
|
550
|
+
expect(rec.estimated_impact.value).toBe(-123.45); // Estimated impact stays in dollars
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should preserve positive amounts for income in create_transaction', () => {
|
|
554
|
+
const bankTxn = createBankTransaction({
|
|
555
|
+
amount: 500.0,
|
|
556
|
+
payee: 'Paycheck Refund',
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
const context = createMockContext({
|
|
560
|
+
analysis: {
|
|
561
|
+
...createMockContext().analysis,
|
|
562
|
+
unmatched_bank: [bankTxn],
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const recommendations = generateRecommendations(context);
|
|
567
|
+
|
|
568
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
569
|
+
expect(rec.parameters.amount).toBe(500000); // In milliunits
|
|
570
|
+
expect(rec.parameters.amount).toBeGreaterThan(0);
|
|
571
|
+
expect(rec.estimated_impact.value).toBe(500.0); // Estimated impact stays in dollars
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
it('should preserve negative amounts in suggested match create_transaction', () => {
|
|
575
|
+
const bankTxn = createBankTransaction({ amount: -99.99 });
|
|
576
|
+
|
|
577
|
+
const suggestedMatch: TransactionMatch = {
|
|
578
|
+
bank_transaction: bankTxn,
|
|
579
|
+
confidence: 'none',
|
|
580
|
+
confidence_score: 0,
|
|
581
|
+
match_reason: 'No match',
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const context = createMockContext({
|
|
585
|
+
analysis: {
|
|
586
|
+
...createMockContext().analysis,
|
|
587
|
+
suggested_matches: [suggestedMatch],
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
const recommendations = generateRecommendations(context);
|
|
592
|
+
|
|
593
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
594
|
+
expect(rec.parameters.amount).toBe(-99990); // In milliunits
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it('should handle zero amounts correctly', () => {
|
|
598
|
+
const bankTxn = createBankTransaction({ amount: 0 });
|
|
599
|
+
|
|
600
|
+
const context = createMockContext({
|
|
601
|
+
analysis: {
|
|
602
|
+
...createMockContext().analysis,
|
|
603
|
+
unmatched_bank: [bankTxn],
|
|
604
|
+
},
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const recommendations = generateRecommendations(context);
|
|
608
|
+
|
|
609
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
610
|
+
expect(rec.parameters.amount).toBe(0); // Zero in milliunits is still zero
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe('sorting logic', () => {
|
|
615
|
+
it('should sort by priority (high > medium > low)', () => {
|
|
616
|
+
const bankTxn = createBankTransaction();
|
|
617
|
+
const ynabTxn = createYNABTransaction({ cleared: 'uncleared' });
|
|
618
|
+
const insight = createInsight('anomaly', 'warning');
|
|
619
|
+
|
|
620
|
+
// Create context that will generate all priority levels
|
|
621
|
+
const context = createMockContext({
|
|
622
|
+
analysis: {
|
|
623
|
+
...createMockContext().analysis,
|
|
624
|
+
unmatched_bank: [bankTxn], // medium priority
|
|
625
|
+
unmatched_ynab: [ynabTxn], // low priority
|
|
626
|
+
insights: [insight], // low priority (anomaly)
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
const recommendations = generateRecommendations(context);
|
|
631
|
+
|
|
632
|
+
expect(recommendations).toHaveLength(3);
|
|
633
|
+
// First should be medium (unmatched_bank)
|
|
634
|
+
expect(recommendations[0].priority).toBe('medium');
|
|
635
|
+
// Next should be low priority items
|
|
636
|
+
expect(recommendations[1].priority).toBe('low');
|
|
637
|
+
expect(recommendations[2].priority).toBe('low');
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('should sort by confidence when priorities are equal', () => {
|
|
641
|
+
const insights = [
|
|
642
|
+
createInsight('near_match', 'warning'), // priority: medium, confidence: 0.7
|
|
643
|
+
createInsight('repeat_amount', 'info'), // priority: medium, confidence: 0.75
|
|
644
|
+
];
|
|
645
|
+
|
|
646
|
+
const context = createMockContext({
|
|
647
|
+
analysis: {
|
|
648
|
+
...createMockContext().analysis,
|
|
649
|
+
insights,
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
const recommendations = generateRecommendations(context);
|
|
654
|
+
|
|
655
|
+
expect(recommendations).toHaveLength(2);
|
|
656
|
+
// Both are medium priority, so should be sorted by confidence (0.75 > 0.7)
|
|
657
|
+
expect(recommendations[0].confidence).toBe(0.75); // repeat_amount
|
|
658
|
+
expect(recommendations[1].confidence).toBe(0.7); // near_match
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
it('should handle mixed priorities and confidence', () => {
|
|
662
|
+
const bankTxn = createBankTransaction();
|
|
663
|
+
const ynabTxn = createYNABTransaction({ cleared: 'uncleared' });
|
|
664
|
+
const suggestedMatch: TransactionMatch = {
|
|
665
|
+
bank_transaction: bankTxn,
|
|
666
|
+
ynab_transaction: ynabTxn,
|
|
667
|
+
confidence: 'medium',
|
|
668
|
+
confidence_score: 75,
|
|
669
|
+
match_reason: 'Suggested',
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
const insights = [
|
|
673
|
+
createInsight('near_match', 'warning'), // medium priority, 0.7 confidence
|
|
674
|
+
createInsight('anomaly', 'warning'), // low priority, 0.5 confidence
|
|
675
|
+
];
|
|
676
|
+
|
|
677
|
+
const context = createMockContext({
|
|
678
|
+
analysis: {
|
|
679
|
+
...createMockContext().analysis,
|
|
680
|
+
suggested_matches: [suggestedMatch], // high priority, 0.75 confidence
|
|
681
|
+
unmatched_bank: [createBankTransaction({ id: 'b2' })], // medium priority, 0.8 confidence
|
|
682
|
+
insights,
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
const recommendations = generateRecommendations(context);
|
|
687
|
+
|
|
688
|
+
// Should be: high priority first, then medium sorted by confidence, then low
|
|
689
|
+
expect(recommendations[0].priority).toBe('high');
|
|
690
|
+
expect(recommendations[1].priority).toBe('medium');
|
|
691
|
+
expect(recommendations[1].confidence).toBe(0.8); // unmatched_bank
|
|
692
|
+
expect(recommendations[2].priority).toBe('medium');
|
|
693
|
+
expect(recommendations[2].confidence).toBe(0.7); // near_match
|
|
694
|
+
expect(recommendations[3].priority).toBe('low');
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
describe('recommendation metadata', () => {
|
|
699
|
+
it('should include correct metadata in all recommendations', () => {
|
|
700
|
+
const bankTxn = createBankTransaction();
|
|
701
|
+
const context = createMockContext({
|
|
702
|
+
analysis: {
|
|
703
|
+
...createMockContext().analysis,
|
|
704
|
+
unmatched_bank: [bankTxn],
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
const recommendations = generateRecommendations(context);
|
|
709
|
+
|
|
710
|
+
const rec = recommendations[0];
|
|
711
|
+
expect(rec.id).toBeDefined();
|
|
712
|
+
expect(rec.account_id).toBe('test-account-id');
|
|
713
|
+
expect(rec.metadata.version).toBe('1.0');
|
|
714
|
+
expect(rec.metadata.created_at).toBeDefined();
|
|
715
|
+
expect(new Date(rec.metadata.created_at).getTime()).not.toBeNaN();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it('should generate unique IDs for each recommendation', () => {
|
|
719
|
+
const bankTxns = [
|
|
720
|
+
createBankTransaction({ id: 'b1' }),
|
|
721
|
+
createBankTransaction({ id: 'b2' }),
|
|
722
|
+
createBankTransaction({ id: 'b3' }),
|
|
723
|
+
];
|
|
724
|
+
|
|
725
|
+
const context = createMockContext({
|
|
726
|
+
analysis: {
|
|
727
|
+
...createMockContext().analysis,
|
|
728
|
+
unmatched_bank: bankTxns,
|
|
729
|
+
},
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const recommendations = generateRecommendations(context);
|
|
733
|
+
|
|
734
|
+
const ids = recommendations.map((r) => r.id);
|
|
735
|
+
const uniqueIds = new Set(ids);
|
|
736
|
+
expect(uniqueIds.size).toBe(ids.length);
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
it('should use correct currency from balance info', () => {
|
|
740
|
+
const bankTxn = createBankTransaction({ amount: -50.0 });
|
|
741
|
+
const context = createMockContext({
|
|
742
|
+
analysis: {
|
|
743
|
+
...createMockContext().analysis,
|
|
744
|
+
unmatched_bank: [bankTxn],
|
|
745
|
+
balance_info: {
|
|
746
|
+
...createMockContext().analysis.balance_info,
|
|
747
|
+
current_cleared: makeMoney(100, 'EUR'),
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
const recommendations = generateRecommendations(context);
|
|
753
|
+
|
|
754
|
+
const rec = recommendations[0];
|
|
755
|
+
expect(rec.estimated_impact.currency).toBe('EUR');
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
it('should include enhanced metadata in insight-based manual review recommendations', () => {
|
|
759
|
+
const insight = createInsight('near_match', 'critical');
|
|
760
|
+
const context = createMockContext({
|
|
761
|
+
analysis: {
|
|
762
|
+
...createMockContext().analysis,
|
|
763
|
+
insights: [insight],
|
|
764
|
+
balance_info: {
|
|
765
|
+
...createMockContext().analysis.balance_info,
|
|
766
|
+
discrepancy: makeMoney(-100, 'USD'),
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const recommendations = generateRecommendations(context);
|
|
772
|
+
|
|
773
|
+
expect(recommendations).toHaveLength(1);
|
|
774
|
+
const rec = recommendations[0] as ManualReviewRecommendation;
|
|
775
|
+
expect(rec.action_type).toBe('manual_review');
|
|
776
|
+
|
|
777
|
+
// Verify enhanced metadata fields
|
|
778
|
+
expect(rec.metadata?.current_discrepancy).toBeDefined();
|
|
779
|
+
expect(rec.metadata?.current_discrepancy.value).toBe(-100);
|
|
780
|
+
expect(rec.metadata?.current_discrepancy.currency).toBe('USD');
|
|
781
|
+
expect(rec.metadata?.insight_severity).toBe('critical');
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
it('should include enhanced metadata for all insight-based recommendation types', () => {
|
|
785
|
+
const nearMatchInsight = createInsight('near_match', 'warning');
|
|
786
|
+
const repeatAmountInsight = createInsight('repeat_amount', 'info');
|
|
787
|
+
const anomalyInsight = createInsight('anomaly', 'critical');
|
|
788
|
+
|
|
789
|
+
const context = createMockContext({
|
|
790
|
+
analysis: {
|
|
791
|
+
...createMockContext().analysis,
|
|
792
|
+
insights: [nearMatchInsight, repeatAmountInsight, anomalyInsight],
|
|
793
|
+
balance_info: {
|
|
794
|
+
...createMockContext().analysis.balance_info,
|
|
795
|
+
discrepancy: makeMoney(-250.75, 'CAD'),
|
|
796
|
+
},
|
|
797
|
+
},
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
const recommendations = generateRecommendations(context);
|
|
801
|
+
|
|
802
|
+
expect(recommendations).toHaveLength(3);
|
|
803
|
+
|
|
804
|
+
// All insight-based recommendations should have enhanced metadata
|
|
805
|
+
for (const rec of recommendations) {
|
|
806
|
+
expect(rec.action_type).toBe('manual_review');
|
|
807
|
+
const manualRec = rec as ManualReviewRecommendation;
|
|
808
|
+
|
|
809
|
+
expect(manualRec.metadata?.current_discrepancy).toBeDefined();
|
|
810
|
+
expect(manualRec.metadata?.current_discrepancy.value).toBe(-250.75);
|
|
811
|
+
expect(manualRec.metadata?.current_discrepancy.currency).toBe('CAD');
|
|
812
|
+
expect(manualRec.metadata?.insight_severity).toMatch(/^(info|warning|critical)$/);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// Verify specific severities
|
|
816
|
+
const severities = recommendations.map(
|
|
817
|
+
(r) => (r as ManualReviewRecommendation).metadata?.insight_severity,
|
|
818
|
+
);
|
|
819
|
+
expect(severities).toContain('warning');
|
|
820
|
+
expect(severities).toContain('info');
|
|
821
|
+
expect(severities).toContain('critical');
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
describe('edge cases', () => {
|
|
826
|
+
it('should handle empty insights array', () => {
|
|
827
|
+
const context = createMockContext({
|
|
828
|
+
analysis: {
|
|
829
|
+
...createMockContext().analysis,
|
|
830
|
+
insights: [],
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
const recommendations = generateRecommendations(context);
|
|
835
|
+
expect(recommendations).toEqual([]);
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
it('should handle empty unmatched arrays', () => {
|
|
839
|
+
const context = createMockContext({
|
|
840
|
+
analysis: {
|
|
841
|
+
...createMockContext().analysis,
|
|
842
|
+
unmatched_bank: [],
|
|
843
|
+
unmatched_ynab: [],
|
|
844
|
+
suggested_matches: [],
|
|
845
|
+
},
|
|
846
|
+
});
|
|
847
|
+
|
|
848
|
+
const recommendations = generateRecommendations(context);
|
|
849
|
+
expect(recommendations).toEqual([]);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it('should handle missing optional fields', () => {
|
|
853
|
+
const bankTxn = createBankTransaction();
|
|
854
|
+
delete bankTxn.memo;
|
|
855
|
+
|
|
856
|
+
const context = createMockContext({
|
|
857
|
+
analysis: {
|
|
858
|
+
...createMockContext().analysis,
|
|
859
|
+
unmatched_bank: [bankTxn],
|
|
860
|
+
},
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
const recommendations = generateRecommendations(context);
|
|
864
|
+
|
|
865
|
+
expect(recommendations).toHaveLength(1);
|
|
866
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
867
|
+
expect(rec.parameters.memo).toBeUndefined();
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
it('should handle very large arrays of recommendations', () => {
|
|
871
|
+
const bankTxns = Array.from({ length: 100 }, (_, i) =>
|
|
872
|
+
createBankTransaction({ id: `b${i}`, amount: -10.0 - i }),
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
const context = createMockContext({
|
|
876
|
+
analysis: {
|
|
877
|
+
...createMockContext().analysis,
|
|
878
|
+
unmatched_bank: bankTxns,
|
|
879
|
+
},
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
const recommendations = generateRecommendations(context);
|
|
883
|
+
|
|
884
|
+
expect(recommendations).toHaveLength(100);
|
|
885
|
+
// Verify sorting still works
|
|
886
|
+
for (let i = 0; i < recommendations.length - 1; i++) {
|
|
887
|
+
expect(recommendations[i].priority).toBeDefined();
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
it('should handle insight without evidence', () => {
|
|
892
|
+
const insight = createInsight('near_match');
|
|
893
|
+
delete insight.evidence;
|
|
894
|
+
|
|
895
|
+
const context = createMockContext({
|
|
896
|
+
analysis: {
|
|
897
|
+
...createMockContext().analysis,
|
|
898
|
+
insights: [insight],
|
|
899
|
+
},
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
const recommendations = generateRecommendations(context);
|
|
903
|
+
|
|
904
|
+
expect(recommendations).toHaveLength(1);
|
|
905
|
+
expect(recommendations[0]).toBeDefined();
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('should handle transactions with very small amounts', () => {
|
|
909
|
+
const bankTxn = createBankTransaction({ amount: -0.01 });
|
|
910
|
+
|
|
911
|
+
const context = createMockContext({
|
|
912
|
+
analysis: {
|
|
913
|
+
...createMockContext().analysis,
|
|
914
|
+
unmatched_bank: [bankTxn],
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
const recommendations = generateRecommendations(context);
|
|
919
|
+
|
|
920
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
921
|
+
expect(rec.parameters.amount).toBe(-10); // In milliunits (0.01 * 1000)
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('should handle transactions with very large amounts', () => {
|
|
925
|
+
const bankTxn = createBankTransaction({ amount: -999999.99 });
|
|
926
|
+
|
|
927
|
+
const context = createMockContext({
|
|
928
|
+
analysis: {
|
|
929
|
+
...createMockContext().analysis,
|
|
930
|
+
unmatched_bank: [bankTxn],
|
|
931
|
+
},
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
const recommendations = generateRecommendations(context);
|
|
935
|
+
|
|
936
|
+
const rec = recommendations[0] as CreateTransactionRecommendation;
|
|
937
|
+
expect(rec.parameters.amount).toBe(-999999990); // In milliunits
|
|
938
|
+
});
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
describe('combined scenarios', () => {
|
|
942
|
+
it('should handle combination of insights, unmatched bank, and unmatched YNAB', () => {
|
|
943
|
+
const bankTxn = createBankTransaction({ id: 'b1' });
|
|
944
|
+
const ynabTxn = createYNABTransaction({ id: 'y1', cleared: 'uncleared' });
|
|
945
|
+
const insight = createInsight('near_match', 'warning');
|
|
946
|
+
|
|
947
|
+
const context = createMockContext({
|
|
948
|
+
analysis: {
|
|
949
|
+
...createMockContext().analysis,
|
|
950
|
+
insights: [insight],
|
|
951
|
+
unmatched_bank: [bankTxn],
|
|
952
|
+
unmatched_ynab: [ynabTxn],
|
|
953
|
+
},
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
const recommendations = generateRecommendations(context);
|
|
957
|
+
|
|
958
|
+
expect(recommendations).toHaveLength(3);
|
|
959
|
+
expect(recommendations.map((r) => r.action_type).sort()).toEqual([
|
|
960
|
+
'create_transaction',
|
|
961
|
+
'manual_review',
|
|
962
|
+
'update_cleared',
|
|
963
|
+
]);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
it('should handle all recommendation types at once', () => {
|
|
967
|
+
const bankTxn = createBankTransaction({ id: 'b1' });
|
|
968
|
+
const ynabTxn = createYNABTransaction({ id: 'y1', cleared: 'uncleared' });
|
|
969
|
+
const suggestedMatch: TransactionMatch = {
|
|
970
|
+
bank_transaction: createBankTransaction({ id: 'b2' }),
|
|
971
|
+
ynab_transaction: createYNABTransaction({ id: 'y2' }),
|
|
972
|
+
confidence: 'medium',
|
|
973
|
+
confidence_score: 75,
|
|
974
|
+
match_reason: 'Suggested',
|
|
975
|
+
};
|
|
976
|
+
const insights = [
|
|
977
|
+
createInsight('near_match', 'warning'),
|
|
978
|
+
createInsight('repeat_amount', 'info'),
|
|
979
|
+
createInsight('anomaly', 'critical'),
|
|
980
|
+
];
|
|
981
|
+
|
|
982
|
+
const context = createMockContext({
|
|
983
|
+
analysis: {
|
|
984
|
+
...createMockContext().analysis,
|
|
985
|
+
insights,
|
|
986
|
+
unmatched_bank: [bankTxn],
|
|
987
|
+
unmatched_ynab: [ynabTxn],
|
|
988
|
+
suggested_matches: [suggestedMatch],
|
|
989
|
+
},
|
|
990
|
+
});
|
|
991
|
+
|
|
992
|
+
const recommendations = generateRecommendations(context);
|
|
993
|
+
|
|
994
|
+
expect(recommendations.length).toBeGreaterThan(0);
|
|
995
|
+
const actionTypes = new Set(recommendations.map((r) => r.action_type));
|
|
996
|
+
expect(actionTypes.size).toBeGreaterThan(1);
|
|
997
|
+
});
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
});
|