@dizzlkheinz/ynab-mcpb 0.26.2 → 0.26.4
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/CHANGELOG.md +19 -0
- package/CLAUDE.md +1 -1
- package/dist/bundle/index.cjs +91 -93
- package/dist/tools/reconciliation/executor.js +5 -12
- package/dist/tools/reconciliation/index.d.ts +9 -12
- package/dist/tools/reconciliation/index.js +98 -90
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +81 -770
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +38 -66
- package/package.json +1 -1
- package/src/__tests__/performance.test.ts +2 -4
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +2 -4
- package/src/tools/reconciliation/__tests__/executor.test.ts +6 -10
- package/src/tools/reconciliation/__tests__/index.test.ts +10 -6
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +1 -110
- package/src/tools/reconciliation/executor.ts +6 -13
- package/src/tools/reconciliation/index.ts +152 -137
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +146 -312
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +47 -84
|
@@ -257,6 +257,31 @@ export const ExecutionActionRecordSchema = z.discriminatedUnion("type", [
|
|
|
257
257
|
reason: z.string(),
|
|
258
258
|
bulk_chunk_index: z.number(),
|
|
259
259
|
}),
|
|
260
|
+
z.object({
|
|
261
|
+
type: z.literal("batch_update_failed"),
|
|
262
|
+
transaction: z.null(),
|
|
263
|
+
reason: z.string(),
|
|
264
|
+
}),
|
|
265
|
+
z.object({
|
|
266
|
+
type: z.literal("batch_reconcile_failed"),
|
|
267
|
+
transaction: z.null(),
|
|
268
|
+
reason: z.string(),
|
|
269
|
+
}),
|
|
270
|
+
z.object({
|
|
271
|
+
type: z.literal("reconciliation_complete"),
|
|
272
|
+
transaction: z.null(),
|
|
273
|
+
reason: z.string(),
|
|
274
|
+
}),
|
|
275
|
+
z.object({
|
|
276
|
+
type: z.literal("diagnostic_step3_entry"),
|
|
277
|
+
transaction: z.null(),
|
|
278
|
+
reason: z.string(),
|
|
279
|
+
}),
|
|
280
|
+
z.object({
|
|
281
|
+
type: z.literal("diagnostic_unmatched_ynab"),
|
|
282
|
+
transaction: z.record(z.string(), z.unknown()),
|
|
283
|
+
reason: z.string(),
|
|
284
|
+
}),
|
|
260
285
|
]);
|
|
261
286
|
export const ExecutionSummarySchema = z.object({
|
|
262
287
|
bank_transactions_count: z.number(),
|
|
@@ -319,78 +344,25 @@ export const CsvFormatMetadataSchema = z.object({
|
|
|
319
344
|
amount_column: z.string().nullable(),
|
|
320
345
|
payee_column: z.string().nullable(),
|
|
321
346
|
});
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}),
|
|
330
|
-
summary: ReconciliationSummarySchema,
|
|
331
|
-
balance: BalanceInfoSchema.extend({
|
|
332
|
-
discrepancy_direction: z.enum(["balanced", "ynab_higher", "bank_higher"]),
|
|
333
|
-
}),
|
|
334
|
-
insights: z.array(ReconciliationInsightSchema),
|
|
335
|
-
next_steps: z.array(z.string()),
|
|
336
|
-
matches: z.object({
|
|
337
|
-
auto: z.array(TransactionMatchSchema),
|
|
338
|
-
suggested: z.array(TransactionMatchSchema),
|
|
339
|
-
}),
|
|
340
|
-
unmatched: z.object({
|
|
341
|
-
bank: z.array(BankTransactionSchema),
|
|
342
|
-
ynab: z.array(YNABTransactionSimpleSchema),
|
|
343
|
-
ynab_outside_date_range: z.array(YNABTransactionSimpleSchema).optional(),
|
|
344
|
-
}),
|
|
345
|
-
recommendations: z.array(ActionableRecommendationSchema).optional(),
|
|
346
|
-
csv_format: CsvFormatMetadataSchema.optional(),
|
|
347
|
-
execution: ExecutionResultSchema.optional(),
|
|
348
|
-
audit: AuditMetadataSchema.optional(),
|
|
347
|
+
export const ExecutionSummaryOutputSchema = z.object({
|
|
348
|
+
transactions_created: z.number().int(),
|
|
349
|
+
transactions_updated: z.number().int(),
|
|
350
|
+
dates_adjusted: z.number().int(),
|
|
351
|
+
dry_run: z.boolean(),
|
|
352
|
+
balance_status: z.enum(["balanced", "unbalanced", "not_verified"]).optional(),
|
|
353
|
+
recommendations: z.array(z.string()).optional(),
|
|
349
354
|
});
|
|
350
355
|
export const StructuredReconciliationUnmatchedOnlySchema = z
|
|
351
356
|
.object({
|
|
352
357
|
unmatched_bank: z.array(BankTransactionSchema),
|
|
353
358
|
unmatched_ynab: z.array(YNABTransactionSimpleSchema),
|
|
354
359
|
suggestions: z.array(TransactionMatchSchema),
|
|
360
|
+
execution_summary: ExecutionSummaryOutputSchema.optional(),
|
|
355
361
|
})
|
|
356
362
|
.strict();
|
|
357
363
|
export const ReconcileAccountOutputSchema = z
|
|
358
|
-
.
|
|
359
|
-
z
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
StructuredReconciliationDataBaseSchema,
|
|
364
|
-
StructuredReconciliationUnmatchedOnlySchema,
|
|
365
|
-
]),
|
|
366
|
-
})
|
|
367
|
-
.strict(),
|
|
368
|
-
z
|
|
369
|
-
.object({
|
|
370
|
-
human: z.string(),
|
|
371
|
-
})
|
|
372
|
-
.strict(),
|
|
373
|
-
])
|
|
374
|
-
.refine((data) => {
|
|
375
|
-
if ("structured" in data &&
|
|
376
|
-
data.structured &&
|
|
377
|
-
"balance" in data.structured &&
|
|
378
|
-
typeof data.structured.balance === "object" &&
|
|
379
|
-
data.structured.balance !== null) {
|
|
380
|
-
const discrepancyAmount = data.structured.balance.discrepancy.value;
|
|
381
|
-
const direction = data.structured.balance.discrepancy_direction;
|
|
382
|
-
if (Math.abs(discrepancyAmount) < 0.01) {
|
|
383
|
-
return direction === "balanced";
|
|
384
|
-
}
|
|
385
|
-
if (discrepancyAmount > 0) {
|
|
386
|
-
return direction === "ynab_higher";
|
|
387
|
-
}
|
|
388
|
-
if (discrepancyAmount < 0) {
|
|
389
|
-
return direction === "bank_higher";
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
return true;
|
|
393
|
-
}, {
|
|
394
|
-
message: "Discrepancy direction mismatch: direction must match the numeric discrepancy amount",
|
|
395
|
-
path: ["balance", "discrepancy_direction"],
|
|
396
|
-
});
|
|
364
|
+
.object({
|
|
365
|
+
human: z.string(),
|
|
366
|
+
structured: StructuredReconciliationUnmatchedOnlySchema,
|
|
367
|
+
})
|
|
368
|
+
.strict();
|
package/package.json
CHANGED
|
@@ -242,16 +242,14 @@ function buildPerformanceParams(
|
|
|
242
242
|
account_id: "account-performance",
|
|
243
243
|
csv_data: "Date,Description,Amount",
|
|
244
244
|
statement_balance: statementBalance,
|
|
245
|
-
|
|
245
|
+
statement_end_date: "2025-08-31",
|
|
246
246
|
date_tolerance_days: 1,
|
|
247
|
-
|
|
248
|
-
suggestion_threshold: 60,
|
|
247
|
+
match_strictness: "strict" as const,
|
|
249
248
|
auto_create_transactions: true,
|
|
250
249
|
auto_update_cleared_status: false,
|
|
251
250
|
auto_unclear_missing: false,
|
|
252
251
|
auto_adjust_dates: false,
|
|
253
252
|
dry_run: false,
|
|
254
|
-
include_structured_data: false,
|
|
255
253
|
...overrides,
|
|
256
254
|
};
|
|
257
255
|
}
|
|
@@ -376,16 +376,14 @@ function buildIntegrationParams(
|
|
|
376
376
|
account_id: accountId,
|
|
377
377
|
csv_data: "Date,Description,Amount",
|
|
378
378
|
statement_balance: statementBalance,
|
|
379
|
-
|
|
379
|
+
statement_end_date: new Date().toISOString().slice(0, 10),
|
|
380
380
|
date_tolerance_days: 1,
|
|
381
|
-
|
|
382
|
-
suggestion_threshold: 60,
|
|
381
|
+
match_strictness: "strict" as const,
|
|
383
382
|
auto_create_transactions: true,
|
|
384
383
|
auto_update_cleared_status: false,
|
|
385
384
|
auto_unclear_missing: false,
|
|
386
385
|
auto_adjust_dates: false,
|
|
387
386
|
dry_run: false,
|
|
388
|
-
include_structured_data: false,
|
|
389
387
|
...overrides,
|
|
390
388
|
};
|
|
391
389
|
}
|
|
@@ -155,10 +155,9 @@ const buildBulkParams = (
|
|
|
155
155
|
account_id: "account-bulk",
|
|
156
156
|
csv_data: "Date,Payee,Amount",
|
|
157
157
|
statement_balance: statementBalance.value, // Use decimal value for params
|
|
158
|
-
|
|
158
|
+
statement_end_date: "2025-10-31",
|
|
159
159
|
date_tolerance_days: 1,
|
|
160
|
-
|
|
161
|
-
suggestion_threshold: 60,
|
|
160
|
+
match_strictness: "strict" as const,
|
|
162
161
|
auto_create_transactions: true,
|
|
163
162
|
auto_update_cleared_status: false,
|
|
164
163
|
auto_unclear_missing: false,
|
|
@@ -240,8 +239,7 @@ describe("executeReconciliation (dry run)", () => {
|
|
|
240
239
|
csv_data: "Date,Description,Amount",
|
|
241
240
|
statement_balance: -921.24,
|
|
242
241
|
date_tolerance_days: 2,
|
|
243
|
-
|
|
244
|
-
suggestion_threshold: 60,
|
|
242
|
+
match_strictness: "strict" as const,
|
|
245
243
|
auto_create_transactions: true,
|
|
246
244
|
auto_update_cleared_status: true,
|
|
247
245
|
auto_unclear_missing: true,
|
|
@@ -284,10 +282,9 @@ describe("executeReconciliation (apply mode)", () => {
|
|
|
284
282
|
account_id: "account-apply",
|
|
285
283
|
csv_data: "Date,Description,Amount",
|
|
286
284
|
statement_balance: -921.24,
|
|
287
|
-
|
|
285
|
+
statement_end_date: "2025-10-31",
|
|
288
286
|
date_tolerance_days: 2,
|
|
289
|
-
|
|
290
|
-
suggestion_threshold: 60,
|
|
287
|
+
match_strictness: "strict" as const,
|
|
291
288
|
auto_create_transactions: true,
|
|
292
289
|
auto_update_cleared_status: true,
|
|
293
290
|
auto_unclear_missing: true,
|
|
@@ -448,8 +445,7 @@ describe("executeReconciliation (ordered halting)", () => {
|
|
|
448
445
|
csv_data: "Date,Description,Amount",
|
|
449
446
|
statement_balance: 100,
|
|
450
447
|
date_tolerance_days: 2,
|
|
451
|
-
|
|
452
|
-
suggestion_threshold: 60,
|
|
448
|
+
match_strictness: "strict" as const,
|
|
453
449
|
auto_create_transactions: false,
|
|
454
450
|
auto_update_cleared_status: true,
|
|
455
451
|
auto_unclear_missing: false,
|
|
@@ -4,7 +4,7 @@ import { DeltaFetcher } from "../../deltaFetcher.js";
|
|
|
4
4
|
import { handleReconcileAccount, ReconcileAccountSchema } from "../index.js";
|
|
5
5
|
|
|
6
6
|
describe("ReconcileAccountSchema", () => {
|
|
7
|
-
it("parses
|
|
7
|
+
it("parses params and respects max_suggestions_in_output", () => {
|
|
8
8
|
const result = ReconcileAccountSchema.parse({
|
|
9
9
|
budget_id: "budget-1",
|
|
10
10
|
account_id: "account-1",
|
|
@@ -13,7 +13,6 @@ describe("ReconcileAccountSchema", () => {
|
|
|
13
13
|
max_suggestions_in_output: 3,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
expect(result.structured_content).toBe("full");
|
|
17
16
|
expect(result.max_suggestions_in_output).toBe(3);
|
|
18
17
|
});
|
|
19
18
|
|
|
@@ -48,7 +47,7 @@ describe("ReconcileAccountSchema", () => {
|
|
|
48
47
|
});
|
|
49
48
|
|
|
50
49
|
describe("handleReconcileAccount", () => {
|
|
51
|
-
it(
|
|
50
|
+
it("always returns unmatched_only structured content", async () => {
|
|
52
51
|
const ynabAPI = {
|
|
53
52
|
accounts: {
|
|
54
53
|
getAccounts: async () => ({
|
|
@@ -96,10 +95,7 @@ describe("handleReconcileAccount", () => {
|
|
|
96
95
|
account_id: "account-1",
|
|
97
96
|
csv_data: "Date,Description,Amount\n2025-01-01,Coffee,-10.00",
|
|
98
97
|
statement_balance: -10,
|
|
99
|
-
include_structured_data: true,
|
|
100
|
-
structured_content: "unmatched_only",
|
|
101
98
|
auto_unclear_missing: false,
|
|
102
|
-
force_full_refresh: true,
|
|
103
99
|
});
|
|
104
100
|
|
|
105
101
|
const structured = (result.structuredContent as Record<string, unknown>)
|
|
@@ -115,5 +111,13 @@ describe("handleReconcileAccount", () => {
|
|
|
115
111
|
expect(Array.isArray(structured["unmatched_bank"])).toBe(true);
|
|
116
112
|
expect(Array.isArray(structured["unmatched_ynab"])).toBe(true);
|
|
117
113
|
expect(Array.isArray(structured["suggestions"])).toBe(true);
|
|
114
|
+
expect(structured).toHaveProperty("execution_summary");
|
|
115
|
+
expect(
|
|
116
|
+
(
|
|
117
|
+
structured["execution_summary"] as {
|
|
118
|
+
balance_status?: string;
|
|
119
|
+
}
|
|
120
|
+
).balance_status,
|
|
121
|
+
).toBe("unbalanced");
|
|
118
122
|
});
|
|
119
123
|
});
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
1
|
import {
|
|
3
2
|
afterEach,
|
|
4
3
|
beforeAll,
|
|
@@ -34,50 +33,6 @@ describeIntegration("Reconciliation delta isolation", () => {
|
|
|
34
33
|
let deltaFetcher: DeltaFetcher;
|
|
35
34
|
let previousNodeEnv: string | undefined;
|
|
36
35
|
let setupRateLimited = false;
|
|
37
|
-
const parseStructuredPayload = (result: CallToolResult) => {
|
|
38
|
-
if (result.isError) {
|
|
39
|
-
const errorContent = result.content?.find(
|
|
40
|
-
(entry) => entry.type === "text",
|
|
41
|
-
);
|
|
42
|
-
throw new Error(
|
|
43
|
-
errorContent && errorContent.type === "text"
|
|
44
|
-
? errorContent.text
|
|
45
|
-
: "Unexpected reconciliation error response",
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Find the last text entry that contains valid JSON with an "audit" key
|
|
50
|
-
const textEntries =
|
|
51
|
-
result.content?.filter((entry) => entry.type === "text") ?? [];
|
|
52
|
-
for (let i = textEntries.length - 1; i >= 0; i--) {
|
|
53
|
-
const entry = textEntries[i];
|
|
54
|
-
if (entry.type === "text") {
|
|
55
|
-
try {
|
|
56
|
-
const parsed = JSON.parse(entry.text);
|
|
57
|
-
if (parsed && typeof parsed === "object") {
|
|
58
|
-
// Check if audit is at top level (old format)
|
|
59
|
-
if ("audit" in parsed) {
|
|
60
|
-
return parsed;
|
|
61
|
-
}
|
|
62
|
-
// Check if audit is nested under structured (current format: { human, structured: { audit, ... } })
|
|
63
|
-
const structured = (parsed as any).structured;
|
|
64
|
-
if (
|
|
65
|
-
structured &&
|
|
66
|
-
typeof structured === "object" &&
|
|
67
|
-
"audit" in structured
|
|
68
|
-
) {
|
|
69
|
-
return structured;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// Not valid JSON, continue searching
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
throw new Error(
|
|
78
|
-
'Expected structured reconciliation payload with "audit" key to be present',
|
|
79
|
-
);
|
|
80
|
-
};
|
|
81
36
|
|
|
82
37
|
beforeAll(async () => {
|
|
83
38
|
try {
|
|
@@ -161,7 +116,6 @@ describeIntegration("Reconciliation delta isolation", () => {
|
|
|
161
116
|
account_id: testAccountId,
|
|
162
117
|
csv_data: csvData,
|
|
163
118
|
statement_balance: 0,
|
|
164
|
-
include_structured_data: true,
|
|
165
119
|
};
|
|
166
120
|
|
|
167
121
|
const accountsFullSpy = vi.spyOn(deltaFetcher, "fetchAccountsFull");
|
|
@@ -171,11 +125,7 @@ describeIntegration("Reconciliation delta isolation", () => {
|
|
|
171
125
|
);
|
|
172
126
|
const txDeltaSpy = vi.spyOn(deltaFetcher, "fetchTransactionsByAccount");
|
|
173
127
|
|
|
174
|
-
|
|
175
|
-
ynabAPI,
|
|
176
|
-
deltaFetcher,
|
|
177
|
-
params,
|
|
178
|
-
);
|
|
128
|
+
await handleReconcileAccount(ynabAPI, deltaFetcher, params);
|
|
179
129
|
|
|
180
130
|
expect(accountsFullSpy).toHaveBeenCalledWith(testBudgetId);
|
|
181
131
|
expect(txFullSpy).toHaveBeenCalledWith(
|
|
@@ -184,65 +134,6 @@ describeIntegration("Reconciliation delta isolation", () => {
|
|
|
184
134
|
expect.any(String),
|
|
185
135
|
);
|
|
186
136
|
expect(txDeltaSpy).not.toHaveBeenCalled();
|
|
187
|
-
|
|
188
|
-
const structuredPayload = parseStructuredPayload(result);
|
|
189
|
-
expect(structuredPayload.audit).toMatchObject({
|
|
190
|
-
data_freshness: "guaranteed_fresh",
|
|
191
|
-
data_source: "full_api_fetch_no_delta",
|
|
192
|
-
});
|
|
193
|
-
expect(structuredPayload.audit).toHaveProperty("server_knowledge");
|
|
194
|
-
expect(structuredPayload.audit).toHaveProperty("transactions_count");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it("can opt into delta-backed fetches when force_full_refresh is false", {
|
|
199
|
-
meta: { tier: "domain", domain: "delta" },
|
|
200
|
-
}, async (ctx) => {
|
|
201
|
-
await withRateLimitSkip(ctx, async () => {
|
|
202
|
-
const csvData = ["Date,Amount,Description", "2024-01-01,10,Coffee"].join(
|
|
203
|
-
"\n",
|
|
204
|
-
);
|
|
205
|
-
const params = {
|
|
206
|
-
budget_id: testBudgetId,
|
|
207
|
-
account_id: testAccountId,
|
|
208
|
-
csv_data: csvData,
|
|
209
|
-
statement_balance: 0,
|
|
210
|
-
include_structured_data: true,
|
|
211
|
-
force_full_refresh: false,
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const accountsFullSpy = vi.spyOn(deltaFetcher, "fetchAccountsFull");
|
|
215
|
-
const txFullSpy = vi.spyOn(
|
|
216
|
-
deltaFetcher,
|
|
217
|
-
"fetchTransactionsByAccountFull",
|
|
218
|
-
);
|
|
219
|
-
const accountsDeltaSpy = vi.spyOn(deltaFetcher, "fetchAccounts");
|
|
220
|
-
const txDeltaSpy = vi.spyOn(deltaFetcher, "fetchTransactionsByAccount");
|
|
221
|
-
|
|
222
|
-
const result = await handleReconcileAccount(
|
|
223
|
-
ynabAPI,
|
|
224
|
-
deltaFetcher,
|
|
225
|
-
params,
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
expect(accountsFullSpy).not.toHaveBeenCalled();
|
|
229
|
-
expect(txFullSpy).not.toHaveBeenCalled();
|
|
230
|
-
expect(accountsDeltaSpy).toHaveBeenCalledWith(testBudgetId);
|
|
231
|
-
expect(txDeltaSpy).toHaveBeenCalledWith(
|
|
232
|
-
testBudgetId,
|
|
233
|
-
testAccountId,
|
|
234
|
-
expect.any(String),
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
const structuredPayload = parseStructuredPayload(result);
|
|
238
|
-
expect(structuredPayload.audit).toMatchObject({
|
|
239
|
-
data_source: expect.stringMatching(/^delta_fetch_/),
|
|
240
|
-
});
|
|
241
|
-
expect(structuredPayload.audit.cache_status).toMatchObject({
|
|
242
|
-
accounts_cached: expect.any(Boolean),
|
|
243
|
-
transactions_cached: expect.any(Boolean),
|
|
244
|
-
delta_merge_applied: expect.any(Boolean),
|
|
245
|
-
});
|
|
246
137
|
});
|
|
247
138
|
});
|
|
248
139
|
});
|
|
@@ -130,17 +130,10 @@ function resolveStatementWindow(
|
|
|
130
130
|
params: ReconcileAccountRequest,
|
|
131
131
|
analysisDateRange?: string | undefined,
|
|
132
132
|
): StatementWindow | undefined {
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
undefined;
|
|
138
|
-
|
|
139
|
-
if (start || end) {
|
|
140
|
-
const window: StatementWindow = {};
|
|
141
|
-
if (start) window.start = start;
|
|
142
|
-
if (end) window.end = end;
|
|
143
|
-
return window;
|
|
133
|
+
const end = parseISODate(params.statement_end_date);
|
|
134
|
+
|
|
135
|
+
if (end) {
|
|
136
|
+
return { end };
|
|
144
137
|
}
|
|
145
138
|
|
|
146
139
|
if (analysisDateRange?.includes(" to ")) {
|
|
@@ -941,12 +934,12 @@ export async function executeReconciliation(
|
|
|
941
934
|
|
|
942
935
|
// STEP 5: Balance reconciliation snapshot (only once per execution)
|
|
943
936
|
let balance_reconciliation: ExecutionResult["balance_reconciliation"];
|
|
944
|
-
if (params.statement_balance !== undefined && params.
|
|
937
|
+
if (params.statement_balance !== undefined && params.statement_end_date) {
|
|
945
938
|
balance_reconciliation = await buildBalanceReconciliation({
|
|
946
939
|
ynabAPI,
|
|
947
940
|
budgetId,
|
|
948
941
|
accountId,
|
|
949
|
-
statementDate: params.
|
|
942
|
+
statementDate: params.statement_end_date,
|
|
950
943
|
statementBalanceMilli: statementTargetMilli,
|
|
951
944
|
analysis,
|
|
952
945
|
});
|