@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,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),
|
|
@@ -275,12 +289,13 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
275
289
|
bulkOperationDetails.transaction_failures += 1; // Canonical counter for per-transaction failures
|
|
276
290
|
}
|
|
277
291
|
const failureReason = ynabError.message || 'Unknown error occurred';
|
|
292
|
+
const statusSuffix = ynabError.status ? ` (HTTP ${ynabError.status})` : '';
|
|
278
293
|
const failureAction: ExecutionActionRecord = {
|
|
279
294
|
type: 'create_transaction_failed',
|
|
280
295
|
transaction: entry.saveTransaction as unknown as Record<string, unknown>,
|
|
281
296
|
reason: options.fallbackError
|
|
282
|
-
? `Bulk fallback failed for ${entry.bankTransaction.payee ?? 'Unknown'} (${failureReason})`
|
|
283
|
-
: `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})`,
|
|
284
299
|
correlation_key: entry.correlationKey,
|
|
285
300
|
};
|
|
286
301
|
if (options.chunkIndex !== undefined) {
|
|
@@ -289,7 +304,7 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
289
304
|
actions_taken.push(failureAction);
|
|
290
305
|
|
|
291
306
|
if (shouldPropagateYnabError(ynabError)) {
|
|
292
|
-
throw attachStatusToError(ynabError);
|
|
307
|
+
throw attachStatusToError(ynabError, error);
|
|
293
308
|
}
|
|
294
309
|
}
|
|
295
310
|
}
|
|
@@ -431,14 +446,16 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
431
446
|
|
|
432
447
|
if (shouldPropagateYnabError(ynabError)) {
|
|
433
448
|
bulkOperationDetails.transaction_failures += chunk.length;
|
|
434
|
-
throw attachStatusToError(ynabError);
|
|
449
|
+
throw attachStatusToError(ynabError, error);
|
|
435
450
|
}
|
|
436
451
|
|
|
437
452
|
bulkOperationDetails.sequential_fallbacks += 1;
|
|
438
453
|
actions_taken.push({
|
|
439
454
|
type: 'bulk_create_fallback',
|
|
440
455
|
transaction: null,
|
|
441
|
-
reason: `Bulk chunk #${chunkIndex} failed (${failureReason}
|
|
456
|
+
reason: `Bulk chunk #${chunkIndex} failed (${failureReason}${
|
|
457
|
+
ynabError.status ? ` (HTTP ${ynabError.status})` : ''
|
|
458
|
+
}) - falling back to sequential creation`,
|
|
442
459
|
bulk_chunk_index: chunkIndex,
|
|
443
460
|
});
|
|
444
461
|
await processSequentialEntries(chunk, { chunkIndex, fallbackError: ynabError });
|
|
@@ -460,17 +477,23 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
460
477
|
if (balanceAligned) break;
|
|
461
478
|
const flags = computeUpdateFlags(match, params);
|
|
462
479
|
if (!flags.needsClearedUpdate && !flags.needsDateUpdate) continue;
|
|
463
|
-
if (!match.
|
|
480
|
+
if (!match.ynabTransaction) continue;
|
|
464
481
|
|
|
465
482
|
// Build minimal update payload - only include ID and fields that are changing
|
|
466
|
-
// 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
|
|
467
485
|
const updatePayload: ynab.SaveTransactionWithIdOrImportId = {
|
|
468
|
-
id: match.
|
|
486
|
+
id: match.ynabTransaction.id,
|
|
469
487
|
};
|
|
470
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
|
+
|
|
471
494
|
// Only include fields that are actually changing
|
|
472
495
|
if (flags.needsDateUpdate) {
|
|
473
|
-
updatePayload.date = match.
|
|
496
|
+
updatePayload.date = match.bankTransaction.date;
|
|
474
497
|
}
|
|
475
498
|
if (flags.needsClearedUpdate) {
|
|
476
499
|
updatePayload.cleared = 'cleared' as ynab.TransactionClearedStatus;
|
|
@@ -482,17 +505,17 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
482
505
|
actions_taken.push({
|
|
483
506
|
type: 'update_transaction',
|
|
484
507
|
transaction: {
|
|
485
|
-
transaction_id: match.
|
|
486
|
-
new_date: flags.needsDateUpdate ? match.
|
|
508
|
+
transaction_id: match.ynabTransaction.id,
|
|
509
|
+
new_date: flags.needsDateUpdate ? match.bankTransaction.date : undefined,
|
|
487
510
|
cleared: flags.needsClearedUpdate ? 'cleared' : undefined,
|
|
488
511
|
},
|
|
489
512
|
reason: `Would update transaction: ${updateReason(match, flags, currencyCode)}`,
|
|
490
513
|
});
|
|
491
514
|
if (flags.needsClearedUpdate) {
|
|
492
|
-
applyClearedDelta(match.
|
|
515
|
+
applyClearedDelta(match.ynabTransaction.amount);
|
|
493
516
|
if (
|
|
494
517
|
recordAlignmentIfNeeded(
|
|
495
|
-
`clearing ${match.
|
|
518
|
+
`clearing ${match.ynabTransaction.id ?? 'transaction'} (dry run)`,
|
|
496
519
|
)
|
|
497
520
|
) {
|
|
498
521
|
break;
|
|
@@ -502,8 +525,8 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
502
525
|
transactionsToUpdate.push(updatePayload);
|
|
503
526
|
if (flags.needsDateUpdate) summary.dates_adjusted += 1;
|
|
504
527
|
if (flags.needsClearedUpdate) {
|
|
505
|
-
applyClearedDelta(match.
|
|
506
|
-
if (recordAlignmentIfNeeded(`clearing ${match.
|
|
528
|
+
applyClearedDelta(match.ynabTransaction.amount);
|
|
529
|
+
if (recordAlignmentIfNeeded(`clearing ${match.ynabTransaction.id}`)) {
|
|
507
530
|
break;
|
|
508
531
|
}
|
|
509
532
|
}
|
|
@@ -511,33 +534,85 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
511
534
|
}
|
|
512
535
|
|
|
513
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
|
|
514
538
|
if (!params.dry_run && transactionsToUpdate.length > 0) {
|
|
515
|
-
const
|
|
516
|
-
transactions: transactionsToUpdate,
|
|
517
|
-
});
|
|
539
|
+
const updateChunks = chunkArray(transactionsToUpdate, MAX_BULK_UPDATE_CHUNK);
|
|
518
540
|
|
|
519
|
-
|
|
520
|
-
|
|
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
|
+
});
|
|
521
547
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
548
|
+
const updatedTransactions = response.data.transactions ?? [];
|
|
549
|
+
summary.transactions_updated += updatedTransactions.length;
|
|
550
|
+
|
|
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
|
+
}
|
|
534
584
|
}
|
|
535
|
-
accountSnapshotDirty = true;
|
|
536
585
|
}
|
|
537
586
|
}
|
|
538
587
|
|
|
539
588
|
// STEP 3: Auto-unclear YNAB transactions missing from bank
|
|
540
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
|
+
|
|
541
616
|
if (shouldRunSanityPass) {
|
|
542
617
|
const transactionsToUnclear: ynab.SaveTransactionWithIdOrImportId[] = [];
|
|
543
618
|
|
|
@@ -570,26 +645,121 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
570
645
|
}
|
|
571
646
|
|
|
572
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
|
|
573
649
|
if (!params.dry_run && transactionsToUnclear.length > 0) {
|
|
574
|
-
const
|
|
575
|
-
|
|
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,
|
|
576
705
|
});
|
|
706
|
+
}
|
|
577
707
|
|
|
578
|
-
|
|
579
|
-
|
|
708
|
+
// Batch update all reconciliations in chunks
|
|
709
|
+
if (transactionsToReconcile.length > 0) {
|
|
710
|
+
const reconcileChunks = chunkArray(transactionsToReconcile, MAX_BULK_UPDATE_CHUNK);
|
|
580
711
|
|
|
581
|
-
for (
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
+
}
|
|
587
752
|
}
|
|
588
|
-
|
|
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
|
+
});
|
|
589
759
|
}
|
|
590
760
|
}
|
|
591
761
|
|
|
592
|
-
// STEP
|
|
762
|
+
// STEP 5: Balance reconciliation snapshot (only once per execution)
|
|
593
763
|
let balance_reconciliation: ExecutionResult['balance_reconciliation'];
|
|
594
764
|
if (params.statement_balance !== undefined && params.statement_date) {
|
|
595
765
|
balance_reconciliation = await buildBalanceReconciliation({
|
|
@@ -602,7 +772,7 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
602
772
|
});
|
|
603
773
|
}
|
|
604
774
|
|
|
605
|
-
// STEP
|
|
775
|
+
// STEP 6: Recommendations and balance changes
|
|
606
776
|
if (!params.dry_run && accountSnapshotDirty) {
|
|
607
777
|
afterAccount = await refreshAccountSnapshot(ynabAPI, budgetId, accountId);
|
|
608
778
|
}
|
|
@@ -641,16 +811,16 @@ export async function executeReconciliation(options: ExecutionOptions): Promise<
|
|
|
641
811
|
return result;
|
|
642
812
|
}
|
|
643
813
|
|
|
644
|
-
interface NormalizedYnabError {
|
|
814
|
+
export interface NormalizedYnabError {
|
|
645
815
|
status?: number;
|
|
646
816
|
name?: string;
|
|
647
817
|
message: string;
|
|
648
818
|
detail?: string;
|
|
649
819
|
}
|
|
650
820
|
|
|
651
|
-
const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500]);
|
|
821
|
+
const FATAL_YNAB_STATUS_CODES = new Set([400, 401, 403, 404, 429, 500, 503]);
|
|
652
822
|
|
|
653
|
-
function normalizeYnabError(error: unknown): NormalizedYnabError {
|
|
823
|
+
export function normalizeYnabError(error: unknown): NormalizedYnabError {
|
|
654
824
|
const parseStatus = (value: unknown): number | undefined => {
|
|
655
825
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
656
826
|
if (typeof value === 'string') {
|
|
@@ -661,7 +831,9 @@ function normalizeYnabError(error: unknown): NormalizedYnabError {
|
|
|
661
831
|
};
|
|
662
832
|
|
|
663
833
|
if (error instanceof Error) {
|
|
664
|
-
const status =
|
|
834
|
+
const status =
|
|
835
|
+
parseStatus((error as { status?: unknown }).status) ??
|
|
836
|
+
parseStatus((error as { response?: { status?: unknown } }).response?.status);
|
|
665
837
|
const detailSource = (error as { detail?: unknown }).detail;
|
|
666
838
|
const detail =
|
|
667
839
|
typeof detailSource === 'string' && detailSource.trim().length > 0 ? detailSource : undefined;
|
|
@@ -714,32 +886,45 @@ function normalizeYnabError(error: unknown): NormalizedYnabError {
|
|
|
714
886
|
return { message: 'Unknown error occurred' };
|
|
715
887
|
}
|
|
716
888
|
|
|
717
|
-
function shouldPropagateYnabError(error: NormalizedYnabError): boolean {
|
|
889
|
+
export function shouldPropagateYnabError(error: NormalizedYnabError): boolean {
|
|
718
890
|
return error.status !== undefined && FATAL_YNAB_STATUS_CODES.has(error.status);
|
|
719
891
|
}
|
|
720
892
|
|
|
721
|
-
function attachStatusToError(error: NormalizedYnabError): Error {
|
|
893
|
+
function attachStatusToError(error: NormalizedYnabError, originalError?: unknown): Error {
|
|
722
894
|
const message = error.message || 'YNAB API error';
|
|
723
|
-
|
|
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}`);
|
|
724
912
|
if (error.status !== undefined) {
|
|
725
913
|
(err as { status?: number }).status = error.status;
|
|
726
914
|
}
|
|
727
915
|
if (error.name) {
|
|
728
916
|
err.name = error.name;
|
|
729
917
|
}
|
|
730
|
-
if (error.detail && !message.includes(error.detail)) {
|
|
731
|
-
err.message = `${message} (${error.detail})`;
|
|
732
|
-
}
|
|
733
918
|
return err;
|
|
734
919
|
}
|
|
735
920
|
|
|
736
921
|
function formatDisplay(amount: number, currency: string): string {
|
|
737
|
-
return
|
|
922
|
+
return toMoneyValue(amount, currency).value_display;
|
|
738
923
|
}
|
|
739
924
|
|
|
740
925
|
function computeUpdateFlags(match: TransactionMatch, params: ReconcileAccountRequest): UpdateFlags {
|
|
741
|
-
const ynabTxn = match.
|
|
742
|
-
const bankTxn = match.
|
|
926
|
+
const ynabTxn = match.ynabTransaction;
|
|
927
|
+
const bankTxn = match.bankTransaction;
|
|
743
928
|
if (!ynabTxn) {
|
|
744
929
|
return { needsClearedUpdate: false, needsDateUpdate: false };
|
|
745
930
|
}
|
|
@@ -756,7 +941,7 @@ function updateReason(match: TransactionMatch, flags: UpdateFlags, _currency: st
|
|
|
756
941
|
parts.push('marked as cleared');
|
|
757
942
|
}
|
|
758
943
|
if (flags.needsDateUpdate) {
|
|
759
|
-
parts.push(`date adjusted to ${match.
|
|
944
|
+
parts.push(`date adjusted to ${match.bankTransaction.date}`);
|
|
760
945
|
}
|
|
761
946
|
return parts.join(', ');
|
|
762
947
|
}
|
|
@@ -964,9 +1149,7 @@ function sortByDateDescending<T extends { date: string }>(items: T[]): T[] {
|
|
|
964
1149
|
}
|
|
965
1150
|
|
|
966
1151
|
function sortMatchesByBankDateDescending(matches: TransactionMatch[]): TransactionMatch[] {
|
|
967
|
-
return [...matches].sort((a, b) =>
|
|
968
|
-
compareDates(b.bank_transaction.date, a.bank_transaction.date),
|
|
969
|
-
);
|
|
1152
|
+
return [...matches].sort((a, b) => compareDates(b.bankTransaction.date, a.bankTransaction.date));
|
|
970
1153
|
}
|
|
971
1154
|
|
|
972
1155
|
function compareDates(dateA: string, dateB: string): number {
|