@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
|
@@ -16,16 +16,9 @@ function parseISODate(dateStr) {
|
|
|
16
16
|
return Number.isNaN(d.getTime()) ? undefined : d;
|
|
17
17
|
}
|
|
18
18
|
function resolveStatementWindow(params, analysisDateRange) {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (start || end) {
|
|
23
|
-
const window = {};
|
|
24
|
-
if (start)
|
|
25
|
-
window.start = start;
|
|
26
|
-
if (end)
|
|
27
|
-
window.end = end;
|
|
28
|
-
return window;
|
|
19
|
+
const end = parseISODate(params.statement_end_date);
|
|
20
|
+
if (end) {
|
|
21
|
+
return { end };
|
|
29
22
|
}
|
|
30
23
|
if (analysisDateRange?.includes(" to ")) {
|
|
31
24
|
const [rawStart, rawEnd] = analysisDateRange
|
|
@@ -637,12 +630,12 @@ export async function executeReconciliation(options) {
|
|
|
637
630
|
}
|
|
638
631
|
}
|
|
639
632
|
let balance_reconciliation;
|
|
640
|
-
if (params.statement_balance !== undefined && params.
|
|
633
|
+
if (params.statement_balance !== undefined && params.statement_end_date) {
|
|
641
634
|
balance_reconciliation = await buildBalanceReconciliation({
|
|
642
635
|
ynabAPI,
|
|
643
636
|
budgetId,
|
|
644
637
|
accountId,
|
|
645
|
-
statementDate: params.
|
|
638
|
+
statementDate: params.statement_end_date,
|
|
646
639
|
statementBalanceMilli: statementTargetMilli,
|
|
647
640
|
analysis,
|
|
648
641
|
});
|
|
@@ -24,27 +24,24 @@ export declare const ReconcileAccountSchema: z.ZodObject<{
|
|
|
24
24
|
delimiter: z.ZodOptional<z.ZodString>;
|
|
25
25
|
}, z.core.$strict>>;
|
|
26
26
|
statement_balance: z.ZodNumber;
|
|
27
|
-
statement_start_date: z.ZodOptional<z.ZodString>;
|
|
28
27
|
statement_end_date: z.ZodOptional<z.ZodString>;
|
|
29
|
-
statement_date: z.ZodOptional<z.ZodString>;
|
|
30
|
-
expected_bank_balance: z.ZodOptional<z.ZodNumber>;
|
|
31
|
-
as_of_timezone: z.ZodOptional<z.ZodString>;
|
|
32
28
|
date_tolerance_days: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
33
|
-
|
|
34
|
-
|
|
29
|
+
match_strictness: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
30
|
+
loose: "loose";
|
|
31
|
+
normal: "normal";
|
|
32
|
+
strict: "strict";
|
|
33
|
+
}>>>;
|
|
35
34
|
auto_create_transactions: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
36
35
|
auto_update_cleared_status: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
37
36
|
auto_unclear_missing: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
38
37
|
auto_adjust_dates: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
39
|
-
invert_bank_amounts: z.ZodOptional<z.ZodBoolean>;
|
|
40
38
|
dry_run: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
sign_convention: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
40
|
+
auto: "auto";
|
|
41
|
+
invert: "invert";
|
|
42
|
+
as_is: "as_is";
|
|
45
43
|
}>>>;
|
|
46
44
|
max_suggestions_in_output: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
47
|
-
force_full_refresh: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
48
45
|
}, z.core.$strip>;
|
|
49
46
|
export type ReconcileAccountRequest = z.infer<typeof ReconcileAccountSchema>;
|
|
50
47
|
export declare function handleReconcileAccount(ynabAPI: ynab.API, deltaFetcher: DeltaFetcher, params: ReconcileAccountRequest, sendProgress?: ProgressCallback): Promise<CallToolResult>;
|
|
@@ -18,27 +18,6 @@ import { normalizeYNABTransactions } from "./ynabAdapter.js";
|
|
|
18
18
|
export { analyzeReconciliation } from "./analyzer.js";
|
|
19
19
|
export { findBestMatch, findMatches } from "./matcher.js";
|
|
20
20
|
export { fuzzyMatch, normalizedMatch, normalizePayee, payeeSimilarity, } from "./payeeNormalizer.js";
|
|
21
|
-
function getAuditDataSource(transactionsResult, forceFullRefresh) {
|
|
22
|
-
if (forceFullRefresh) {
|
|
23
|
-
return "full_api_fetch_no_delta";
|
|
24
|
-
}
|
|
25
|
-
if (transactionsResult.usedDelta) {
|
|
26
|
-
return "delta_fetch_with_merge";
|
|
27
|
-
}
|
|
28
|
-
if (transactionsResult.wasCached) {
|
|
29
|
-
return "delta_fetch_cache_hit";
|
|
30
|
-
}
|
|
31
|
-
return "delta_fetch_full_refresh";
|
|
32
|
-
}
|
|
33
|
-
function getDataFreshness(transactionsResult, forceFullRefresh) {
|
|
34
|
-
if (forceFullRefresh) {
|
|
35
|
-
return "guaranteed_fresh";
|
|
36
|
-
}
|
|
37
|
-
if (transactionsResult.wasCached) {
|
|
38
|
-
return "cache_validated_via_server_knowledge";
|
|
39
|
-
}
|
|
40
|
-
return "fresh_via_delta_fetch";
|
|
41
|
-
}
|
|
42
21
|
export const ReconcileAccountSchema = z
|
|
43
22
|
.object({
|
|
44
23
|
budget_id: z.string().min(1, "Budget ID is required"),
|
|
@@ -61,27 +40,22 @@ export const ReconcileAccountSchema = z
|
|
|
61
40
|
statement_balance: z.number({
|
|
62
41
|
message: "Statement balance is required and must be a number",
|
|
63
42
|
}),
|
|
64
|
-
statement_start_date: z.string().optional(),
|
|
65
43
|
statement_end_date: z.string().optional(),
|
|
66
|
-
statement_date: z.string().optional(),
|
|
67
|
-
expected_bank_balance: z.number().optional(),
|
|
68
|
-
as_of_timezone: z.string().optional(),
|
|
69
44
|
date_tolerance_days: z.number().min(0).max(7).optional().default(7),
|
|
70
|
-
|
|
71
|
-
|
|
45
|
+
match_strictness: z
|
|
46
|
+
.enum(["loose", "normal", "strict"])
|
|
47
|
+
.optional()
|
|
48
|
+
.default("normal"),
|
|
72
49
|
auto_create_transactions: z.boolean().optional().default(false),
|
|
73
50
|
auto_update_cleared_status: z.boolean().optional().default(false),
|
|
74
51
|
auto_unclear_missing: z.boolean().optional().default(true),
|
|
75
52
|
auto_adjust_dates: z.boolean().optional().default(false),
|
|
76
|
-
invert_bank_amounts: z.boolean().optional(),
|
|
77
53
|
dry_run: z.boolean().optional().default(true),
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.enum(["full", "unmatched_only"])
|
|
54
|
+
sign_convention: z
|
|
55
|
+
.enum(["auto", "invert", "as_is"])
|
|
81
56
|
.optional()
|
|
82
|
-
.default("
|
|
57
|
+
.default("auto"),
|
|
83
58
|
max_suggestions_in_output: z.number().int().min(1).optional().default(20),
|
|
84
|
-
force_full_refresh: z.boolean().optional().default(true),
|
|
85
59
|
})
|
|
86
60
|
.refine((data) => data.csv_file_path || data.csv_data, {
|
|
87
61
|
message: "csv_data or csv_file_path is required. " +
|
|
@@ -92,23 +66,27 @@ export const ReconcileAccountSchema = z
|
|
|
92
66
|
});
|
|
93
67
|
export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, maybeParams, sendProgress, errorHandler) {
|
|
94
68
|
const { deltaFetcher, params } = resolveDeltaFetcherArgs(ynabAPI, deltaFetcherOrParams, maybeParams);
|
|
95
|
-
const forceFullRefresh = params.force_full_refresh ?? true;
|
|
96
69
|
return await withToolErrorHandling(async () => {
|
|
70
|
+
const STRICTNESS_THRESHOLDS = {
|
|
71
|
+
loose: { autoMatch: 70, suggested: 55 },
|
|
72
|
+
normal: { autoMatch: 85, suggested: 60 },
|
|
73
|
+
strict: { autoMatch: 93, suggested: 75 },
|
|
74
|
+
};
|
|
75
|
+
const strictness = params.match_strictness ?? "normal";
|
|
76
|
+
const { autoMatch: autoMatchThreshold, suggested: suggestionThreshold } = STRICTNESS_THRESHOLDS[strictness];
|
|
97
77
|
const config = {
|
|
98
78
|
weights: {
|
|
99
79
|
date: 0.15,
|
|
100
80
|
payee: 0.35,
|
|
101
81
|
},
|
|
102
82
|
dateToleranceDays: params.date_tolerance_days ?? 7,
|
|
103
|
-
autoMatchThreshold
|
|
104
|
-
suggestedMatchThreshold:
|
|
83
|
+
autoMatchThreshold,
|
|
84
|
+
suggestedMatchThreshold: suggestionThreshold,
|
|
105
85
|
minimumCandidateScore: 40,
|
|
106
86
|
exactDateBonus: 5,
|
|
107
87
|
exactPayeeBonus: 10,
|
|
108
88
|
};
|
|
109
|
-
const accountResult =
|
|
110
|
-
? await deltaFetcher.fetchAccountsFull(params.budget_id)
|
|
111
|
-
: await deltaFetcher.fetchAccounts(params.budget_id);
|
|
89
|
+
const accountResult = await deltaFetcher.fetchAccountsFull(params.budget_id);
|
|
112
90
|
const accountData = accountResult.data.find((account) => account.id === params.account_id);
|
|
113
91
|
if (!accountData) {
|
|
114
92
|
throw new Error(`Account ${params.account_id} not found in budget ${params.budget_id}`);
|
|
@@ -124,9 +102,7 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
124
102
|
accountType === "medicalDebt" ||
|
|
125
103
|
accountType === "otherDebt" ||
|
|
126
104
|
accountType === "otherLiability";
|
|
127
|
-
const shouldInvertBankAmounts =
|
|
128
|
-
? params.invert_bank_amounts
|
|
129
|
-
: accountIsLiability;
|
|
105
|
+
const shouldInvertBankAmounts = accountIsLiability;
|
|
130
106
|
const adjustedStatementBalance = accountIsLiability
|
|
131
107
|
? -Math.abs(params.statement_balance)
|
|
132
108
|
: params.statement_balance;
|
|
@@ -202,12 +178,8 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
202
178
|
: "Unknown error while parsing CSV";
|
|
203
179
|
throw new Error(`Failed to parse CSV data: ${message}`);
|
|
204
180
|
}
|
|
205
|
-
const
|
|
206
|
-
const statementWindowEnd = normalizeStatementDate(params.statement_end_date ?? params.statement_date);
|
|
181
|
+
const statementWindowEnd = normalizeStatementDate(params.statement_end_date);
|
|
207
182
|
const statementWindowBounds = {
|
|
208
|
-
...(statementWindowStart !== undefined && {
|
|
209
|
-
startDate: statementWindowStart,
|
|
210
|
-
}),
|
|
211
183
|
...(statementWindowEnd !== undefined && {
|
|
212
184
|
endDate: statementWindowEnd,
|
|
213
185
|
}),
|
|
@@ -222,13 +194,15 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
222
194
|
if (clampedCsv.windowApplied && rawCsvResult.transactions.length === 0) {
|
|
223
195
|
throw new Error(`No CSV transactions remain after applying statement window ${formatStatementWindow(clampedCsv.windowApplied)}.`);
|
|
224
196
|
}
|
|
197
|
+
const effectiveStatementEndDate = statementWindowEnd ??
|
|
198
|
+
inferLatestTransactionDate(rawCsvResult.transactions);
|
|
199
|
+
if (statementWindowEnd === undefined &&
|
|
200
|
+
effectiveStatementEndDate !== undefined) {
|
|
201
|
+
narrativeNotes.push(`Auto-detected statement_end_date=${effectiveStatementEndDate} from the latest CSV transaction for balance verification.`);
|
|
202
|
+
}
|
|
225
203
|
let sinceDate;
|
|
226
204
|
let dateWindowSource;
|
|
227
|
-
if (
|
|
228
|
-
sinceDate = new Date(params.statement_start_date);
|
|
229
|
-
dateWindowSource = "statement_start_date";
|
|
230
|
-
}
|
|
231
|
-
else if (rawCsvResult.transactions.length > 0) {
|
|
205
|
+
if (rawCsvResult.transactions.length > 0) {
|
|
232
206
|
sinceDate = inferSinceDateFromTransactions(rawCsvResult.transactions);
|
|
233
207
|
dateWindowSource = "csv_min_date_with_buffer";
|
|
234
208
|
}
|
|
@@ -238,23 +212,31 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
238
212
|
narrativeNotes.push("CSV contained no parsable transactions for date detection; fetched the last 90 days from YNAB.");
|
|
239
213
|
}
|
|
240
214
|
const sinceDateString = sinceDate.toISOString().split("T")[0];
|
|
241
|
-
const transactionsResult =
|
|
242
|
-
? await deltaFetcher.fetchTransactionsByAccountFull(params.budget_id, params.account_id, sinceDateString)
|
|
243
|
-
: await deltaFetcher.fetchTransactionsByAccount(params.budget_id, params.account_id, sinceDateString);
|
|
215
|
+
const transactionsResult = await deltaFetcher.fetchTransactionsByAccountFull(params.budget_id, params.account_id, sinceDateString);
|
|
244
216
|
const ynabTransactions = transactionsResult.data;
|
|
245
217
|
const normalizedYNAB = normalizeYNABTransactions(ynabTransactions);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
218
|
+
const signConvention = params.sign_convention ?? "auto";
|
|
219
|
+
let finalInvertAmounts;
|
|
220
|
+
if (signConvention === "invert") {
|
|
221
|
+
finalInvertAmounts = true;
|
|
222
|
+
narrativeNotes.push("Using explicit sign_convention=invert; bank amounts will be negated.");
|
|
223
|
+
}
|
|
224
|
+
else if (signConvention === "as_is") {
|
|
225
|
+
finalInvertAmounts = false;
|
|
226
|
+
narrativeNotes.push("Using explicit sign_convention=as_is; bank amounts used as-is.");
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
finalInvertAmounts = shouldInvertBankAmounts;
|
|
230
|
+
if (rawCsvResult.transactions.length > 0 && normalizedYNAB.length > 0) {
|
|
231
|
+
const needsInversion = detectSignInversion(rawCsvResult.transactions, normalizedYNAB);
|
|
232
|
+
if (needsInversion !== null) {
|
|
233
|
+
if (needsInversion !== finalInvertAmounts) {
|
|
234
|
+
narrativeNotes.push(needsInversion
|
|
235
|
+
? "Detected bank CSV amounts opposite YNAB; inverting bank amounts for matching."
|
|
236
|
+
: "Detected bank CSV amounts already align with YNAB; using CSV amounts as-is.");
|
|
237
|
+
}
|
|
238
|
+
finalInvertAmounts = needsInversion;
|
|
256
239
|
}
|
|
257
|
-
finalInvertAmounts = needsInversion;
|
|
258
240
|
}
|
|
259
241
|
}
|
|
260
242
|
const parseResult = finalInvertAmounts
|
|
@@ -267,8 +249,8 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
267
249
|
}
|
|
268
250
|
: rawCsvResult;
|
|
269
251
|
const auditMetadata = {
|
|
270
|
-
data_freshness:
|
|
271
|
-
data_source:
|
|
252
|
+
data_freshness: "guaranteed_fresh",
|
|
253
|
+
data_source: "full_api_fetch_no_delta",
|
|
272
254
|
server_knowledge: transactionsResult.serverKnowledge,
|
|
273
255
|
fetched_at: new Date().toISOString(),
|
|
274
256
|
accounts_count: accountResult.data.length,
|
|
@@ -300,8 +282,14 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
300
282
|
uncleared_balance: accountData.uncleared_balance,
|
|
301
283
|
};
|
|
302
284
|
const analysis = analyzeReconciliation(parseResult, params.csv_file_path, ynabTransactions, adjustedStatementBalance, config, currencyCode, params.account_id, params.budget_id, finalInvertAmounts, csvOptions, initialAccount);
|
|
285
|
+
const effectiveParams = effectiveStatementEndDate !== params.statement_end_date
|
|
286
|
+
? {
|
|
287
|
+
...params,
|
|
288
|
+
statement_end_date: effectiveStatementEndDate,
|
|
289
|
+
}
|
|
290
|
+
: params;
|
|
303
291
|
let executionData;
|
|
304
|
-
const wantsBalanceVerification = Boolean(
|
|
292
|
+
const wantsBalanceVerification = Boolean(effectiveParams.statement_end_date);
|
|
305
293
|
const shouldExecute = params.auto_create_transactions ||
|
|
306
294
|
params.auto_update_cleared_status ||
|
|
307
295
|
params.auto_unclear_missing ||
|
|
@@ -311,7 +299,7 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
311
299
|
executionData = await executeReconciliation({
|
|
312
300
|
ynabAPI,
|
|
313
301
|
analysis,
|
|
314
|
-
params,
|
|
302
|
+
params: effectiveParams,
|
|
315
303
|
budgetId: params.budget_id,
|
|
316
304
|
accountId: params.account_id,
|
|
317
305
|
initialAccount,
|
|
@@ -335,22 +323,35 @@ export async function handleReconcileAccount(ynabAPI, deltaFetcherOrParams, mayb
|
|
|
335
323
|
adapterOptions.notes = narrativeNotes;
|
|
336
324
|
}
|
|
337
325
|
const payload = buildReconciliationPayload(analysis, adapterOptions, executionData);
|
|
326
|
+
let executionSummary;
|
|
327
|
+
if (executionData) {
|
|
328
|
+
const balanceRecon = executionData.balance_reconciliation;
|
|
329
|
+
const balanceStatus = !wantsBalanceVerification
|
|
330
|
+
? "not_verified"
|
|
331
|
+
: balanceRecon?.status === "balanced"
|
|
332
|
+
? "balanced"
|
|
333
|
+
: "unbalanced";
|
|
334
|
+
executionSummary = {
|
|
335
|
+
transactions_created: executionData.summary.transactions_created,
|
|
336
|
+
transactions_updated: executionData.summary.transactions_updated,
|
|
337
|
+
dates_adjusted: executionData.summary.dates_adjusted,
|
|
338
|
+
dry_run: executionData.summary.dry_run,
|
|
339
|
+
balance_status: balanceStatus,
|
|
340
|
+
recommendations: executionData.recommendations,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const structured = {
|
|
344
|
+
unmatched_bank: payload.structured.unmatched.bank,
|
|
345
|
+
unmatched_ynab: payload.structured.unmatched.ynab,
|
|
346
|
+
suggestions: payload.structured.matches.suggested,
|
|
347
|
+
...(executionSummary !== undefined && {
|
|
348
|
+
execution_summary: executionSummary,
|
|
349
|
+
}),
|
|
350
|
+
};
|
|
338
351
|
const responseData = {
|
|
339
352
|
human: payload.human,
|
|
353
|
+
structured,
|
|
340
354
|
};
|
|
341
|
-
if (params.include_structured_data) {
|
|
342
|
-
if (params.structured_content === "unmatched_only") {
|
|
343
|
-
const filteredStructured = {
|
|
344
|
-
unmatched_bank: payload.structured.unmatched.bank,
|
|
345
|
-
unmatched_ynab: payload.structured.unmatched.ynab,
|
|
346
|
-
suggestions: payload.structured.matches.suggested,
|
|
347
|
-
};
|
|
348
|
-
responseData["structured"] = filteredStructured;
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
responseData["structured"] = payload.structured;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
355
|
return {
|
|
355
356
|
content: [
|
|
356
357
|
{
|
|
@@ -398,17 +399,15 @@ Args:
|
|
|
398
399
|
- csv_file_path or csv_data (string, required): Bank export file path or inline CSV text.
|
|
399
400
|
- statement_balance (number, required): Ending balance from the bank statement (dollars).
|
|
400
401
|
For credit cards and other liability accounts, pass a negative value (e.g. -6143.27 means you owe $6,143.27).
|
|
402
|
+
- statement_end_date (string, optional): Statement closing date (YYYY-MM-DD). Filters CSV and triggers balance verification. Auto-detected from CSV if omitted.
|
|
403
|
+
- match_strictness (string, optional): Matching sensitivity — "loose" (more matches), "normal" (default), or "strict" (fewer false positives).
|
|
404
|
+
- sign_convention (string, optional): How to treat CSV amount signs — "auto" (default, detects from data), "invert" (negate all amounts), "as_is" (use amounts unchanged). Useful when auto-detection fails for liability accounts.
|
|
401
405
|
- dry_run (boolean, optional): Preview actions without executing. Default: true.
|
|
402
406
|
- auto_create_transactions (boolean, optional): Auto-create missing transactions. Default: false.
|
|
403
407
|
- auto_update_cleared_status (boolean, optional): Auto-mark matched transactions as cleared. Default: false.
|
|
404
|
-
-
|
|
405
|
-
Lower to 70–75 to match more transactions automatically; raise to 90+ for stricter matching.
|
|
406
|
-
- include_structured_data (boolean, optional): Include full JSON output alongside narrative. Default: false.
|
|
407
|
-
- structured_content (string, optional): "full" or "unmatched_only". Default: "full".
|
|
408
|
-
- max_suggestions_in_output (number, optional): Limit unmatched items and suggestions shown in the human report.
|
|
409
|
-
Default: 20.
|
|
408
|
+
- max_suggestions_in_output (number, optional): Limit unmatched items and suggestions shown in the human report. Default: 20.
|
|
410
409
|
|
|
411
|
-
Returns: human-readable reconciliation narrative
|
|
410
|
+
Returns: human-readable reconciliation narrative + structured JSON (unmatched_bank, unmatched_ynab, suggestions, execution_summary when actions are performed).
|
|
412
411
|
|
|
413
412
|
Examples:
|
|
414
413
|
- Preview reconciliation: set dry_run=true (default)
|
|
@@ -443,6 +442,15 @@ function mapCsvDateFormatToHint(format) {
|
|
|
443
442
|
}
|
|
444
443
|
return undefined;
|
|
445
444
|
}
|
|
445
|
+
function inferLatestTransactionDate(transactions) {
|
|
446
|
+
let latestDate;
|
|
447
|
+
for (const transaction of transactions) {
|
|
448
|
+
if (latestDate === undefined || transaction.date > latestDate) {
|
|
449
|
+
latestDate = transaction.date;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return latestDate;
|
|
453
|
+
}
|
|
446
454
|
function mapCsvFormatForPayload(format) {
|
|
447
455
|
if (!format) {
|
|
448
456
|
return undefined;
|