@dizzlkheinz/ynab-mcpb 0.13.1 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
- package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
- package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
- package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
- package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
- package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
- package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
- package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
- package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
- package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
- package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
- package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
- package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
- package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
- package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
- package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
- package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
- package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
- package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
- package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
- package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
- package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
- package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
- package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
- package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
- package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
- package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
- package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
- package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
- package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
- package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
- package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
- package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
- package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
- package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
- package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
- package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
- package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
- package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
- package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
- package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
- package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
- package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
- package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
- package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
- package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
- package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
- package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
- package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
- package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
- package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
- package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
- package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
- package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
- package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
- package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
- package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
- package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
- package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
- package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
- package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
- package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
- package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
- package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
- package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
- package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
- package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/release.yml +4 -0
- package/CHANGELOG.md +75 -0
- package/NUL +1 -0
- package/dist/bundle/index.cjs +65 -42
- package/dist/server/errorHandler.d.ts +2 -0
- package/dist/server/errorHandler.js +49 -5
- package/dist/tools/reconcileAdapter.js +10 -5
- package/dist/tools/reconciliation/analyzer.d.ts +8 -2
- package/dist/tools/reconciliation/analyzer.js +127 -409
- package/dist/tools/reconciliation/csvParser.d.ts +51 -0
- package/dist/tools/reconciliation/csvParser.js +413 -0
- package/dist/tools/reconciliation/executor.d.ts +8 -0
- package/dist/tools/reconciliation/executor.js +204 -58
- package/dist/tools/reconciliation/index.d.ts +7 -7
- package/dist/tools/reconciliation/index.js +115 -39
- package/dist/tools/reconciliation/matcher.d.ts +24 -3
- package/dist/tools/reconciliation/matcher.js +175 -133
- package/dist/tools/reconciliation/recommendationEngine.js +22 -18
- package/dist/tools/reconciliation/reportFormatter.js +9 -8
- package/dist/tools/reconciliation/signDetector.d.ts +2 -0
- package/dist/tools/reconciliation/signDetector.js +54 -0
- package/dist/tools/reconciliation/types.d.ts +20 -34
- package/dist/tools/reconciliation/types.js +1 -7
- package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
- package/dist/tools/reconciliation/ynabAdapter.js +15 -0
- package/dist/types/reconciliation.d.ts +24 -0
- package/dist/types/reconciliation.js +1 -0
- package/docs/guides/ARCHITECTURE.md +12 -129
- package/docs/plans/2025-11-21-v014-hardening.md +153 -0
- package/docs/plans/reconciliation-v2-redesign.md +1571 -0
- package/package.json +6 -1
- package/scripts/test-recommendations.ts +1 -1
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
- package/src/server/errorHandler.ts +52 -5
- package/src/tools/reconcileAdapter.ts +10 -5
- package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +1 -1
- package/src/tools/reconciliation/__tests__/executor.test.ts +88 -61
- package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
- package/src/tools/reconciliation/analyzer.ts +191 -550
- package/src/tools/reconciliation/csvParser.ts +617 -0
- package/src/tools/reconciliation/executor.ts +249 -66
- package/src/tools/reconciliation/index.ts +148 -54
- package/src/tools/reconciliation/matcher.ts +234 -214
- package/src/tools/reconciliation/recommendationEngine.ts +23 -19
- package/src/tools/reconciliation/reportFormatter.ts +16 -11
- package/src/tools/reconciliation/signDetector.ts +117 -0
- package/src/tools/reconciliation/types.ts +39 -61
- package/src/tools/reconciliation/ynabAdapter.ts +33 -0
- package/src/types/reconciliation.ts +49 -0
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
- package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
- package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
- package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
- package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
- package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
- package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
- package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
- package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
- package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
- package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
- package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
- package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
- package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
- package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
- package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
- package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
- package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
- package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
- package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
- package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
- package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
- package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
- package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
- package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
- package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
- package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
- package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
- package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
- package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
- package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
- package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
- package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
- package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
- package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
- package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
- package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
- package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
- package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
- package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
- package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
- package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
- package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
- package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
- package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
- package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
- package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
- package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
- package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
- package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
- package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
- package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
- package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
- package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
- package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
- package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
- package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
- package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
- package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
- package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
- package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
- package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
- package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
- package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
- package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
- package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
- package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
- package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
- package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
- package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
- package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
- package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
- package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
- package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
- package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
- package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
- package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
- package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
- package/.gemini/settings.json +0 -8
- package/WARP.md +0 -245
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import {
|
|
2
|
+
import { YNABAPIError } from '../../server/errorHandler.js';
|
|
3
|
+
import { toMilli, toMoneyValue, addMilli } from '../../utils/money.js';
|
|
3
4
|
import { generateCorrelationKey, correlateResults, toCorrelationPayload, } from '../transactionTools.js';
|
|
4
5
|
const MONEY_EPSILON_MILLI = 100;
|
|
5
6
|
const DEFAULT_TOLERANCE_CENTS = 1;
|
|
6
7
|
const CENTS_TO_MILLI = 10;
|
|
7
8
|
const MAX_BULK_CREATE_CHUNK = 100;
|
|
9
|
+
const MAX_BULK_UPDATE_CHUNK = 100;
|
|
10
|
+
const BATCH_DELAY_MS = 200;
|
|
11
|
+
const MAX_MEMO_LENGTH = 500;
|
|
8
12
|
function chunkArray(array, size) {
|
|
9
13
|
if (size <= 0) {
|
|
10
14
|
throw new Error('chunk size must be positive');
|
|
@@ -15,6 +19,16 @@ function chunkArray(array, size) {
|
|
|
15
19
|
}
|
|
16
20
|
return chunks;
|
|
17
21
|
}
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
function truncateMemo(memo) {
|
|
26
|
+
if (!memo)
|
|
27
|
+
return 'Auto-reconciled from bank statement';
|
|
28
|
+
if (memo.length <= MAX_MEMO_LENGTH)
|
|
29
|
+
return memo;
|
|
30
|
+
return memo.substring(0, MAX_MEMO_LENGTH - 3) + '...';
|
|
31
|
+
}
|
|
18
32
|
function generateBulkImportId(accountId, date, amountMilli, payee) {
|
|
19
33
|
const normalizedPayee = (payee ?? '').trim().toLowerCase();
|
|
20
34
|
const raw = `${accountId}|${date}|${amountMilli}|${normalizedPayee}`;
|
|
@@ -74,13 +88,13 @@ export async function executeReconciliation(options) {
|
|
|
74
88
|
let bulkOperationDetails;
|
|
75
89
|
if (params.auto_create_transactions && !balanceAligned) {
|
|
76
90
|
const buildPreparedEntry = (bankTxn) => {
|
|
77
|
-
const amountMilli =
|
|
91
|
+
const amountMilli = bankTxn.amount;
|
|
78
92
|
const saveTransaction = {
|
|
79
93
|
account_id: accountId,
|
|
80
94
|
amount: amountMilli,
|
|
81
95
|
date: bankTxn.date,
|
|
82
96
|
payee_name: bankTxn.payee ?? undefined,
|
|
83
|
-
memo: bankTxn.memo
|
|
97
|
+
memo: truncateMemo(bankTxn.memo),
|
|
84
98
|
cleared: 'cleared',
|
|
85
99
|
approved: true,
|
|
86
100
|
import_id: generateBulkImportId(accountId, bankTxn.date, amountMilli, bankTxn.payee),
|
|
@@ -144,12 +158,13 @@ export async function executeReconciliation(options) {
|
|
|
144
158
|
bulkOperationDetails.transaction_failures += 1;
|
|
145
159
|
}
|
|
146
160
|
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
161
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
147
162
|
const failureAction = {
|
|
148
163
|
type: 'create_transaction_failed',
|
|
149
164
|
transaction: entry.saveTransaction,
|
|
150
165
|
reason: options.fallbackError
|
|
151
|
-
? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason})`
|
|
152
|
-
: `Failed to create transaction ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason})`,
|
|
166
|
+
? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`
|
|
167
|
+
: `Failed to create transaction ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason}${statusSuffix})`,
|
|
153
168
|
correlation_key: entry.correlationKey,
|
|
154
169
|
};
|
|
155
170
|
if (options.chunkIndex !== undefined) {
|
|
@@ -157,7 +172,7 @@ export async function executeReconciliation(options) {
|
|
|
157
172
|
}
|
|
158
173
|
actions_taken.push(failureAction);
|
|
159
174
|
if (shouldPropagateYnabError(ynabError)) {
|
|
160
|
-
throw attachStatusToError(ynabError);
|
|
175
|
+
throw attachStatusToError(ynabError, error);
|
|
161
176
|
}
|
|
162
177
|
}
|
|
163
178
|
}
|
|
@@ -289,13 +304,13 @@ export async function executeReconciliation(options) {
|
|
|
289
304
|
bulkOperationDetails.bulk_chunk_failures += 1;
|
|
290
305
|
if (shouldPropagateYnabError(ynabError)) {
|
|
291
306
|
bulkOperationDetails.transaction_failures += chunk.length;
|
|
292
|
-
throw attachStatusToError(ynabError);
|
|
307
|
+
throw attachStatusToError(ynabError, error);
|
|
293
308
|
}
|
|
294
309
|
bulkOperationDetails.sequential_fallbacks += 1;
|
|
295
310
|
actions_taken.push({
|
|
296
311
|
type: 'bulk_create_fallback',
|
|
297
312
|
transaction: null,
|
|
298
|
-
reason: `Bulk chunk #${chunkIndex} failed (${failureReason}) - falling back to sequential creation`,
|
|
313
|
+
reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${ynabError.status ? ` (HTTP ${ynabError.status})` : ''}) - falling back to sequential creation`,
|
|
299
314
|
bulk_chunk_index: chunkIndex,
|
|
300
315
|
});
|
|
301
316
|
await processSequentialEntries(chunk, { chunkIndex, fallbackError: ynabError });
|
|
@@ -316,13 +331,16 @@ export async function executeReconciliation(options) {
|
|
|
316
331
|
const flags = computeUpdateFlags(match, params);
|
|
317
332
|
if (!flags.needsClearedUpdate && !flags.needsDateUpdate)
|
|
318
333
|
continue;
|
|
319
|
-
if (!match.
|
|
334
|
+
if (!match.ynabTransaction)
|
|
320
335
|
continue;
|
|
321
336
|
const updatePayload = {
|
|
322
|
-
id: match.
|
|
337
|
+
id: match.ynabTransaction.id,
|
|
323
338
|
};
|
|
339
|
+
if (match.ynabTransaction.memo) {
|
|
340
|
+
updatePayload.memo = truncateMemo(match.ynabTransaction.memo);
|
|
341
|
+
}
|
|
324
342
|
if (flags.needsDateUpdate) {
|
|
325
|
-
updatePayload.date = match.
|
|
343
|
+
updatePayload.date = match.bankTransaction.date;
|
|
326
344
|
}
|
|
327
345
|
if (flags.needsClearedUpdate) {
|
|
328
346
|
updatePayload.cleared = 'cleared';
|
|
@@ -334,15 +352,15 @@ export async function executeReconciliation(options) {
|
|
|
334
352
|
actions_taken.push({
|
|
335
353
|
type: 'update_transaction',
|
|
336
354
|
transaction: {
|
|
337
|
-
transaction_id: match.
|
|
338
|
-
new_date: flags.needsDateUpdate ? match.
|
|
355
|
+
transaction_id: match.ynabTransaction.id,
|
|
356
|
+
new_date: flags.needsDateUpdate ? match.bankTransaction.date : undefined,
|
|
339
357
|
cleared: flags.needsClearedUpdate ? 'cleared' : undefined,
|
|
340
358
|
},
|
|
341
359
|
reason: `Would update transaction: ${updateReason(match, flags, currencyCode)}`,
|
|
342
360
|
});
|
|
343
361
|
if (flags.needsClearedUpdate) {
|
|
344
|
-
applyClearedDelta(match.
|
|
345
|
-
if (recordAlignmentIfNeeded(`clearing ${match.
|
|
362
|
+
applyClearedDelta(match.ynabTransaction.amount);
|
|
363
|
+
if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id ?? 'transaction'} (dry run)`)) {
|
|
346
364
|
break;
|
|
347
365
|
}
|
|
348
366
|
}
|
|
@@ -352,34 +370,75 @@ export async function executeReconciliation(options) {
|
|
|
352
370
|
if (flags.needsDateUpdate)
|
|
353
371
|
summary.dates_adjusted += 1;
|
|
354
372
|
if (flags.needsClearedUpdate) {
|
|
355
|
-
applyClearedDelta(match.
|
|
356
|
-
if (recordAlignmentIfNeeded(`clearing ${match.
|
|
373
|
+
applyClearedDelta(match.ynabTransaction.amount);
|
|
374
|
+
if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id}`)) {
|
|
357
375
|
break;
|
|
358
376
|
}
|
|
359
377
|
}
|
|
360
378
|
}
|
|
361
379
|
}
|
|
362
380
|
if (!params.dry_run && transactionsToUpdate.length > 0) {
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
381
|
+
const updateChunks = chunkArray(transactionsToUpdate, MAX_BULK_UPDATE_CHUNK);
|
|
382
|
+
for (let chunkIdx = 0; chunkIdx < updateChunks.length; chunkIdx++) {
|
|
383
|
+
const chunk = updateChunks[chunkIdx];
|
|
384
|
+
try {
|
|
385
|
+
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
386
|
+
transactions: chunk,
|
|
387
|
+
});
|
|
388
|
+
const updatedTransactions = response.data.transactions ?? [];
|
|
389
|
+
summary.transactions_updated += updatedTransactions.length;
|
|
390
|
+
for (const updatedTransaction of updatedTransactions) {
|
|
391
|
+
const match = orderedAutoMatches.find((m) => m.ynabTransaction?.id === updatedTransaction.id);
|
|
392
|
+
const flags = match
|
|
393
|
+
? computeUpdateFlags(match, params)
|
|
394
|
+
: { needsClearedUpdate: false, needsDateUpdate: false };
|
|
395
|
+
actions_taken.push({
|
|
396
|
+
type: 'update_transaction',
|
|
397
|
+
transaction: updatedTransaction,
|
|
398
|
+
reason: `Updated transaction: ${match ? updateReason(match, flags, currencyCode) : 'cleared'}`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
accountSnapshotDirty = true;
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
const ynabError = normalizeYnabError(error);
|
|
405
|
+
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
406
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
407
|
+
actions_taken.push({
|
|
408
|
+
type: 'batch_update_failed',
|
|
409
|
+
transaction: null,
|
|
410
|
+
reason: `Failed to update chunk ${chunkIdx + 1}/${updateChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
411
|
+
});
|
|
412
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
413
|
+
throw attachStatusToError(ynabError, error);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (chunkIdx < updateChunks.length - 1) {
|
|
417
|
+
await sleep(BATCH_DELAY_MS);
|
|
418
|
+
}
|
|
378
419
|
}
|
|
379
|
-
accountSnapshotDirty = true;
|
|
380
420
|
}
|
|
381
421
|
}
|
|
382
422
|
const shouldRunSanityPass = params.auto_unclear_missing && !balanceAligned;
|
|
423
|
+
actions_taken.push({
|
|
424
|
+
type: 'diagnostic_step3_entry',
|
|
425
|
+
transaction: null,
|
|
426
|
+
reason: `STEP 3 diagnostics: auto_unclear_missing=${params.auto_unclear_missing}, balanceAligned=${balanceAligned}, shouldRunSanityPass=${shouldRunSanityPass}, orderedUnmatchedYNAB.length=${orderedUnmatchedYNAB.length}`,
|
|
427
|
+
});
|
|
428
|
+
if (orderedUnmatchedYNAB.length > 0) {
|
|
429
|
+
const unmatchedDetails = orderedUnmatchedYNAB.slice(0, 10).map((t) => ({
|
|
430
|
+
id: t.id,
|
|
431
|
+
date: t.date,
|
|
432
|
+
cleared: t.cleared,
|
|
433
|
+
amount: formatDisplay(t.amount, currencyCode),
|
|
434
|
+
payee: t.payee ?? 'Unknown',
|
|
435
|
+
}));
|
|
436
|
+
actions_taken.push({
|
|
437
|
+
type: 'diagnostic_unmatched_ynab',
|
|
438
|
+
transaction: { unmatched_transactions: unmatchedDetails },
|
|
439
|
+
reason: `First ${Math.min(10, orderedUnmatchedYNAB.length)} unmatched YNAB transactions (cleared status and amounts)`,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
383
442
|
if (shouldRunSanityPass) {
|
|
384
443
|
const transactionsToUnclear = [];
|
|
385
444
|
for (const ynabTxn of orderedUnmatchedYNAB) {
|
|
@@ -411,19 +470,97 @@ export async function executeReconciliation(options) {
|
|
|
411
470
|
}
|
|
412
471
|
}
|
|
413
472
|
if (!params.dry_run && transactionsToUnclear.length > 0) {
|
|
414
|
-
const
|
|
415
|
-
|
|
473
|
+
const unclearChunks = chunkArray(transactionsToUnclear, MAX_BULK_UPDATE_CHUNK);
|
|
474
|
+
for (let chunkIdx = 0; chunkIdx < unclearChunks.length; chunkIdx++) {
|
|
475
|
+
const chunk = unclearChunks[chunkIdx];
|
|
476
|
+
try {
|
|
477
|
+
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
478
|
+
transactions: chunk,
|
|
479
|
+
});
|
|
480
|
+
const updatedTransactions = response.data.transactions ?? [];
|
|
481
|
+
summary.transactions_updated += updatedTransactions.length;
|
|
482
|
+
for (const updatedTransaction of updatedTransactions) {
|
|
483
|
+
actions_taken.push({
|
|
484
|
+
type: 'update_transaction',
|
|
485
|
+
transaction: updatedTransaction,
|
|
486
|
+
reason: `Marked transaction ${updatedTransaction.id} as uncleared - not found on statement`,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
accountSnapshotDirty = true;
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
const ynabError = normalizeYnabError(error);
|
|
493
|
+
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
494
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
495
|
+
actions_taken.push({
|
|
496
|
+
type: 'batch_unclear_failed',
|
|
497
|
+
transaction: null,
|
|
498
|
+
reason: `Failed to unclear chunk ${chunkIdx + 1}/${unclearChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
499
|
+
});
|
|
500
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
501
|
+
throw attachStatusToError(ynabError, error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (chunkIdx < unclearChunks.length - 1) {
|
|
505
|
+
await sleep(BATCH_DELAY_MS);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
if (balanceAligned && !params.dry_run) {
|
|
511
|
+
const transactionsToReconcile = [];
|
|
512
|
+
for (const match of orderedAutoMatches) {
|
|
513
|
+
if (!match.ynabTransaction)
|
|
514
|
+
continue;
|
|
515
|
+
if (match.ynabTransaction.cleared === 'reconciled')
|
|
516
|
+
continue;
|
|
517
|
+
transactionsToReconcile.push({
|
|
518
|
+
id: match.ynabTransaction.id,
|
|
519
|
+
cleared: 'reconciled',
|
|
416
520
|
});
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
521
|
+
}
|
|
522
|
+
if (transactionsToReconcile.length > 0) {
|
|
523
|
+
const reconcileChunks = chunkArray(transactionsToReconcile, MAX_BULK_UPDATE_CHUNK);
|
|
524
|
+
for (let chunkIdx = 0; chunkIdx < reconcileChunks.length; chunkIdx++) {
|
|
525
|
+
const chunk = reconcileChunks[chunkIdx];
|
|
526
|
+
try {
|
|
527
|
+
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
528
|
+
transactions: chunk,
|
|
529
|
+
});
|
|
530
|
+
const reconciledTransactions = response.data.transactions ?? [];
|
|
531
|
+
summary.transactions_updated += reconciledTransactions.length;
|
|
532
|
+
for (const reconciledTransaction of reconciledTransactions) {
|
|
533
|
+
const match = orderedAutoMatches.find((m) => m.ynabTransaction?.id === reconciledTransaction.id);
|
|
534
|
+
actions_taken.push({
|
|
535
|
+
type: 'update_transaction',
|
|
536
|
+
transaction: reconciledTransaction,
|
|
537
|
+
reason: `Marked as reconciled: ${match?.bankTransaction.payee ?? 'transaction'} (${formatDisplay(reconciledTransaction.amount, currencyCode)})`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
accountSnapshotDirty = true;
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
const ynabError = normalizeYnabError(error);
|
|
544
|
+
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
545
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
546
|
+
actions_taken.push({
|
|
547
|
+
type: 'batch_reconcile_failed',
|
|
548
|
+
transaction: null,
|
|
549
|
+
reason: `Failed to reconcile chunk ${chunkIdx + 1}/${reconcileChunks.length} (${chunk.length} transaction(s)): ${failureReason}${statusSuffix}`,
|
|
550
|
+
});
|
|
551
|
+
if (shouldPropagateYnabError(ynabError)) {
|
|
552
|
+
throw attachStatusToError(ynabError, error);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (chunkIdx < reconcileChunks.length - 1) {
|
|
556
|
+
await sleep(BATCH_DELAY_MS);
|
|
557
|
+
}
|
|
425
558
|
}
|
|
426
|
-
|
|
559
|
+
actions_taken.push({
|
|
560
|
+
type: 'reconciliation_complete',
|
|
561
|
+
transaction: null,
|
|
562
|
+
reason: `Marked ${transactionsToReconcile.length} matched transaction(s) as reconciled - balance aligned within tolerance`,
|
|
563
|
+
});
|
|
427
564
|
}
|
|
428
565
|
}
|
|
429
566
|
let balance_reconciliation;
|
|
@@ -466,8 +603,8 @@ export async function executeReconciliation(options) {
|
|
|
466
603
|
}
|
|
467
604
|
return result;
|
|
468
605
|
}
|
|
469
|
-
const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500]);
|
|
470
|
-
function normalizeYnabError(error) {
|
|
606
|
+
const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500, 503]);
|
|
607
|
+
export function normalizeYnabError(error) {
|
|
471
608
|
const parseStatus = (value) => {
|
|
472
609
|
if (typeof value === 'number' && Number.isFinite(value))
|
|
473
610
|
return value;
|
|
@@ -479,7 +616,8 @@ function normalizeYnabError(error) {
|
|
|
479
616
|
return undefined;
|
|
480
617
|
};
|
|
481
618
|
if (error instanceof Error) {
|
|
482
|
-
const status = parseStatus(error.status)
|
|
619
|
+
const status = parseStatus(error.status) ??
|
|
620
|
+
parseStatus(error.response?.status);
|
|
483
621
|
const detailSource = error.detail;
|
|
484
622
|
const detail = typeof detailSource === 'string' && detailSource.trim().length > 0 ? detailSource : undefined;
|
|
485
623
|
const result = {
|
|
@@ -520,29 +658,37 @@ function normalizeYnabError(error) {
|
|
|
520
658
|
}
|
|
521
659
|
return { message: 'Unknown error occurred' };
|
|
522
660
|
}
|
|
523
|
-
function shouldPropagateYnabError(error) {
|
|
661
|
+
export function shouldPropagateYnabError(error) {
|
|
524
662
|
return error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status);
|
|
525
663
|
}
|
|
526
|
-
function attachStatusToError(error) {
|
|
664
|
+
function attachStatusToError(error, originalError) {
|
|
527
665
|
const message = error.message || 'YNAB API error';
|
|
528
|
-
const
|
|
666
|
+
const isKnownCode = error.status === 400 ||
|
|
667
|
+
error.status === 401 ||
|
|
668
|
+
error.status === 403 ||
|
|
669
|
+
error.status === 404 ||
|
|
670
|
+
error.status === 429 ||
|
|
671
|
+
error.status === 500;
|
|
672
|
+
if (isKnownCode) {
|
|
673
|
+
return new YNABAPIError(error.status, message, originalError);
|
|
674
|
+
}
|
|
675
|
+
const statusFragment = error.status ? ` (HTTP ${error.status})` : '';
|
|
676
|
+
const detailFragment = error.detail && !message.includes(error.detail) ? ` (${error.detail})` : '';
|
|
677
|
+
const err = new Error(`${message}${statusFragment}${detailFragment}`);
|
|
529
678
|
if (error.status !== undefined) {
|
|
530
679
|
err.status = error.status;
|
|
531
680
|
}
|
|
532
681
|
if (error.name) {
|
|
533
682
|
err.name = error.name;
|
|
534
683
|
}
|
|
535
|
-
if (error.detail && !message.includes(error.detail)) {
|
|
536
|
-
err.message = `${message} (${error.detail})`;
|
|
537
|
-
}
|
|
538
684
|
return err;
|
|
539
685
|
}
|
|
540
686
|
function formatDisplay(amount, currency) {
|
|
541
|
-
return
|
|
687
|
+
return toMoneyValue(amount, currency).value_display;
|
|
542
688
|
}
|
|
543
689
|
function computeUpdateFlags(match, params) {
|
|
544
|
-
const ynabTxn = match.
|
|
545
|
-
const bankTxn = match.
|
|
690
|
+
const ynabTxn = match.ynabTransaction;
|
|
691
|
+
const bankTxn = match.bankTransaction;
|
|
546
692
|
if (!ynabTxn) {
|
|
547
693
|
return { needsClearedUpdate: false, needsDateUpdate: false };
|
|
548
694
|
}
|
|
@@ -556,7 +702,7 @@ function updateReason(match, flags, _currency) {
|
|
|
556
702
|
parts.push('marked as cleared');
|
|
557
703
|
}
|
|
558
704
|
if (flags.needsDateUpdate) {
|
|
559
|
-
parts.push(`date adjusted to ${match.
|
|
705
|
+
parts.push(`date adjusted to ${match.bankTransaction.date}`);
|
|
560
706
|
}
|
|
561
707
|
return parts.join(', ');
|
|
562
708
|
}
|
|
@@ -677,7 +823,7 @@ function sortByDateDescending(items) {
|
|
|
677
823
|
return [...items].sort((a, b) => compareDates(b.date, a.date));
|
|
678
824
|
}
|
|
679
825
|
function sortMatchesByBankDateDescending(matches) {
|
|
680
|
-
return [...matches].sort((a, b) => compareDates(b.
|
|
826
|
+
return [...matches].sort((a, b) => compareDates(b.bankTransaction.date, a.bankTransaction.date));
|
|
681
827
|
}
|
|
682
828
|
function compareDates(dateA, dateB) {
|
|
683
829
|
return toChronoValue(dateA) - toChronoValue(dateB);
|
|
@@ -11,16 +11,16 @@ export declare const ReconcileAccountSchema: z.ZodObject<{
|
|
|
11
11
|
account_id: z.ZodString;
|
|
12
12
|
csv_file_path: z.ZodOptional<z.ZodString>;
|
|
13
13
|
csv_data: z.ZodOptional<z.ZodString>;
|
|
14
|
-
csv_format: z.
|
|
15
|
-
date_column: z.
|
|
14
|
+
csv_format: z.ZodOptional<z.ZodObject<{
|
|
15
|
+
date_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
16
16
|
amount_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
17
17
|
debit_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
18
18
|
credit_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
19
|
-
description_column: z.
|
|
20
|
-
date_format: z.
|
|
21
|
-
has_header: z.
|
|
22
|
-
delimiter: z.
|
|
23
|
-
}, z.core.$strict
|
|
19
|
+
description_column: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
|
|
20
|
+
date_format: z.ZodOptional<z.ZodString>;
|
|
21
|
+
has_header: z.ZodOptional<z.ZodBoolean>;
|
|
22
|
+
delimiter: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strict>>;
|
|
24
24
|
statement_balance: z.ZodNumber;
|
|
25
25
|
statement_start_date: z.ZodOptional<z.ZodString>;
|
|
26
26
|
statement_end_date: z.ZodOptional<z.ZodString>;
|