@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,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import type * as ynab from 'ynab';
|
|
3
3
|
import type { SaveTransaction } from 'ynab/dist/models/SaveTransaction.js';
|
|
4
|
-
import {
|
|
4
|
+
import { YNABAPIError, YNABErrorCode } from '../../server/errorHandler.js';
|
|
5
|
+
import { toMilli, toMoneyValue, addMilli } from '../../utils/money.js';
|
|
5
6
|
import type { ReconciliationAnalysis, TransactionMatch, BankTransaction } from './types.js';
|
|
6
7
|
import type { ReconcileAccountRequest } from './index.js';
|
|
7
8
|
import {
|
|
@@ -87,6 +88,9 @@ const MONEY_EPSILON_MILLI = 100; // $0.10
|
|
|
87
88
|
const DEFAULT_TOLERANCE_CENTS = 1;
|
|
88
89
|
const CENTS_TO_MILLI = 10;
|
|
89
90
|
const MAX_BULK_CREATE_CHUNK = 100;
|
|
91
|
+
const MAX_BULK_UPDATE_CHUNK = 100; // YNAB API supports up to 100 transactions per batch for updates
|
|
92
|
+
const BATCH_DELAY_MS = 200; // Delay between batch chunks to avoid rate limiting
|
|
93
|
+
const MAX_MEMO_LENGTH = 500; // YNAB's maximum memo length
|
|
90
94
|
|
|
91
95
|
function chunkArray<T>(array: T[], size: number): T[][] {
|
|
92
96
|
if (size <= 0) {
|
|
@@ -99,6 +103,16 @@ function chunkArray<T>(array: T[], size: number): T[][] {
|
|
|
99
103
|
return chunks;
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
function sleep(ms: number): Promise<void> {
|
|
107
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function truncateMemo(memo: string | null | undefined): string {
|
|
111
|
+
if (!memo) return 'Auto-reconciled from bank statement';
|
|
112
|
+
if (memo.length <= MAX_MEMO_LENGTH) return memo;
|
|
113
|
+
return memo.substring(0, MAX_MEMO_LENGTH - 3) + '...';
|
|
114
|
+
}
|
|
115
|
+
|
|
102
116
|
interface PreparedBulkCreateEntry {
|
|
103
117
|
bankTransaction: BankTransaction;
|
|
104
118
|
saveTransaction: SaveTransaction;
|
|
@@ -195,13 +209,13 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
195
209
|
// STEP 1: Auto-create missing transactions (bank -> YNAB)
|
|
196
210
|
if (params.auto_create_transactions && !balanceAligned) {
|
|
197
211
|
const buildPreparedEntry = (bankTxn: BankTransaction): PreparedBulkCreateEntry => {
|
|
198
|
-
const amountMilli =
|
|
212
|
+
const amountMilli = bankTxn.amount;
|
|
199
213
|
const saveTransaction: SaveTransaction = {
|
|
200
214
|
account_id: accountId,
|
|
201
215
|
amount: amountMilli,
|
|
202
216
|
date: bankTxn.date,
|
|
203
217
|
payee_name: bankTxn.payee ?? undefined,
|
|
204
|
-
memo: bankTxn.memo
|
|
218
|
+
memo: truncateMemo(bankTxn.memo),
|
|
205
219
|
cleared: 'cleared',
|
|
206
220
|
approved: true,
|
|
207
221
|
import_id: generateBulkImportId(accountId, bankTxn.date, amountMilli, bankTxn.payee),
|
|
@@ -270,22 +284,28 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
270
284
|
: `creating ${entry.bankTransaction.payee ?? 'missing transaction'}`;
|
|
271
285
|
recordAlignmentIfNeeded(trigger);
|
|
272
286
|
} catch (error) {
|
|
287
|
+
const ynabError = normalizeYnabError(error);
|
|
273
288
|
if (bulkOperationDetails) {
|
|
274
289
|
bulkOperationDetails.transaction_failures += 1; // Canonical counter for per-transaction failures
|
|
275
290
|
}
|
|
276
|
-
const failureReason =
|
|
291
|
+
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
292
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
277
293
|
const failureAction: ExecutionActionRecord = {
|
|
278
294
|
type: 'create_transaction_failed',
|
|
279
295
|
transaction: entry.saveTransaction as unknown as Record<string, unknown>,
|
|
280
296
|
reason: options.fallbackError
|
|
281
|
-
? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason})`
|
|
282
|
-
: `Failed to create transaction ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason})`,
|
|
297
|
+
? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`
|
|
298
|
+
: `Failed to create transaction ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`,
|
|
283
299
|
correlation_key: entry.correlationKey,
|
|
284
300
|
};
|
|
285
301
|
if (options.chunkIndex !== undefined) {
|
|
286
302
|
failureAction.bulk_chunk_index = options.chunkIndex;
|
|
287
303
|
}
|
|
288
304
|
actions_taken.push(failureAction);
|
|
305
|
+
|
|
306
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
307
|
+
throw attachStatusToError(ynabError, error);
|
|
308
|
+
}
|
|
289
309
|
}
|
|
290
310
|
}
|
|
291
311
|
// Update sequential_attempts metric if this was a fallback operation
|
|
@@ -420,17 +440,25 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
420
440
|
await processBulkChunk(chunk, chunkIndex);
|
|
421
441
|
bulkOperationDetails.bulk_successes += 1;
|
|
422
442
|
} catch (error) {
|
|
423
|
-
|
|
443
|
+
const ynabError = normalizeYnabError(error);
|
|
444
|
+
const failureReason = ynabError.message || 'unknown error';
|
|
424
445
|
bulkOperationDetails.bulk_chunk_failures += 1; // API-level failure (entire chunk failed)
|
|
446
|
+
|
|
447
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
448
|
+
bulkOperationDetails.transaction_failures += chunk.length;
|
|
449
|
+
throw attachStatusToError(ynabError, error);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
bulkOperationDetails.sequential_fallbacks += 1;
|
|
425
453
|
actions_taken.push({
|
|
426
454
|
type: 'bulk_create_fallback',
|
|
427
455
|
transaction: null,
|
|
428
|
-
reason: `Bulk chunk #${chunkIndex} failed (${
|
|
429
|
-
|
|
456
|
+
reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${
|
|
457
|
+
ynabError.status ? ` (HTTP ${ynabError.status})` : ''
|
|
430
458
|
}) - falling back to sequential creation`,
|
|
431
459
|
bulk_chunk_index: chunkIndex,
|
|
432
460
|
});
|
|
433
|
-
await processSequentialEntries(chunk, { chunkIndex, fallbackError:
|
|
461
|
+
await processSequentialEntries(chunk, { chunkIndex, fallbackError: ynabError });
|
|
434
462
|
}
|
|
435
463
|
}
|
|
436
464
|
}
|
|
@@ -449,17 +477,23 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
449
477
|
if (balanceAligned) break;
|
|
450
478
|
const flags = computeUpdateFlags(match, params);
|
|
451
479
|
if (!flags.needsClearedUpdate && !flags.needsDateUpdate) continue;
|
|
452
|
-
if (!match.
|
|
480
|
+
if (!match.ynabTransaction) continue;
|
|
453
481
|
|
|
454
482
|
// Build minimal update payload - only include ID and fields that are changing
|
|
455
|
-
// Including unnecessary fields (like amount, payee_name
|
|
483
|
+
// Including unnecessary fields (like amount, payee_name) can cause unexpected behavior
|
|
484
|
+
// BUT we must include memo to fix existing memos that exceed YNAB's 500 char limit
|
|
456
485
|
const updatePayload: ynab.SaveTransactionWithIdOrImportId = {
|
|
457
|
-
id: match.
|
|
486
|
+
id: match.ynabTransaction.id,
|
|
458
487
|
};
|
|
459
488
|
|
|
489
|
+
// Truncate memo if it exists and is too long (YNAB validates on update even if not changed)
|
|
490
|
+
if (match.ynabTransaction.memo) {
|
|
491
|
+
updatePayload.memo = truncateMemo(match.ynabTransaction.memo);
|
|
492
|
+
}
|
|
493
|
+
|
|
460
494
|
// Only include fields that are actually changing
|
|
461
495
|
if (flags.needsDateUpdate) {
|
|
462
|
-
updatePayload.date = match.
|
|
496
|
+
updatePayload.date = match.bankTransaction.date;
|
|
463
497
|
}
|
|
464
498
|
if (flags.needsClearedUpdate) {
|
|
465
499
|
updatePayload.cleared = 'cleared' as ynab.TransactionClearedStatus;
|
|
@@ -471,17 +505,17 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
471
505
|
actions_taken.push({
|
|
472
506
|
type: 'update_transaction',
|
|
473
507
|
transaction: {
|
|
474
|
-
transaction_id: match.
|
|
475
|
-
new_date: flags.needsDateUpdate ? match.
|
|
508
|
+
transaction_id: match.ynabTransaction.id,
|
|
509
|
+
new_date: flags.needsDateUpdate ? match.bankTransaction.date : undefined,
|
|
476
510
|
cleared: flags.needsClearedUpdate ? 'cleared' : undefined,
|
|
477
511
|
},
|
|
478
512
|
reason: `Would update transaction: ${updateReason(match, flags, currencyCode)}`,
|
|
479
513
|
});
|
|
480
514
|
if (flags.needsClearedUpdate) {
|
|
481
|
-
applyClearedDelta(match.
|
|
515
|
+
applyClearedDelta(match.ynabTransaction.amount);
|
|
482
516
|
if (
|
|
483
517
|
recordAlignmentIfNeeded(
|
|
484
|
-
`clearing ${match.
|
|
518
|
+
`clearing ${match.ynabTransaction.id ?? 'transaction'} (dry run)`,
|
|
485
519
|
)
|
|
486
520
|
) {
|
|
487
521
|
break;
|
|
@@ -491,8 +525,8 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
491
525
|
transactionsToUpdate.push(updatePayload);
|
|
492
526
|
if (flags.needsDateUpdate) summary.dates_adjusted += 1;
|
|
493
527
|
if (flags.needsClearedUpdate) {
|
|
494
|
-
applyClearedDelta(match.
|
|
495
|
-
if (recordAlignmentIfNeeded(`clearing ${match.
|
|
528
|
+
applyClearedDelta(match.ynabTransaction.amount);
|
|
529
|
+
if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id}`)) {
|
|
496
530
|
break;
|
|
497
531
|
}
|
|
498
532
|
}
|
|
@@ -500,33 +534,85 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
500
534
|
}
|
|
501
535
|
|
|
502
536
|
// Batch update all transactions in a single API call
|
|
537
|
+
// YNAB API has a limit of ~100 transactions per batch, so we chunk the updates
|
|
503
538
|
if (!params.dry_run && transactionsToUpdate.length > 0) {
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
|
|
539
|
+
const updateChunks = chunkArray(transactionsToUpdate, MAX_BULK_UPDATE_CHUNK);
|
|
540
|
+
|
|
541
|
+
for (let chunkIdx = 0; chunkIdx < updateChunks.length; chunkIdx++) {
|
|
542
|
+
const chunk = updateChunks[chunkIdx]!;
|
|
543
|
+
try {
|
|
544
|
+
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
545
|
+
transactions: chunk,
|
|
546
|
+
});
|
|
507
547
|
|
|
508
|
-
|
|
509
|
-
|
|
548
|
+
const updatedTransactions = response.data.transactions ?? [];
|
|
549
|
+
summary.transactions_updated += updatedTransactions.length;
|
|
510
550
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
551
|
+
for (const updatedTransaction of updatedTransactions) {
|
|
552
|
+
const match = orderedAutoMatches.find(
|
|
553
|
+
(m) => m.ynabTransaction?.id === updatedTransaction.id,
|
|
554
|
+
);
|
|
555
|
+
const flags = match
|
|
556
|
+
? computeUpdateFlags(match, params)
|
|
557
|
+
: { needsClearedUpdate: false, needsDateUpdate: false };
|
|
558
|
+
actions_taken.push({
|
|
559
|
+
type: 'update_transaction',
|
|
560
|
+
transaction: updatedTransaction as unknown as Record<string, unknown> | null,
|
|
561
|
+
reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) : 'cleared'}`,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
accountSnapshotDirty = true;
|
|
565
|
+
} catch (error) {
|
|
566
|
+
const ynabError = normalizeYnabError(error);
|
|
567
|
+
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
568
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
569
|
+
actions_taken.push({
|
|
570
|
+
type: 'batch_update_failed',
|
|
571
|
+
transaction: null,
|
|
572
|
+
reason: `Failed to update chunk ${chunkIdx + 1}/${updateChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
576
|
+
throw attachStatusToError(ynabError, error);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Add delay between chunks to avoid rate limiting (except after last chunk)
|
|
581
|
+
if (chunkIdx < updateChunks.length - 1) {
|
|
582
|
+
await sleep(BATCH_DELAY_MS);
|
|
583
|
+
}
|
|
523
584
|
}
|
|
524
|
-
accountSnapshotDirty = true;
|
|
525
585
|
}
|
|
526
586
|
}
|
|
527
587
|
|
|
528
588
|
// STEP 3: Auto-unclear YNAB transactions missing from bank
|
|
529
589
|
const shouldRunSanityPass = params.auto_unclear_missing && !balanceAligned;
|
|
590
|
+
|
|
591
|
+
// Diagnostic logging for auto_unclear_missing debugging
|
|
592
|
+
actions_taken.push({
|
|
593
|
+
type: 'diagnostic_step3_entry',
|
|
594
|
+
transaction: null,
|
|
595
|
+
reason: `STEP 3 diagnostics: auto_unclear_missing=${params.auto_unclear_missing}, balanceAligned=${balanceAligned}, shouldRunSanityPass=${shouldRunSanityPass}, orderedUnmatchedYNAB.length=${orderedUnmatchedYNAB.length}`,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (orderedUnmatchedYNAB.length > 0) {
|
|
599
|
+
const unmatchedDetails = orderedUnmatchedYNAB.slice(0, 10).map((t) => ({
|
|
600
|
+
id: t.id,
|
|
601
|
+
date: t.date,
|
|
602
|
+
cleared: t.cleared,
|
|
603
|
+
amount: formatDisplay(t.amount, currencyCode),
|
|
604
|
+
payee: t.payee ?? 'Unknown',
|
|
605
|
+
}));
|
|
606
|
+
actions_taken.push({
|
|
607
|
+
type: 'diagnostic_unmatched_ynab',
|
|
608
|
+
transaction: { unmatched_transactions: unmatchedDetails } as unknown as Record<
|
|
609
|
+
string,
|
|
610
|
+
unknown
|
|
611
|
+
>,
|
|
612
|
+
reason: `First ${Math.min(10, orderedUnmatchedYNAB.length)} unmatched YNAB transactions (cleared status and amounts)`,
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
|
|
530
616
|
if (shouldRunSanityPass) {
|
|
531
617
|
const transactionsToUnclear: ynab.SaveTransactionWithIdOrImportId[] = [];
|
|
532
618
|
|
|
@@ -559,26 +645,121 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
559
645
|
}
|
|
560
646
|
|
|
561
647
|
// Batch update all unclear operations in a single API call
|
|
648
|
+
// YNAB API has a limit of ~100 transactions per batch, so we chunk the updates
|
|
562
649
|
if (!params.dry_run && transactionsToUnclear.length > 0) {
|
|
563
|
-
const
|
|
564
|
-
|
|
650
|
+
const unclearChunks = chunkArray(transactionsToUnclear, MAX_BULK_UPDATE_CHUNK);
|
|
651
|
+
|
|
652
|
+
for (let chunkIdx = 0; chunkIdx < unclearChunks.length; chunkIdx++) {
|
|
653
|
+
const chunk = unclearChunks[chunkIdx]!;
|
|
654
|
+
try {
|
|
655
|
+
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
656
|
+
transactions: chunk,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const updatedTransactions = response.data.transactions ?? [];
|
|
660
|
+
summary.transactions_updated += updatedTransactions.length;
|
|
661
|
+
|
|
662
|
+
for (const updatedTransaction of updatedTransactions) {
|
|
663
|
+
actions_taken.push({
|
|
664
|
+
type: 'update_transaction',
|
|
665
|
+
transaction: updatedTransaction as unknown as Record<string, unknown> | null,
|
|
666
|
+
reason: `Marked transaction ${updatedTransaction.id} as uncleared - not found on statement`,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
accountSnapshotDirty = true;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
const ynabError = normalizeYnabError(error);
|
|
672
|
+
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
673
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
674
|
+
actions_taken.push({
|
|
675
|
+
type: 'batch_unclear_failed',
|
|
676
|
+
transaction: null,
|
|
677
|
+
reason: `Failed to unclear chunk ${chunkIdx + 1}/${unclearChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
678
|
+
});
|
|
679
|
+
|
|
680
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
681
|
+
throw attachStatusToError(ynabError, error);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Add delay between chunks to avoid rate limiting (except after last chunk)
|
|
686
|
+
if (chunkIdx < unclearChunks.length - 1) {
|
|
687
|
+
await sleep(BATCH_DELAY_MS);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// STEP 4: Mark all matched transactions as reconciled when balance aligns
|
|
694
|
+
if (balanceAligned && !params.dry_run) {
|
|
695
|
+
const transactionsToReconcile: ynab.SaveTransactionWithIdOrImportId[] = [];
|
|
696
|
+
|
|
697
|
+
for (const match of orderedAutoMatches) {
|
|
698
|
+
if (!match.ynabTransaction) continue;
|
|
699
|
+
// Only reconcile transactions that are not already reconciled
|
|
700
|
+
if (match.ynabTransaction.cleared === 'reconciled') continue;
|
|
701
|
+
|
|
702
|
+
transactionsToReconcile.push({
|
|
703
|
+
id: match.ynabTransaction.id,
|
|
704
|
+
cleared: 'reconciled' as ynab.TransactionClearedStatus,
|
|
565
705
|
});
|
|
706
|
+
}
|
|
566
707
|
|
|
567
|
-
|
|
568
|
-
|
|
708
|
+
// Batch update all reconciliations in chunks
|
|
709
|
+
if (transactionsToReconcile.length > 0) {
|
|
710
|
+
const reconcileChunks = chunkArray(transactionsToReconcile, MAX_BULK_UPDATE_CHUNK);
|
|
569
711
|
|
|
570
|
-
for (
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
712
|
+
for (let chunkIdx = 0; chunkIdx < reconcileChunks.length; chunkIdx++) {
|
|
713
|
+
const chunk = reconcileChunks[chunkIdx]!;
|
|
714
|
+
try {
|
|
715
|
+
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
716
|
+
transactions: chunk,
|
|
717
|
+
});
|
|
718
|
+
|
|
719
|
+
const reconciledTransactions = response.data.transactions ?? [];
|
|
720
|
+
summary.transactions_updated += reconciledTransactions.length;
|
|
721
|
+
|
|
722
|
+
for (const reconciledTransaction of reconciledTransactions) {
|
|
723
|
+
const match = orderedAutoMatches.find(
|
|
724
|
+
(m) => m.ynabTransaction?.id === reconciledTransaction.id,
|
|
725
|
+
);
|
|
726
|
+
actions_taken.push({
|
|
727
|
+
type: 'update_transaction',
|
|
728
|
+
transaction: reconciledTransaction as unknown as Record<string, unknown> | null,
|
|
729
|
+
reason: `Marked as reconciled: ${match?.bankTransaction.payee ?? 'transaction'} (${formatDisplay(reconciledTransaction.amount, currencyCode)})`,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
accountSnapshotDirty = true;
|
|
733
|
+
} catch (error) {
|
|
734
|
+
const ynabError = normalizeYnabError(error);
|
|
735
|
+
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
736
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
737
|
+
actions_taken.push({
|
|
738
|
+
type: 'batch_reconcile_failed',
|
|
739
|
+
transaction: null,
|
|
740
|
+
reason: `Failed to reconcile chunk ${chunkIdx + 1}/${reconcileChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
744
|
+
throw attachStatusToError(ynabError, error);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Add delay between chunks to avoid rate limiting (except after last chunk)
|
|
749
|
+
if (chunkIdx < reconcileChunks.length - 1) {
|
|
750
|
+
await sleep(BATCH_DELAY_MS);
|
|
751
|
+
}
|
|
576
752
|
}
|
|
577
|
-
|
|
753
|
+
|
|
754
|
+
actions_taken.push({
|
|
755
|
+
type: 'reconciliation_complete',
|
|
756
|
+
transaction: null,
|
|
757
|
+
reason: `Marked ${transactionsToReconcile.length} matched transaction(s) as reconciled - balance aligned within tolerance`,
|
|
758
|
+
});
|
|
578
759
|
}
|
|
579
760
|
}
|
|
580
761
|
|
|
581
|
-
// STEP
|
|
762
|
+
// STEP 5: Balance reconciliation snapshot (only once per execution)
|
|
582
763
|
let balance_reconciliation: ExecutionResult['balance_reconciliation'];
|
|
583
764
|
if (params.statement_balance !== undefined && params.statement_date) {
|
|
584
765
|
balance_reconciliation = await buildBalanceReconciliation({
|
|
@@ -591,7 +772,7 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
591
772
|
});
|
|
592
773
|
}
|
|
593
774
|
|
|
594
|
-
// STEP
|
|
775
|
+
// STEP 6: Recommendations and balance changes
|
|
595
776
|
if (!params.dry_run && accountSnapshotDirty) {
|
|
596
777
|
afterAccount = await refreshAccountSnapshot(ynabAPI, budgetId, accountId);
|
|
597
778
|
}
|
|
@@ -630,13 +811,120 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
630
811
|
return result;
|
|
631
812
|
}
|
|
632
813
|
|
|
814
|
+
export interface NormalizedYnabError {
|
|
815
|
+
status?: number;
|
|
816
|
+
name?: string;
|
|
817
|
+
message: string;
|
|
818
|
+
detail?: string;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500, 503]);
|
|
822
|
+
|
|
823
|
+
export function normalizeYnabError(error: unknown): NormalizedYnabError {
|
|
824
|
+
const parseStatus = (value: unknown): number | undefined => {
|
|
825
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
826
|
+
if (typeof value === 'string') {
|
|
827
|
+
const numeric = Number(value);
|
|
828
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
829
|
+
}
|
|
830
|
+
return undefined;
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
if (error instanceof Error) {
|
|
834
|
+
const status =
|
|
835
|
+
parseStatus((error as { status?: unknown }).status) ??
|
|
836
|
+
parseStatus((error as { response?: { status?: unknown } }).response?.status);
|
|
837
|
+
const detailSource = (error as { detail?: unknown }).detail;
|
|
838
|
+
const detail =
|
|
839
|
+
typeof detailSource === 'string' && detailSource.trim().length > 0 ? detailSource : undefined;
|
|
840
|
+
|
|
841
|
+
const result: NormalizedYnabError = {
|
|
842
|
+
name: error.name,
|
|
843
|
+
message: error.message || 'Unknown error occurred',
|
|
844
|
+
};
|
|
845
|
+
|
|
846
|
+
if (status !== undefined) result.status = status;
|
|
847
|
+
if (detail !== undefined) result.detail = detail;
|
|
848
|
+
|
|
849
|
+
return result;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (error && typeof error === 'object') {
|
|
853
|
+
const errObj = (error as { error?: unknown }).error ?? error;
|
|
854
|
+
const status = parseStatus(
|
|
855
|
+
(errObj as { id?: unknown }).id ?? (errObj as { status?: unknown }).status,
|
|
856
|
+
);
|
|
857
|
+
const detailCandidate =
|
|
858
|
+
(errObj as { detail?: unknown }).detail ??
|
|
859
|
+
(errObj as { message?: unknown }).message ??
|
|
860
|
+
(errObj as { name?: unknown }).name;
|
|
861
|
+
const detail =
|
|
862
|
+
typeof detailCandidate === 'string' && detailCandidate.trim().length > 0
|
|
863
|
+
? detailCandidate
|
|
864
|
+
: undefined;
|
|
865
|
+
const message =
|
|
866
|
+
detail ??
|
|
867
|
+
(typeof errObj === 'string' && errObj.trim().length > 0 ? errObj : 'Unknown error occurred');
|
|
868
|
+
const name =
|
|
869
|
+
typeof (errObj as { name?: unknown }).name === 'string'
|
|
870
|
+
? ((errObj as { name: string }).name as string)
|
|
871
|
+
: undefined;
|
|
872
|
+
|
|
873
|
+
const result: NormalizedYnabError = { message };
|
|
874
|
+
|
|
875
|
+
if (status !== undefined) result.status = status;
|
|
876
|
+
if (name !== undefined) result.name = name;
|
|
877
|
+
if (detail !== undefined) result.detail = detail;
|
|
878
|
+
|
|
879
|
+
return result;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
if (typeof error === 'string') {
|
|
883
|
+
return { message: error };
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return { message: 'Unknown error occurred' };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
export function shouldPropagateYnabError(error: NormalizedYnabError): boolean {
|
|
890
|
+
return error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function attachStatusToError(error: NormalizedYnabError, originalError?: unknown): Error {
|
|
894
|
+
const message = error.message || 'YNAB API error';
|
|
895
|
+
|
|
896
|
+
const isKnownCode =
|
|
897
|
+
error.status === YNABErrorCode.BAD_REQUEST ||
|
|
898
|
+
error.status === YNABErrorCode.UNAUTHORIZED ||
|
|
899
|
+
error.status === YNABErrorCode.FORBIDDEN ||
|
|
900
|
+
error.status === YNABErrorCode.NOT_FOUND ||
|
|
901
|
+
error.status === YNABErrorCode.TOO_MANY_REQUESTS ||
|
|
902
|
+
error.status === YNABErrorCode.INTERNAL_SERVER_ERROR;
|
|
903
|
+
|
|
904
|
+
if (isKnownCode) {
|
|
905
|
+
return new YNABAPIError(error.status as YNABErrorCode, message, originalError);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
const statusFragment = error.status ? ` (HTTP ${error.status})` : '';
|
|
909
|
+
const detailFragment =
|
|
910
|
+
error.detail && !message.includes(error.detail) ? ` (${error.detail})` : '';
|
|
911
|
+
const err = new Error(`${message}${statusFragment}${detailFragment}`);
|
|
912
|
+
if (error.status !== undefined) {
|
|
913
|
+
(err as { status?: number }).status = error.status;
|
|
914
|
+
}
|
|
915
|
+
if (error.name) {
|
|
916
|
+
err.name = error.name;
|
|
917
|
+
}
|
|
918
|
+
return err;
|
|
919
|
+
}
|
|
920
|
+
|
|
633
921
|
function formatDisplay(amount: number, currency: string): string {
|
|
634
|
-
return
|
|
922
|
+
return toMoneyValue(amount, currency).value_display;
|
|
635
923
|
}
|
|
636
924
|
|
|
637
925
|
function computeUpdateFlags(match: TransactionMatch, params: ReconcileAccountRequest): UpdateFlags {
|
|
638
|
-
const ynabTxn = match.
|
|
639
|
-
const bankTxn = match.
|
|
926
|
+
const ynabTxn = match.ynabTransaction;
|
|
927
|
+
const bankTxn = match.bankTransaction;
|
|
640
928
|
if (!ynabTxn) {
|
|
641
929
|
return { needsClearedUpdate: false, needsDateUpdate: false };
|
|
642
930
|
}
|
|
@@ -653,7 +941,7 @@ function updateReason(match: TransactionMatch, flags: UpdateFlags, _currency: st
|
|
|
653
941
|
parts.push('marked as cleared');
|
|
654
942
|
}
|
|
655
943
|
if (flags.needsDateUpdate) {
|
|
656
|
-
parts.push(`date adjusted to ${match.
|
|
944
|
+
parts.push(`date adjusted to ${match.bankTransaction.date}`);
|
|
657
945
|
}
|
|
658
946
|
return parts.join(', ');
|
|
659
947
|
}
|
|
@@ -861,9 +1149,7 @@ function sortByDateDescending<T extends { date: string }>(items: T[]): T[] {
|
|
|
861
1149
|
}
|
|
862
1150
|
|
|
863
1151
|
function sortMatchesByBankDateDescending(matches: TransactionMatch[]): TransactionMatch[] {
|
|
864
|
-
return [...matches].sort((a, b) =>
|
|
865
|
-
compareDates(b.bank_transaction.date, a.bank_transaction.date),
|
|
866
|
-
);
|
|
1152
|
+
return [...matches].sort((a, b) => compareDates(b.bankTransaction.date, a.bankTransaction.date));
|
|
867
1153
|
}
|
|
868
1154
|
|
|
869
1155
|
function compareDates(dateA: string, dateB: string): number {
|