@dizzlkheinz/ynab-mcpb 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/bundle/index.cjs +50 -49
- package/dist/server/YNABMCPServer.d.ts +2 -6
- package/dist/server/YNABMCPServer.js +5 -1
- package/dist/server/resources.d.ts +17 -13
- package/dist/server/resources.js +237 -48
- package/dist/tools/reconcileAdapter.d.ts +1 -0
- package/dist/tools/reconcileAdapter.js +1 -0
- package/dist/tools/reconciliation/analyzer.d.ts +5 -1
- package/dist/tools/reconciliation/analyzer.js +10 -8
- package/dist/tools/reconciliation/csvParser.d.ts +3 -0
- package/dist/tools/reconciliation/csvParser.js +58 -19
- package/dist/tools/reconciliation/executor.js +47 -1
- package/dist/tools/reconciliation/index.js +82 -42
- package/dist/tools/reconciliation/reportFormatter.d.ts +1 -0
- package/dist/tools/reconciliation/reportFormatter.js +49 -36
- package/docs/reference/API.md +144 -0
- package/docs/technical/reconciliation-system-architecture.md +2251 -0
- package/package.json +1 -1
- package/src/server/YNABMCPServer.ts +7 -0
- package/src/server/__tests__/resources.template.test.ts +198 -0
- package/src/server/__tests__/resources.test.ts +10 -2
- package/src/server/resources.ts +307 -62
- package/src/tools/reconcileAdapter.ts +2 -0
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +23 -23
- package/src/tools/reconciliation/analyzer.ts +18 -6
- package/src/tools/reconciliation/csvParser.ts +84 -18
- package/src/tools/reconciliation/executor.ts +58 -1
- package/src/tools/reconciliation/index.ts +112 -61
- package/src/tools/reconciliation/reportFormatter.ts +55 -37
|
@@ -0,0 +1,2251 @@
|
|
|
1
|
+
# YNAB Reconciliation System - Technical Architecture Documentation
|
|
2
|
+
|
|
3
|
+
**Document Version:** 2.0
|
|
4
|
+
**Last Updated:** 2025-12-01
|
|
5
|
+
**Code Version Compatibility:** v0.16.0+
|
|
6
|
+
**Last Verified Against Code:** v0.16.0 (2025-12-01)
|
|
7
|
+
**Status:** Active Implementation
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Executive Summary
|
|
12
|
+
|
|
13
|
+
The YNAB Reconciliation System is a sophisticated transaction matching and balance verification engine that automatically reconciles bank statement CSV files against YNAB transactions. The system achieves 90%+ auto-match accuracy through intelligent fuzzy matching, progressive confidence scoring, and robust CSV parsing with multi-bank format support.
|
|
14
|
+
|
|
15
|
+
**Key Capabilities:**
|
|
16
|
+
- Automated transaction matching with 85%+ confidence threshold
|
|
17
|
+
- Multi-bank CSV format detection (TD, RBC, Scotiabank, Wealthsimple, Tangerine)
|
|
18
|
+
- Integer-based milliunit arithmetic (eliminates floating-point errors)
|
|
19
|
+
- Bulk transaction operations with correlation tracking
|
|
20
|
+
- Smart sign detection for liability accounts
|
|
21
|
+
- Actionable recommendations with prioritization
|
|
22
|
+
|
|
23
|
+
**Target Accuracy:** 90%+ auto-match rate for Canadian bank statements
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Table of Contents
|
|
28
|
+
|
|
29
|
+
1. [System Architecture](#1-system-architecture)
|
|
30
|
+
2. [Core Components](#2-core-components)
|
|
31
|
+
3. [Data Flow](#3-data-flow)
|
|
32
|
+
4. [Transaction Matching Algorithm](#4-transaction-matching-algorithm)
|
|
33
|
+
5. [CSV Parsing Engine](#5-csv-parsing-engine)
|
|
34
|
+
6. [Execution Engine](#6-execution-engine)
|
|
35
|
+
7. [Type System](#7-type-system)
|
|
36
|
+
8. [Integration Points](#8-integration-points)
|
|
37
|
+
9. [Performance Characteristics](#9-performance-characteristics)
|
|
38
|
+
10. [Testing Strategy](#10-testing-strategy)
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 1. System Architecture
|
|
43
|
+
|
|
44
|
+
### 1.1 High-Level Architecture
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
graph TB
|
|
48
|
+
A[MCP Tool Entry Point] --> B[index.ts Handler]
|
|
49
|
+
B --> C{Parse CSV}
|
|
50
|
+
C --> D[csvParser.ts]
|
|
51
|
+
D --> E[BankTransaction[]]
|
|
52
|
+
|
|
53
|
+
B --> F{Fetch YNAB Data}
|
|
54
|
+
F --> G[DeltaFetcher]
|
|
55
|
+
G --> H[ynabAdapter.ts]
|
|
56
|
+
H --> I[NormalizedYNABTransaction[]]
|
|
57
|
+
|
|
58
|
+
E --> J[analyzer.ts]
|
|
59
|
+
I --> J
|
|
60
|
+
|
|
61
|
+
J --> K[matcher.ts]
|
|
62
|
+
K --> L[Match Results]
|
|
63
|
+
|
|
64
|
+
J --> M[recommendationEngine.ts]
|
|
65
|
+
M --> N[Actionable Recommendations]
|
|
66
|
+
|
|
67
|
+
L --> O{Execute Actions?}
|
|
68
|
+
N --> O
|
|
69
|
+
|
|
70
|
+
O -->|Yes| P[executor.ts]
|
|
71
|
+
P --> Q[Bulk Operations]
|
|
72
|
+
Q --> R[YNAB API Updates]
|
|
73
|
+
|
|
74
|
+
O -->|No| S[reportFormatter.ts]
|
|
75
|
+
L --> S
|
|
76
|
+
N --> S
|
|
77
|
+
S --> T[Human-Readable Report]
|
|
78
|
+
|
|
79
|
+
R --> S
|
|
80
|
+
|
|
81
|
+
T --> U[MCP Response]
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 1.2 Component Responsibility Matrix
|
|
85
|
+
|
|
86
|
+
| Component | Primary Role | Inputs | Outputs | Dependencies |
|
|
87
|
+
|-----------|-------------|--------|---------|--------------|
|
|
88
|
+
| **index.ts** | Orchestration | MCP Tool Request | CallToolResult | All components |
|
|
89
|
+
| **csvParser.ts** | CSV Parsing | Raw CSV string | BankTransaction[] | PapaParse, chrono-node |
|
|
90
|
+
| **ynabAdapter.ts** | YNAB Normalization | TransactionDetail[] | NormalizedYNABTransaction[] | YNAB SDK |
|
|
91
|
+
| **matcher.ts** | Matching Algorithm | Bank + YNAB Transactions | MatchResult[] | fuzzball |
|
|
92
|
+
| **analyzer.ts** | Analysis Orchestration | Parsed Data | ReconciliationAnalysis | matcher, recommendationEngine |
|
|
93
|
+
| **executor.ts** | Action Execution | Analysis + Params | ExecutionResult | YNAB API, transactionTools |
|
|
94
|
+
| **recommendationEngine.ts** | Recommendation Generation | Analysis | ActionableRecommendation[] | None (pure logic) |
|
|
95
|
+
| **reportFormatter.ts** | Human-Readable Output | Analysis + Execution | Formatted Report | None (pure formatting) |
|
|
96
|
+
|
|
97
|
+
### 1.3 Data Architecture
|
|
98
|
+
|
|
99
|
+
```mermaid
|
|
100
|
+
graph LR
|
|
101
|
+
A[CSV Float Dollars] -->|parseAmount| B[BankTransaction Milliunits]
|
|
102
|
+
C[YNAB API Milliunits] -->|normalizeYNABTransaction| D[NormalizedYNABTransaction]
|
|
103
|
+
|
|
104
|
+
B --> E[Matcher Integer Comparison]
|
|
105
|
+
D --> E
|
|
106
|
+
|
|
107
|
+
E -->|amount === amount| F[Exact Match 100 score]
|
|
108
|
+
E -->|Math.abs diff <= tolerance| G[Tolerance Match 95 score]
|
|
109
|
+
|
|
110
|
+
F --> H[High Confidence 85+]
|
|
111
|
+
G --> H
|
|
112
|
+
|
|
113
|
+
H --> I[Auto-Match Execution]
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Critical Design Decision:** All amounts use **milliunits (integers)** throughout the system. Conversion from CSV floats happens once at the parser boundary, then all comparisons use exact integer arithmetic (`===` instead of `Math.abs(a - b) < epsilon`).
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## 2. Core Components
|
|
121
|
+
|
|
122
|
+
### 2.1 Entry Point: index.ts
|
|
123
|
+
|
|
124
|
+
**File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\index.ts`
|
|
125
|
+
|
|
126
|
+
**Responsibilities:**
|
|
127
|
+
- MCP tool interface implementation
|
|
128
|
+
- Schema validation (ReconcileAccountSchema)
|
|
129
|
+
- CSV vs File input handling
|
|
130
|
+
- Date range auto-detection
|
|
131
|
+
- Smart sign inversion for liability accounts
|
|
132
|
+
- Orchestration of analysis and execution phases
|
|
133
|
+
|
|
134
|
+
**Key Functions:**
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
export async function handleReconcileAccount(
|
|
138
|
+
ynabAPI: ynab.API,
|
|
139
|
+
deltaFetcher: DeltaFetcher,
|
|
140
|
+
params: ReconcileAccountRequest,
|
|
141
|
+
): Promise<CallToolResult>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Decision Tree:**
|
|
145
|
+
|
|
146
|
+
```mermaid
|
|
147
|
+
graph TD
|
|
148
|
+
A[handleReconcileAccount] --> B{CSV Source?}
|
|
149
|
+
B -->|csv_data| C[Use inline CSV]
|
|
150
|
+
B -->|csv_file_path| D[Read from filesystem]
|
|
151
|
+
|
|
152
|
+
C --> E{Account Type?}
|
|
153
|
+
D --> E
|
|
154
|
+
|
|
155
|
+
E -->|Liability| F[Negate statement_balance]
|
|
156
|
+
E -->|Asset| G[Use statement_balance as-is]
|
|
157
|
+
|
|
158
|
+
F --> H{invert_bank_amounts set?}
|
|
159
|
+
G --> H
|
|
160
|
+
|
|
161
|
+
H -->|Yes| I[Use explicit value]
|
|
162
|
+
H -->|No| J[Auto-detect sign inversion]
|
|
163
|
+
|
|
164
|
+
I --> K[analyzeReconciliation]
|
|
165
|
+
J --> K
|
|
166
|
+
|
|
167
|
+
K --> L{Execute Actions?}
|
|
168
|
+
L -->|auto_create/update enabled| M[executeReconciliation]
|
|
169
|
+
L -->|Analysis only| N[Skip execution]
|
|
170
|
+
|
|
171
|
+
M --> O[Build Response Payload]
|
|
172
|
+
N --> O
|
|
173
|
+
|
|
174
|
+
O --> P[Return MCP Response]
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**Configuration Mapping:**
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
// V2 Matching Configuration from Request Parameters
|
|
181
|
+
const config: MatchingConfig = {
|
|
182
|
+
weights: {
|
|
183
|
+
amount: 0.5, // 50% weight
|
|
184
|
+
date: 0.15, // 15% weight
|
|
185
|
+
payee: 0.35, // 35% weight
|
|
186
|
+
},
|
|
187
|
+
dateToleranceDays: params.date_tolerance_days ?? 5,
|
|
188
|
+
amountToleranceMilliunits: (params.amount_tolerance_cents ?? 1) * 10,
|
|
189
|
+
autoMatchThreshold: params.auto_match_threshold ?? 90,
|
|
190
|
+
suggestedMatchThreshold: params.suggestion_threshold ?? 60,
|
|
191
|
+
minimumCandidateScore: 40,
|
|
192
|
+
exactAmountBonus: 10,
|
|
193
|
+
exactDateBonus: 5,
|
|
194
|
+
exactPayeeBonus: 10,
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### 2.2 CSV Parser: csvParser.ts
|
|
201
|
+
|
|
202
|
+
**File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\csvParser.ts`
|
|
203
|
+
|
|
204
|
+
**Purpose:** Parse bank CSV exports into standardized BankTransaction objects with robust error handling and multi-format support.
|
|
205
|
+
|
|
206
|
+
**Architecture:**
|
|
207
|
+
|
|
208
|
+
```mermaid
|
|
209
|
+
graph TD
|
|
210
|
+
A[Raw CSV String] --> B{Security Check}
|
|
211
|
+
B -->|Size > 10MB| C[Throw Error]
|
|
212
|
+
B -->|Size OK| D[Auto-Detect Format]
|
|
213
|
+
|
|
214
|
+
D --> E{Preset Detected?}
|
|
215
|
+
E -->|Yes| F[Load Bank Preset]
|
|
216
|
+
E -->|No| G[Use Default Columns]
|
|
217
|
+
|
|
218
|
+
F --> H[PapaParse]
|
|
219
|
+
G --> H
|
|
220
|
+
|
|
221
|
+
H --> I{Header Row?}
|
|
222
|
+
I -->|Yes| J[Parse with Headers]
|
|
223
|
+
I -->|No| K[Parse by Index]
|
|
224
|
+
|
|
225
|
+
J --> L[Process Each Row]
|
|
226
|
+
K --> L
|
|
227
|
+
|
|
228
|
+
L --> M{Parse Date}
|
|
229
|
+
M -->|ISO YYYY-MM-DD| N[Direct Parse]
|
|
230
|
+
M -->|Format Hint| O[Apply Preset Format]
|
|
231
|
+
M -->|Fallback| P[chrono-node Parse]
|
|
232
|
+
|
|
233
|
+
N --> Q{Parse Amount}
|
|
234
|
+
O --> Q
|
|
235
|
+
P --> Q
|
|
236
|
+
|
|
237
|
+
Q -->|Single Column| R[parseAmount]
|
|
238
|
+
Q -->|Debit/Credit| S[Combine Columns]
|
|
239
|
+
|
|
240
|
+
R --> T{Warnings?}
|
|
241
|
+
S --> U{Both Populated?}
|
|
242
|
+
|
|
243
|
+
U -->|Yes| V[Emit Warning]
|
|
244
|
+
U -->|No| T
|
|
245
|
+
V --> T
|
|
246
|
+
|
|
247
|
+
T --> W[Create BankTransaction]
|
|
248
|
+
W --> X[Sanitize Description]
|
|
249
|
+
X --> Y[Return Results + Errors + Warnings]
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
**Bank Presets:**
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
export const BANK_PRESETS: Record<string, BankPreset> = {
|
|
256
|
+
'td': {
|
|
257
|
+
name: 'TD Canada Trust',
|
|
258
|
+
header: false, // Headerless CSV support
|
|
259
|
+
dateColumn: ['0', 'Date'],
|
|
260
|
+
debitColumn: '2',
|
|
261
|
+
creditColumn: '3',
|
|
262
|
+
descriptionColumn: ['1', 'Description'],
|
|
263
|
+
dateFormat: 'MDY', // MM/DD/YYYY
|
|
264
|
+
},
|
|
265
|
+
'rbc': {
|
|
266
|
+
name: 'RBC Royal Bank',
|
|
267
|
+
dateColumn: ['Transaction Date', 'Date'],
|
|
268
|
+
debitColumn: 'Debit',
|
|
269
|
+
creditColumn: 'Credit',
|
|
270
|
+
descriptionColumn: ['Description 1', 'Description'],
|
|
271
|
+
dateFormat: 'YMD', // YYYY-MM-DD
|
|
272
|
+
},
|
|
273
|
+
'wealthsimple': {
|
|
274
|
+
name: 'Wealthsimple',
|
|
275
|
+
dateColumn: ['Date'],
|
|
276
|
+
amountColumn: ['Amount'],
|
|
277
|
+
descriptionColumn: ['Description', 'Payee'],
|
|
278
|
+
amountMultiplier: 1, // Already in correct sign
|
|
279
|
+
dateFormat: 'YMD',
|
|
280
|
+
},
|
|
281
|
+
// ... scotiabank, tangerine
|
|
282
|
+
};
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
**Amount Conversion Logic (validated via `parseAmount`):**
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
function parseAmount(str: string): {
|
|
289
|
+
valid: boolean;
|
|
290
|
+
valueMilliunits: number;
|
|
291
|
+
reason?: string;
|
|
292
|
+
} {
|
|
293
|
+
if (!str || !str.trim()) {
|
|
294
|
+
return { valid: false, valueMilliunits: 0, reason: 'Missing amount value' };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let cleaned = str.replace(/[$€£¥]/g, '').replace(/\b(CAD|USD|EUR|GBP)\b/gi, '').trim();
|
|
298
|
+
|
|
299
|
+
if (cleaned.startsWith('(') && cleaned.endsWith(')')) {
|
|
300
|
+
cleaned = '-' + cleaned.slice(1, -1);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (/^-?\d{1,3}(\.\d{3})+,\d{2}$/.test(cleaned)) {
|
|
304
|
+
cleaned = cleaned.replace(/\./g, '').replace(',', '.');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (cleaned.includes('.')) {
|
|
308
|
+
cleaned = cleaned.replace(/,/g, '');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const dollars = parseFloat(cleaned);
|
|
312
|
+
if (!Number.isFinite(dollars)) {
|
|
313
|
+
return { valid: false, valueMilliunits: 0, reason: `Invalid amount: "${str}"` };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { valid: true, valueMilliunits: Math.round(dollars * 1000) };
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Security Measures:**
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// Security: Remove malicious Unicode characters
|
|
324
|
+
rawDesc = rawDesc
|
|
325
|
+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // ASCII + C1 control chars
|
|
326
|
+
.replace(/[\u202A-\u202E\u2066-\u2069]/g, '') // Bidirectional overrides
|
|
327
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars
|
|
328
|
+
.replace(/[\u2028-\u2029]/g, '') // Line/paragraph separators
|
|
329
|
+
.substring(0, 500); // YNAB max memo length
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
### 2.3 Transaction Matcher: matcher.ts
|
|
335
|
+
|
|
336
|
+
**File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\matcher.ts`
|
|
337
|
+
|
|
338
|
+
**Purpose:** Match bank transactions to YNAB transactions using multi-dimensional scoring with configurable thresholds.
|
|
339
|
+
|
|
340
|
+
**Matching Pipeline:**
|
|
341
|
+
|
|
342
|
+
```mermaid
|
|
343
|
+
graph TD
|
|
344
|
+
A[BankTransaction] --> B{Sign Check}
|
|
345
|
+
B -->|Signs Differ| C[Skip Candidate]
|
|
346
|
+
B -->|Signs Match| D{Amount Tolerance}
|
|
347
|
+
|
|
348
|
+
D -->|Diff > Tolerance| C
|
|
349
|
+
D -->|Diff <= Tolerance| E[Calculate Scores]
|
|
350
|
+
|
|
351
|
+
E --> F[Amount Score 0-100]
|
|
352
|
+
E --> G[Date Score 0-100]
|
|
353
|
+
E --> H[Payee Score 0-100]
|
|
354
|
+
|
|
355
|
+
F --> I[Weighted Combination]
|
|
356
|
+
G --> I
|
|
357
|
+
H --> I
|
|
358
|
+
|
|
359
|
+
I --> J{Apply Bonuses}
|
|
360
|
+
J -->|Amount === exact| K[+10 bonus]
|
|
361
|
+
J -->|Date === exact| L[+5 bonus]
|
|
362
|
+
J -->|Payee >= 95| M[+10 bonus]
|
|
363
|
+
|
|
364
|
+
K --> N[Final Combined Score]
|
|
365
|
+
L --> N
|
|
366
|
+
M --> N
|
|
367
|
+
|
|
368
|
+
N --> O{Score >= Min Candidate?}
|
|
369
|
+
O -->|No| C
|
|
370
|
+
O -->|Yes| P[Add to Candidates]
|
|
371
|
+
|
|
372
|
+
P --> Q[Sort by Score Descending]
|
|
373
|
+
Q --> R{Top Score >= Auto-Match?}
|
|
374
|
+
|
|
375
|
+
R -->|Yes| S[confidence: high]
|
|
376
|
+
R -->|No| T{Score >= Suggested?}
|
|
377
|
+
|
|
378
|
+
T -->|Yes| U[confidence: medium]
|
|
379
|
+
T -->|No| V{Score >= Min?}
|
|
380
|
+
|
|
381
|
+
V -->|Yes| W[confidence: low]
|
|
382
|
+
V -->|No| X[confidence: none]
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Scoring Algorithms:**
|
|
386
|
+
|
|
387
|
+
**Amount Score (Integer Comparison):**
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
|
|
391
|
+
let amountScore: number;
|
|
392
|
+
|
|
393
|
+
if (amountDiff === 0) {
|
|
394
|
+
// Exact integer match - no floating point issues!
|
|
395
|
+
amountScore = 100;
|
|
396
|
+
} else if (amountDiff <= config.amountToleranceMilliunits) { // Default: 10 (1 cent)
|
|
397
|
+
amountScore = 95;
|
|
398
|
+
} else if (amountDiff <= 1000) { // Within $1
|
|
399
|
+
amountScore = 80 - (amountDiff / 1000 * 20);
|
|
400
|
+
} else {
|
|
401
|
+
amountScore = Math.max(0, 60 - (amountDiff / 1000 * 5));
|
|
402
|
+
}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
**Date Score (Days Difference):**
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
const daysDiff = Math.abs(bankDate.getTime() - ynabDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
409
|
+
let dateScore: number;
|
|
410
|
+
|
|
411
|
+
if (daysDiff < 0.5) {
|
|
412
|
+
dateScore = 100; // Same day
|
|
413
|
+
} else if (daysDiff <= 1) {
|
|
414
|
+
dateScore = 95; // 1 day
|
|
415
|
+
} else if (daysDiff <= config.dateToleranceDays) { // Default: 7 days
|
|
416
|
+
dateScore = 90 - ((daysDiff - 1) * (40 / config.dateToleranceDays));
|
|
417
|
+
} else {
|
|
418
|
+
dateScore = Math.max(0, 50 - ((daysDiff - config.dateToleranceDays) * 5));
|
|
419
|
+
}
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
**Payee Score (Fuzzy Matching with Fuzzball):**
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
import * as fuzz from 'fuzzball';
|
|
426
|
+
|
|
427
|
+
function calculatePayeeScore(bankPayee: string, ynabPayee: string | null): number {
|
|
428
|
+
if (!ynabPayee) return 30;
|
|
429
|
+
|
|
430
|
+
const scores = [
|
|
431
|
+
fuzz.token_set_ratio(bankPayee, ynabPayee), // Handles word order
|
|
432
|
+
fuzz.token_sort_ratio(bankPayee, ynabPayee), // Alphabetizes then compares
|
|
433
|
+
fuzz.partial_ratio(bankPayee, ynabPayee), // Best substring match
|
|
434
|
+
fuzz.WRatio(bankPayee, ynabPayee), // Weighted combination
|
|
435
|
+
];
|
|
436
|
+
|
|
437
|
+
return Math.max(...scores);
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
**Why Fuzzball?**
|
|
442
|
+
|
|
443
|
+
| Input Bank | Input YNAB | Levenshtein | Fuzzball token_set_ratio |
|
|
444
|
+
|------------|------------|-------------|--------------------------|
|
|
445
|
+
| "AMZN MKTP CA*123456" | "Amazon" | 45 | 90 |
|
|
446
|
+
| "SQ *COFFEE SHOP TORONTO" | "Square Coffee" | 38 | 85 |
|
|
447
|
+
| "PAYPAL *NETFLIX" | "Netflix" | 54 | 100 |
|
|
448
|
+
|
|
449
|
+
**Tie-Breaking Rules:**
|
|
450
|
+
|
|
451
|
+
```typescript
|
|
452
|
+
candidates.sort((a, b) => {
|
|
453
|
+
// 1. Sort by combined score
|
|
454
|
+
const scoreDiff = b.scores.combined - a.scores.combined;
|
|
455
|
+
if (scoreDiff !== 0) return scoreDiff;
|
|
456
|
+
|
|
457
|
+
// 2. Prefer uncleared over cleared (expecting confirmation)
|
|
458
|
+
const aUncleared = a.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
|
|
459
|
+
const bUncleared = b.ynabTransaction.cleared === 'uncleared' ? 1 : 0;
|
|
460
|
+
if (aUncleared !== bUncleared) return bUncleared - aUncleared;
|
|
461
|
+
|
|
462
|
+
// 3. Prefer closer date proximity
|
|
463
|
+
const bankTime = new Date(bankTxn.date).getTime();
|
|
464
|
+
const aDiff = Math.abs(bankTime - new Date(a.ynabTransaction.date).getTime());
|
|
465
|
+
const bDiff = Math.abs(bankTime - new Date(b.ynabTransaction.date).getTime());
|
|
466
|
+
if (aDiff !== bDiff) return aDiff - bDiff;
|
|
467
|
+
|
|
468
|
+
return 0;
|
|
469
|
+
});
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
### 2.4 Analyzer: analyzer.ts
|
|
475
|
+
|
|
476
|
+
**File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\analyzer.ts`
|
|
477
|
+
|
|
478
|
+
**Purpose:** Orchestrate the analysis phase, coordinating CSV parsing, YNAB normalization, matching, and insight generation.
|
|
479
|
+
|
|
480
|
+
**Analysis Pipeline:**
|
|
481
|
+
|
|
482
|
+
```mermaid
|
|
483
|
+
graph TD
|
|
484
|
+
A[analyzeReconciliation] --> B{CSV Pre-Parsed?}
|
|
485
|
+
B -->|Yes| C[Use Provided Result]
|
|
486
|
+
B -->|No| D[parseCSV]
|
|
487
|
+
|
|
488
|
+
C --> E[BankTransaction[]]
|
|
489
|
+
D --> E
|
|
490
|
+
|
|
491
|
+
A --> F[normalizeYNABTransactions]
|
|
492
|
+
F --> G[NormalizedYNABTransaction[]]
|
|
493
|
+
|
|
494
|
+
E --> H[findMatches]
|
|
495
|
+
G --> H
|
|
496
|
+
|
|
497
|
+
H --> I[MatchResult[]]
|
|
498
|
+
|
|
499
|
+
I --> J[Categorize by Confidence]
|
|
500
|
+
J --> K[auto_matches: high]
|
|
501
|
+
J --> L[suggested_matches: medium]
|
|
502
|
+
J --> M[unmatchedBank: low/none]
|
|
503
|
+
|
|
504
|
+
G --> N[Find Unmatched YNAB]
|
|
505
|
+
I --> N
|
|
506
|
+
N --> O[unmatched_ynab]
|
|
507
|
+
|
|
508
|
+
K --> P[calculateBalances]
|
|
509
|
+
G --> P
|
|
510
|
+
P --> Q[BalanceInfo]
|
|
511
|
+
|
|
512
|
+
K --> R[generateSummary]
|
|
513
|
+
L --> R
|
|
514
|
+
M --> R
|
|
515
|
+
O --> R
|
|
516
|
+
Q --> R
|
|
517
|
+
R --> S[ReconciliationSummary]
|
|
518
|
+
|
|
519
|
+
S --> T[generateNextSteps]
|
|
520
|
+
T --> U[next_steps: string[]]
|
|
521
|
+
|
|
522
|
+
M --> V[detectInsights]
|
|
523
|
+
S --> V
|
|
524
|
+
Q --> V
|
|
525
|
+
V --> W[insights: ReconciliationInsight[]]
|
|
526
|
+
|
|
527
|
+
K --> X{Recommendations Enabled?}
|
|
528
|
+
L --> X
|
|
529
|
+
M --> X
|
|
530
|
+
O --> X
|
|
531
|
+
|
|
532
|
+
X -->|Yes| Y[generateRecommendations]
|
|
533
|
+
Y --> Z[recommendations: ActionableRecommendation[]]
|
|
534
|
+
|
|
535
|
+
S --> AA[Build ReconciliationAnalysis]
|
|
536
|
+
U --> AA
|
|
537
|
+
W --> AA
|
|
538
|
+
Z --> AA
|
|
539
|
+
|
|
540
|
+
AA --> AB[Return Analysis]
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Insight Detection:**
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
function detectInsights(
|
|
547
|
+
unmatchedBank: BankTransaction[],
|
|
548
|
+
summary: ReconciliationSummary,
|
|
549
|
+
balances: BalanceInfo,
|
|
550
|
+
currency: string,
|
|
551
|
+
csvErrors: ParseError[],
|
|
552
|
+
csvWarnings: ParseWarning[],
|
|
553
|
+
): ReconciliationInsight[]
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
**Insight Types:**
|
|
557
|
+
|
|
558
|
+
| Type | Severity | Trigger | Purpose |
|
|
559
|
+
|------|----------|---------|---------|
|
|
560
|
+
| `csv-parse-errors` | critical/warning | CSV parsing errors | Surface parsing failures |
|
|
561
|
+
| `csv-parse-warnings` | info | CSV parsing warnings | Surface ambiguous data |
|
|
562
|
+
| `repeat_amount` | critical/warning | 2+ unmatched txns same amount | Highlight quick wins |
|
|
563
|
+
| `balance-gap` | critical/warning | Discrepancy > $1.00 | Focus attention on gap |
|
|
564
|
+
|
|
565
|
+
**Example Insight:**
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
{
|
|
569
|
+
id: 'repeat--45230',
|
|
570
|
+
type: 'repeat_amount',
|
|
571
|
+
severity: 'warning',
|
|
572
|
+
title: '3 unmatched transactions at -$45.23',
|
|
573
|
+
description: 'The bank statement shows 3 unmatched transaction(s) at -$45.23. Repeated amounts are usually the quickest wins — reconcile these first.',
|
|
574
|
+
evidence: {
|
|
575
|
+
amount: -45230, // Milliunits
|
|
576
|
+
occurrences: 3,
|
|
577
|
+
dates: ['2025-09-15', '2025-09-20', '2025-09-25'],
|
|
578
|
+
csv_rows: [2, 5, 8],
|
|
579
|
+
},
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
### 2.5 Recommendation Engine: recommendationEngine.ts
|
|
586
|
+
|
|
587
|
+
**File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\recommendationEngine.ts`
|
|
588
|
+
|
|
589
|
+
**Purpose:** Generate actionable, prioritized recommendations from reconciliation analysis results.
|
|
590
|
+
|
|
591
|
+
**Recommendation Types:**
|
|
592
|
+
|
|
593
|
+
```mermaid
|
|
594
|
+
graph TD
|
|
595
|
+
A[ReconciliationAnalysis] --> B{Process Insights}
|
|
596
|
+
B --> C{Insight Type?}
|
|
597
|
+
|
|
598
|
+
C -->|near_match| D[createNearMatchRecommendation]
|
|
599
|
+
C -->|repeat_amount| E[createRepeatAmountRecommendations]
|
|
600
|
+
C -->|anomaly| F[createManualReviewRecommendation]
|
|
601
|
+
|
|
602
|
+
D --> G[ManualReviewRecommendation]
|
|
603
|
+
E --> G
|
|
604
|
+
F --> G
|
|
605
|
+
|
|
606
|
+
A --> H{Process Unmatched Bank}
|
|
607
|
+
H --> I[createUnmatchedBankRecommendation]
|
|
608
|
+
I --> J[CreateTransactionRecommendation]
|
|
609
|
+
|
|
610
|
+
A --> K{Process Suggested Matches}
|
|
611
|
+
K --> L{Has YNAB Transaction?}
|
|
612
|
+
|
|
613
|
+
L -->|Yes + Confidence high/medium| M[createSuggestedMatchRecommendation]
|
|
614
|
+
L -->|No| N[Create Missing Transaction]
|
|
615
|
+
L -->|Multiple Candidates| O[Combination Match]
|
|
616
|
+
|
|
617
|
+
M --> P[ReviewDuplicateRecommendation]
|
|
618
|
+
N --> J
|
|
619
|
+
O --> Q[createCombinationReviewRecommendation]
|
|
620
|
+
Q --> G
|
|
621
|
+
|
|
622
|
+
A --> R{Process Unmatched YNAB}
|
|
623
|
+
R -->|Uncleared| S[createUpdateClearedRecommendation]
|
|
624
|
+
S --> T[UpdateClearedRecommendation]
|
|
625
|
+
|
|
626
|
+
G --> U[All Recommendations]
|
|
627
|
+
J --> U
|
|
628
|
+
P --> U
|
|
629
|
+
T --> U
|
|
630
|
+
|
|
631
|
+
U --> V[sortRecommendations]
|
|
632
|
+
V --> W{Sort Order}
|
|
633
|
+
|
|
634
|
+
W -->|1. Priority| X[High > Medium > Low]
|
|
635
|
+
W -->|2. Confidence| Y[0.95 > 0.80 > 0.60]
|
|
636
|
+
|
|
637
|
+
X --> Z[Sorted Recommendations]
|
|
638
|
+
Y --> Z
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
**Recommendation Schema:**
|
|
642
|
+
|
|
643
|
+
```typescript
|
|
644
|
+
export type ActionableRecommendation =
|
|
645
|
+
| CreateTransactionRecommendation
|
|
646
|
+
| UpdateClearedRecommendation
|
|
647
|
+
| ReviewDuplicateRecommendation
|
|
648
|
+
| ManualReviewRecommendation;
|
|
649
|
+
|
|
650
|
+
interface CreateTransactionRecommendation {
|
|
651
|
+
action_type: 'create_transaction';
|
|
652
|
+
priority: 'high' | 'medium' | 'low';
|
|
653
|
+
confidence: number; // 0-1
|
|
654
|
+
message: string;
|
|
655
|
+
reason: string;
|
|
656
|
+
estimated_impact: MoneyValue;
|
|
657
|
+
parameters: {
|
|
658
|
+
account_id: string;
|
|
659
|
+
date: string;
|
|
660
|
+
amount: number; // Milliunits
|
|
661
|
+
payee_name: string;
|
|
662
|
+
memo?: string;
|
|
663
|
+
cleared: 'cleared' | 'uncleared';
|
|
664
|
+
approved: boolean;
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
**Confidence Levels:**
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
const CONFIDENCE = {
|
|
673
|
+
CREATE_EXACT_MATCH: 0.95,
|
|
674
|
+
NEAR_MATCH_REVIEW: 0.7,
|
|
675
|
+
REPEAT_AMOUNT: 0.75,
|
|
676
|
+
ANOMALY_REVIEW: 0.5,
|
|
677
|
+
UNMATCHED_BANK: 0.8,
|
|
678
|
+
UPDATE_CLEARED: 0.6,
|
|
679
|
+
} as const;
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
**Priority Assignment:**
|
|
683
|
+
|
|
684
|
+
| Scenario | Action Type | Priority | Confidence | Rationale |
|
|
685
|
+
|----------|-------------|----------|------------|-----------|
|
|
686
|
+
| Unmatched bank txn | create_transaction | medium | 0.80 | Safe to create, medium urgency |
|
|
687
|
+
| Exact match suggestion | review_duplicate | high | 0.95 | High confidence match needs confirmation |
|
|
688
|
+
| Combination match | manual_review | medium | 0.70 | Complex scenario, needs human judgment |
|
|
689
|
+
| Uncleared YNAB txn | update_cleared | low | 0.60 | Cleanup action, low urgency |
|
|
690
|
+
| Anomaly insight | manual_review | low | 0.50 | Investigation needed |
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
### 2.6 Executor: executor.ts
|
|
695
|
+
|
|
696
|
+
**File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\executor.ts`
|
|
697
|
+
|
|
698
|
+
**Purpose:** Execute reconciliation actions (create, update, clear, reconcile transactions) with bulk operation support and error handling.
|
|
699
|
+
|
|
700
|
+
**Execution Phases:**
|
|
701
|
+
|
|
702
|
+
```mermaid
|
|
703
|
+
graph TD
|
|
704
|
+
A[executeReconciliation] --> B[Initialize State]
|
|
705
|
+
B --> C[clearedDeltaMilli = cleared - statement]
|
|
706
|
+
|
|
707
|
+
C --> D{Balance Aligned?}
|
|
708
|
+
D -->|Yes| E[Skip Execution]
|
|
709
|
+
D -->|No| F[PHASE 1: Auto-Create Missing]
|
|
710
|
+
|
|
711
|
+
F --> G{auto_create_transactions?}
|
|
712
|
+
G -->|No| H[Skip Phase 1]
|
|
713
|
+
G -->|Yes| I{2+ Unmatched Bank?}
|
|
714
|
+
|
|
715
|
+
I -->|Yes| J[Bulk Create Path]
|
|
716
|
+
I -->|No| K[Sequential Create Path]
|
|
717
|
+
|
|
718
|
+
J --> L[Build Batches Until Balance Aligns]
|
|
719
|
+
L --> M[Chunk by MAX_BULK_CREATE_CHUNK = 100]
|
|
720
|
+
M --> N[Process Each Chunk]
|
|
721
|
+
|
|
722
|
+
N --> O{Bulk API Success?}
|
|
723
|
+
O -->|Yes| P[Correlate Results]
|
|
724
|
+
O -->|No| Q[Sequential Fallback]
|
|
725
|
+
|
|
726
|
+
P --> R[Update clearedDeltaMilli]
|
|
727
|
+
Q --> R
|
|
728
|
+
K --> R
|
|
729
|
+
|
|
730
|
+
R --> S{Balance Aligned?}
|
|
731
|
+
S -->|Yes| T[Skip Remaining Phases]
|
|
732
|
+
S -->|No| U[PHASE 2: Update Matched Txns]
|
|
733
|
+
|
|
734
|
+
U --> V{auto_update_cleared_status?}
|
|
735
|
+
V -->|Yes| W[Collect Updates]
|
|
736
|
+
V -->|No| X[Skip Phase 2]
|
|
737
|
+
|
|
738
|
+
W --> Y[Chunk by MAX_BULK_UPDATE_CHUNK = 100]
|
|
739
|
+
Y --> Z[Batch Update API Call]
|
|
740
|
+
|
|
741
|
+
Z --> AA{Balance Aligned?}
|
|
742
|
+
AA -->|Yes| T
|
|
743
|
+
AA -->|No| AB[PHASE 3: Auto-Unclear Missing]
|
|
744
|
+
|
|
745
|
+
AB --> AC{auto_unclear_missing?}
|
|
746
|
+
AC -->|Yes| AD[Collect Unclear Updates]
|
|
747
|
+
AC -->|No| AE[Skip Phase 3]
|
|
748
|
+
|
|
749
|
+
AD --> AF[Batch Unclear API Call]
|
|
750
|
+
|
|
751
|
+
AF --> AG{Balance Aligned?}
|
|
752
|
+
AG -->|Yes| T
|
|
753
|
+
AG -->|No| AH[PHASE 4: Mark as Reconciled]
|
|
754
|
+
|
|
755
|
+
AH --> AI[Batch Reconcile Matched Txns]
|
|
756
|
+
|
|
757
|
+
T --> AJ[PHASE 5: Balance Reconciliation]
|
|
758
|
+
AI --> AJ
|
|
759
|
+
AH --> AJ
|
|
760
|
+
|
|
761
|
+
AJ --> AK{statement_date provided?}
|
|
762
|
+
AK -->|Yes| AL[buildBalanceReconciliation]
|
|
763
|
+
AK -->|No| AM[Skip Balance Snapshot]
|
|
764
|
+
|
|
765
|
+
AL --> AN[Refresh Account Snapshot]
|
|
766
|
+
AM --> AN
|
|
767
|
+
|
|
768
|
+
AN --> AO[Build Recommendations]
|
|
769
|
+
AO --> AP[Return ExecutionResult]
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
**Bulk Operation Correlation:**
|
|
773
|
+
|
|
774
|
+
```typescript
|
|
775
|
+
// Generate deterministic import_id for deduplication
|
|
776
|
+
function generateBulkImportId(
|
|
777
|
+
accountId: string,
|
|
778
|
+
date: string,
|
|
779
|
+
amountMilli: number,
|
|
780
|
+
payee?: string | null,
|
|
781
|
+
): string {
|
|
782
|
+
const normalizedPayee = (payee ?? '').trim().toLowerCase();
|
|
783
|
+
const raw = `${accountId}|${date}|${amountMilli}|${normalizedPayee}`;
|
|
784
|
+
const digest = createHash('sha256').update(raw).digest('hex').slice(0, 24);
|
|
785
|
+
return `YNAB:bulk:${digest}`;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Correlate bulk API response with requests
|
|
789
|
+
const correlated = correlateResults(
|
|
790
|
+
chunk.map(entry => toCorrelationPayload(entry.saveTransaction)),
|
|
791
|
+
response.data,
|
|
792
|
+
duplicateImportIds
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
for (const result of correlated) {
|
|
796
|
+
if (result.status === 'created') {
|
|
797
|
+
// Success - record action
|
|
798
|
+
} else if (result.status === 'duplicate') {
|
|
799
|
+
// Duplicate detected - emit warning
|
|
800
|
+
bulkDetails.duplicates_detected += 1;
|
|
801
|
+
} else {
|
|
802
|
+
// Failure - record error
|
|
803
|
+
bulkDetails.transaction_failures += 1;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
**Error Handling Strategy:**
|
|
809
|
+
|
|
810
|
+
```mermaid
|
|
811
|
+
graph TD
|
|
812
|
+
A[Bulk Chunk Failure] --> B[normalizeYnabError]
|
|
813
|
+
B --> C{shouldPropagateYnabError?}
|
|
814
|
+
|
|
815
|
+
C -->|Yes 400/401/403/404/429/500/503| D[Throw Error]
|
|
816
|
+
C -->|No Other Status| E[Log Warning]
|
|
817
|
+
|
|
818
|
+
E --> F[Sequential Fallback]
|
|
819
|
+
F --> G[Process Each Transaction Individually]
|
|
820
|
+
|
|
821
|
+
G --> H{Individual Success?}
|
|
822
|
+
H -->|Yes| I[Record Success]
|
|
823
|
+
H -->|No| J{shouldPropagateYnabError?}
|
|
824
|
+
|
|
825
|
+
J -->|Yes| D
|
|
826
|
+
J -->|No| K[Record Failure + Continue]
|
|
827
|
+
|
|
828
|
+
I --> L[Update Metrics]
|
|
829
|
+
K --> L
|
|
830
|
+
|
|
831
|
+
L --> M{More Transactions?}
|
|
832
|
+
M -->|Yes| G
|
|
833
|
+
M -->|No| N[Return with Partial Success]
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
**Bulk Operation Metrics:**
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
export interface BulkOperationDetails {
|
|
840
|
+
chunks_processed: number;
|
|
841
|
+
bulk_successes: number;
|
|
842
|
+
sequential_fallbacks: number;
|
|
843
|
+
duplicates_detected: number;
|
|
844
|
+
failed_transactions: number; // Backward-compatible alias
|
|
845
|
+
bulk_chunk_failures: number; // API-level failures (entire chunk)
|
|
846
|
+
transaction_failures: number; // Canonical per-transaction failures
|
|
847
|
+
sequential_attempts?: number; // Fallback creation attempts
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
### 2.7 Report Formatter: reportFormatter.ts
|
|
854
|
+
|
|
855
|
+
**File:** `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\reportFormatter.ts`
|
|
856
|
+
|
|
857
|
+
**Purpose:** Format reconciliation results into human-readable reports with structured sections.
|
|
858
|
+
|
|
859
|
+
**Report Structure:**
|
|
860
|
+
|
|
861
|
+
```
|
|
862
|
+
📊 [Account Name] Reconciliation Report
|
|
863
|
+
═══════════════════════════════════════════════════════════════
|
|
864
|
+
|
|
865
|
+
Statement Period: [date_range]
|
|
866
|
+
|
|
867
|
+
BALANCE CHECK
|
|
868
|
+
═══════════════════════════════════════════════════════════════
|
|
869
|
+
✓ YNAB Cleared Balance: [amount]
|
|
870
|
+
✓ Statement Balance: [amount]
|
|
871
|
+
|
|
872
|
+
[✅ BALANCES MATCH PERFECTLY | ❌ DISCREPANCY: [amount]]
|
|
873
|
+
|
|
874
|
+
TRANSACTION ANALYSIS
|
|
875
|
+
═══════════════════════════════════════════════════════════════
|
|
876
|
+
✓ Automatically matched: [count] of [total] transactions
|
|
877
|
+
✓ Suggested matches: [count]
|
|
878
|
+
✓ Unmatched bank: [count]
|
|
879
|
+
✓ Unmatched YNAB: [count]
|
|
880
|
+
|
|
881
|
+
❌ UNMATCHED BANK TRANSACTIONS:
|
|
882
|
+
[date] - [payee] [±amount]
|
|
883
|
+
...
|
|
884
|
+
|
|
885
|
+
💡 SUGGESTED MATCHES:
|
|
886
|
+
[date] - [payee] [±amount] ([confidence]% confidence)
|
|
887
|
+
...
|
|
888
|
+
|
|
889
|
+
KEY INSIGHTS
|
|
890
|
+
═══════════════════════════════════════════════════════════════
|
|
891
|
+
[🚨|⚠️|ℹ️] [insight.title]
|
|
892
|
+
[insight.description]
|
|
893
|
+
Evidence: [summary]
|
|
894
|
+
|
|
895
|
+
EXECUTION SUMMARY
|
|
896
|
+
═══════════════════════════════════════════════════════════════
|
|
897
|
+
• Transactions created: [count]
|
|
898
|
+
• Transactions updated: [count]
|
|
899
|
+
• Date adjustments: [count]
|
|
900
|
+
|
|
901
|
+
Recommendations:
|
|
902
|
+
• [recommendation 1]
|
|
903
|
+
• [recommendation 2]
|
|
904
|
+
...
|
|
905
|
+
|
|
906
|
+
[⚠️ Dry run only — no YNAB changes were applied. | ✅ Changes applied to YNAB.]
|
|
907
|
+
|
|
908
|
+
RECOMMENDED ACTIONS
|
|
909
|
+
═══════════════════════════════════════════════════════════════
|
|
910
|
+
• [next_step 1]
|
|
911
|
+
• [next_step 2]
|
|
912
|
+
...
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
**Format Options:**
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
export interface ReportFormatterOptions {
|
|
919
|
+
accountName?: string;
|
|
920
|
+
accountId?: string;
|
|
921
|
+
currencyCode?: string;
|
|
922
|
+
includeDetailedMatches?: boolean;
|
|
923
|
+
maxUnmatchedToShow?: number; // Default: 5
|
|
924
|
+
maxInsightsToShow?: number; // Default: 3
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
---
|
|
929
|
+
|
|
930
|
+
## 3. Data Flow
|
|
931
|
+
|
|
932
|
+
### 3.1 End-to-End Data Flow Diagram
|
|
933
|
+
|
|
934
|
+
```mermaid
|
|
935
|
+
sequenceDiagram
|
|
936
|
+
participant User
|
|
937
|
+
participant MCP as MCP Tool Interface
|
|
938
|
+
participant Handler as index.ts Handler
|
|
939
|
+
participant Parser as csvParser.ts
|
|
940
|
+
participant Fetcher as DeltaFetcher
|
|
941
|
+
participant Adapter as ynabAdapter.ts
|
|
942
|
+
participant Analyzer as analyzer.ts
|
|
943
|
+
participant Matcher as matcher.ts
|
|
944
|
+
participant RecEngine as recommendationEngine.ts
|
|
945
|
+
participant Executor as executor.ts
|
|
946
|
+
participant YNAB as YNAB API
|
|
947
|
+
participant Formatter as reportFormatter.ts
|
|
948
|
+
|
|
949
|
+
User->>MCP: reconcile_account(csv_data, statement_balance, ...)
|
|
950
|
+
MCP->>Handler: handleReconcileAccount(params)
|
|
951
|
+
|
|
952
|
+
Handler->>Parser: parseCSV(csvContent, options)
|
|
953
|
+
Parser->>Parser: Auto-detect format
|
|
954
|
+
Parser->>Parser: PapaParse
|
|
955
|
+
Parser->>Parser: parseAmount
|
|
956
|
+
Parser-->>Handler: { transactions: BankTransaction[], errors, warnings }
|
|
957
|
+
|
|
958
|
+
Handler->>Fetcher: fetchTransactionsByAccount(budget_id, account_id, since_date)
|
|
959
|
+
Fetcher->>YNAB: GET /budgets/{id}/accounts/{id}/transactions
|
|
960
|
+
YNAB-->>Fetcher: TransactionDetail[]
|
|
961
|
+
Fetcher-->>Handler: { data: TransactionDetail[], wasCached, usedDelta }
|
|
962
|
+
|
|
963
|
+
Handler->>Adapter: normalizeYNABTransactions(ynabTransactions)
|
|
964
|
+
Adapter-->>Handler: NormalizedYNABTransaction[]
|
|
965
|
+
|
|
966
|
+
Handler->>Analyzer: analyzeReconciliation(bankTxns, ynabTxns, config)
|
|
967
|
+
|
|
968
|
+
Analyzer->>Matcher: findMatches(bankTxns, ynabTxns, config)
|
|
969
|
+
|
|
970
|
+
loop For each bank transaction
|
|
971
|
+
Matcher->>Matcher: findCandidates(bankTxn, ynabTxns, usedIds)
|
|
972
|
+
Matcher->>Matcher: calculateScores(amount, date, payee)
|
|
973
|
+
Matcher->>Matcher: Sort by combined score
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
Matcher-->>Analyzer: MatchResult[]
|
|
977
|
+
|
|
978
|
+
Analyzer->>Analyzer: Categorize matches (auto, suggested, unmatched)
|
|
979
|
+
Analyzer->>Analyzer: calculateBalances()
|
|
980
|
+
Analyzer->>Analyzer: generateSummary()
|
|
981
|
+
Analyzer->>Analyzer: detectInsights()
|
|
982
|
+
|
|
983
|
+
Analyzer->>RecEngine: generateRecommendations(context)
|
|
984
|
+
|
|
985
|
+
loop For each insight
|
|
986
|
+
RecEngine->>RecEngine: processInsight()
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
loop For each unmatched bank txn
|
|
990
|
+
RecEngine->>RecEngine: createUnmatchedBankRecommendation()
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
loop For each suggested match
|
|
994
|
+
RecEngine->>RecEngine: createSuggestedMatchRecommendation()
|
|
995
|
+
end
|
|
996
|
+
|
|
997
|
+
RecEngine-->>Analyzer: ActionableRecommendation[]
|
|
998
|
+
|
|
999
|
+
Analyzer-->>Handler: ReconciliationAnalysis
|
|
1000
|
+
|
|
1001
|
+
alt auto_create_transactions || auto_update_cleared_status
|
|
1002
|
+
Handler->>Executor: executeReconciliation(analysis, params)
|
|
1003
|
+
|
|
1004
|
+
loop Phase 1: Auto-create missing transactions
|
|
1005
|
+
Executor->>Executor: Build bulk create batches
|
|
1006
|
+
Executor->>YNAB: POST /budgets/{id}/transactions (bulk)
|
|
1007
|
+
YNAB-->>Executor: BulkResponse
|
|
1008
|
+
Executor->>Executor: Correlate results
|
|
1009
|
+
Executor->>Executor: Update clearedDeltaMilli
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
loop Phase 2: Update matched transactions
|
|
1013
|
+
Executor->>Executor: Collect updates (cleared, date adjustments)
|
|
1014
|
+
Executor->>YNAB: PATCH /budgets/{id}/transactions (bulk)
|
|
1015
|
+
YNAB-->>Executor: UpdateResponse
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
loop Phase 3: Auto-unclear missing
|
|
1019
|
+
Executor->>Executor: Collect unclear updates
|
|
1020
|
+
Executor->>YNAB: PATCH /budgets/{id}/transactions (bulk)
|
|
1021
|
+
YNAB-->>Executor: UpdateResponse
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
alt Balance aligned
|
|
1025
|
+
Executor->>YNAB: PATCH /budgets/{id}/transactions (mark reconciled)
|
|
1026
|
+
YNAB-->>Executor: UpdateResponse
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
Executor->>YNAB: GET /budgets/{id}/accounts/{id} (refresh snapshot)
|
|
1030
|
+
YNAB-->>Executor: Account
|
|
1031
|
+
|
|
1032
|
+
Executor-->>Handler: ExecutionResult
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
Handler->>Formatter: formatHumanReadableReport(analysis, execution)
|
|
1036
|
+
Formatter-->>Handler: Formatted report string
|
|
1037
|
+
|
|
1038
|
+
Handler->>Handler: buildReconciliationPayload(analysis, execution)
|
|
1039
|
+
Handler-->>MCP: CallToolResult { human, structured }
|
|
1040
|
+
MCP-->>User: Reconciliation report + structured data
|
|
1041
|
+
```
|
|
1042
|
+
|
|
1043
|
+
### 3.2 Data Transformation Chain
|
|
1044
|
+
|
|
1045
|
+
```mermaid
|
|
1046
|
+
graph LR
|
|
1047
|
+
A[CSV String] -->|parseCSV| B[BankTransaction[]]
|
|
1048
|
+
C[YNAB API] -->|normalizeYNABTransactions| D[NormalizedYNABTransaction[]]
|
|
1049
|
+
|
|
1050
|
+
B --> E{Matching}
|
|
1051
|
+
D --> E
|
|
1052
|
+
|
|
1053
|
+
E -->|findMatches| F[MatchResult[]]
|
|
1054
|
+
|
|
1055
|
+
F -->|Categorize| G[auto_matches]
|
|
1056
|
+
F -->|Categorize| H[suggested_matches]
|
|
1057
|
+
F -->|Categorize| I[unmatched_bank]
|
|
1058
|
+
|
|
1059
|
+
D -->|Filter| J[unmatched_ynab]
|
|
1060
|
+
|
|
1061
|
+
G --> K[ReconciliationAnalysis]
|
|
1062
|
+
H --> K
|
|
1063
|
+
I --> K
|
|
1064
|
+
J --> K
|
|
1065
|
+
|
|
1066
|
+
K -->|generateRecommendations| L[ActionableRecommendation[]]
|
|
1067
|
+
|
|
1068
|
+
K --> M{Execute?}
|
|
1069
|
+
L --> M
|
|
1070
|
+
|
|
1071
|
+
M -->|Yes| N[executeReconciliation]
|
|
1072
|
+
N -->|YNAB API| O[ExecutionResult]
|
|
1073
|
+
|
|
1074
|
+
M -->|No| P[Skip Execution]
|
|
1075
|
+
|
|
1076
|
+
K --> Q[formatHumanReadableReport]
|
|
1077
|
+
O --> Q
|
|
1078
|
+
P --> Q
|
|
1079
|
+
|
|
1080
|
+
Q --> R[Human-Readable Report]
|
|
1081
|
+
|
|
1082
|
+
K --> S[buildReconciliationPayload]
|
|
1083
|
+
O --> S
|
|
1084
|
+
|
|
1085
|
+
S --> T[Structured JSON Payload]
|
|
1086
|
+
|
|
1087
|
+
R --> U[MCP Response]
|
|
1088
|
+
T --> U
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## 4. Transaction Matching Algorithm
|
|
1094
|
+
|
|
1095
|
+
### 4.1 Matching Strategy
|
|
1096
|
+
|
|
1097
|
+
The reconciliation system uses a **multi-dimensional weighted scoring algorithm** with configurable thresholds and bonuses.
|
|
1098
|
+
|
|
1099
|
+
**Core Principles:**
|
|
1100
|
+
1. **Amount is King** (50% weight) - Most reliable signal
|
|
1101
|
+
2. **Dates are Unreliable** (15% weight) - Banks delay posting 3-7 days
|
|
1102
|
+
3. **Payees are Fuzzy** (35% weight) - Merchant names vary significantly
|
|
1103
|
+
|
|
1104
|
+
**Default Configuration:**
|
|
1105
|
+
|
|
1106
|
+
```typescript
|
|
1107
|
+
export const DEFAULT_CONFIG: MatchingConfig = {
|
|
1108
|
+
weights: {
|
|
1109
|
+
amount: 0.5, // 50%
|
|
1110
|
+
date: 0.15, // 15%
|
|
1111
|
+
payee: 0.35, // 35%
|
|
1112
|
+
},
|
|
1113
|
+
amountToleranceMilliunits: 10, // 1 cent (10 milliunits)
|
|
1114
|
+
dateToleranceDays: 7,
|
|
1115
|
+
autoMatchThreshold: 85,
|
|
1116
|
+
suggestedMatchThreshold: 60,
|
|
1117
|
+
minimumCandidateScore: 40,
|
|
1118
|
+
exactAmountBonus: 10,
|
|
1119
|
+
exactDateBonus: 5,
|
|
1120
|
+
exactPayeeBonus: 10,
|
|
1121
|
+
};
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
### 4.2 Scoring Examples
|
|
1125
|
+
|
|
1126
|
+
**Example 1: Perfect Match**
|
|
1127
|
+
|
|
1128
|
+
```
|
|
1129
|
+
Bank: { date: '2025-09-15', amount: -45230, payee: 'Shell Gas' }
|
|
1130
|
+
YNAB: { date: '2025-09-15', amount: -45230, payee: 'Shell' }
|
|
1131
|
+
|
|
1132
|
+
Scores:
|
|
1133
|
+
Amount: 100 (exact match: -45230 === -45230)
|
|
1134
|
+
Date: 100 (same day)
|
|
1135
|
+
Payee: 85 (fuzz.token_set_ratio('Shell Gas', 'Shell'))
|
|
1136
|
+
|
|
1137
|
+
Combined = (100 * 0.5) + (100 * 0.15) + (85 * 0.35) = 94.75
|
|
1138
|
+
Bonuses = +10 (exact amount) + 5 (exact date) = +15
|
|
1139
|
+
Final = 94.75 + 15 = 109.75 → capped at 100
|
|
1140
|
+
|
|
1141
|
+
Confidence: HIGH (≥ 85)
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
**Example 2: Date Mismatch**
|
|
1145
|
+
|
|
1146
|
+
```
|
|
1147
|
+
Bank: { date: '2025-09-15', amount: -12799, payee: 'Amazon Marketplace' }
|
|
1148
|
+
YNAB: { date: '2025-09-20', amount: -12799, payee: 'Amazon' }
|
|
1149
|
+
|
|
1150
|
+
Scores:
|
|
1151
|
+
Amount: 100 (exact match)
|
|
1152
|
+
Date: 55 (5 days diff within tolerance)
|
|
1153
|
+
Payee: 92 (fuzz.token_set_ratio)
|
|
1154
|
+
|
|
1155
|
+
Combined = (100 * 0.5) + (55 * 0.15) + (92 * 0.35) = 90.45
|
|
1156
|
+
Bonuses = +10 (exact amount) + 10 (payee ≥ 95) = +20
|
|
1157
|
+
Final = 90.45 + 20 = 110.45 → capped at 100
|
|
1158
|
+
|
|
1159
|
+
Confidence: HIGH (≥ 85)
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
**Example 3: Suggested Match**
|
|
1163
|
+
|
|
1164
|
+
```
|
|
1165
|
+
Bank: { date: '2025-09-15', amount: -4520, payee: 'Coffee Shop A' }
|
|
1166
|
+
YNAB: { date: '2025-09-16', amount: -4530, payee: 'Coffee Shop B' }
|
|
1167
|
+
|
|
1168
|
+
Scores:
|
|
1169
|
+
Amount: 95 (10 milliunits diff within tolerance)
|
|
1170
|
+
Date: 95 (1 day diff)
|
|
1171
|
+
Payee: 70 (partial match)
|
|
1172
|
+
|
|
1173
|
+
Combined = (95 * 0.5) + (95 * 0.15) + (70 * 0.35) = 86.25
|
|
1174
|
+
Bonuses = 0 (no exact matches)
|
|
1175
|
+
Final = 86.25
|
|
1176
|
+
|
|
1177
|
+
Confidence: HIGH (≥ 85) but close to threshold
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
**Example 4: Low Confidence**
|
|
1181
|
+
|
|
1182
|
+
```
|
|
1183
|
+
Bank: { date: '2025-09-15', amount: -5000, payee: 'Transfer' }
|
|
1184
|
+
YNAB: { date: '2025-09-22', amount: -5005, payee: 'Payment' }
|
|
1185
|
+
|
|
1186
|
+
Scores:
|
|
1187
|
+
Amount: 95 (5 milliunits diff)
|
|
1188
|
+
Date: 35 (7 days diff at edge of tolerance)
|
|
1189
|
+
Payee: 40 (weak match)
|
|
1190
|
+
|
|
1191
|
+
Combined = (95 * 0.5) + (35 * 0.15) + (40 * 0.35) = 66.25
|
|
1192
|
+
Bonuses = 0
|
|
1193
|
+
Final = 66.25
|
|
1194
|
+
|
|
1195
|
+
Confidence: MEDIUM (60-84)
|
|
1196
|
+
```
|
|
1197
|
+
|
|
1198
|
+
### 4.3 Candidate Filtering
|
|
1199
|
+
|
|
1200
|
+
**Pre-Filtering (Before Scoring):**
|
|
1201
|
+
|
|
1202
|
+
```typescript
|
|
1203
|
+
// 1. Sign Check - both must be same sign
|
|
1204
|
+
const bankSign = Math.sign(bankTxn.amount);
|
|
1205
|
+
const ynabSign = Math.sign(ynabTxn.amount);
|
|
1206
|
+
if (bankSign !== ynabSign && bankSign !== 0 && ynabSign !== 0) {
|
|
1207
|
+
continue; // Skip candidate
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// 2. Amount Tolerance Check - hard filter
|
|
1211
|
+
const amountDiff = Math.abs(bankTxn.amount - ynabTxn.amount);
|
|
1212
|
+
if (amountDiff > config.amountToleranceMilliunits) {
|
|
1213
|
+
continue; // Skip candidate
|
|
1214
|
+
}
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
**Post-Filtering (After Scoring):**
|
|
1218
|
+
|
|
1219
|
+
```typescript
|
|
1220
|
+
// Only include if score ≥ minimumCandidateScore (default: 40)
|
|
1221
|
+
if (scores.combined >= config.minimumCandidateScore) {
|
|
1222
|
+
candidates.push({ ynabTransaction, scores, matchReasons });
|
|
1223
|
+
}
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### 4.4 Guardrails & Failure Modes
|
|
1227
|
+
|
|
1228
|
+
**Auto-Match Disabled When:**
|
|
1229
|
+
|
|
1230
|
+
| Condition | Rationale |
|
|
1231
|
+
|-----------|-----------|
|
|
1232
|
+
| Amount score < 80 | Amount is most reliable signal |
|
|
1233
|
+
| Date gap > 14 days | Even with delays, 2+ weeks is suspicious |
|
|
1234
|
+
| Multiple candidates within 5 points | Ambiguous, needs human review |
|
|
1235
|
+
| Payee score < 40 AND date score < 60 | Neither secondary signal is strong |
|
|
1236
|
+
| Transaction has warnings | Parser detected ambiguity |
|
|
1237
|
+
|
|
1238
|
+
**Known Failure Modes:**
|
|
1239
|
+
|
|
1240
|
+
| Failure Mode | Mitigation Strategy |
|
|
1241
|
+
|--------------|---------------------|
|
|
1242
|
+
| Similar merchants (Starbucks #1234 vs #5678) | Require exact amount + date ≤1 day |
|
|
1243
|
+
| Recurring subscriptions | Use date as primary discriminator |
|
|
1244
|
+
| Refunds | Sign check prevents cross-matching |
|
|
1245
|
+
| Split transactions | Combination matching (future) |
|
|
1246
|
+
| Merchant name drift | Payee normalization + token_set_ratio |
|
|
1247
|
+
| Duplicate entries | Prefer uncleared; flag for review |
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
## 5. CSV Parsing Engine
|
|
1252
|
+
|
|
1253
|
+
### 5.1 Multi-Format Support
|
|
1254
|
+
|
|
1255
|
+
The CSV parser supports both **headered** and **headerless** CSV files with bank-specific presets.
|
|
1256
|
+
|
|
1257
|
+
**Auto-Detection Strategy:**
|
|
1258
|
+
|
|
1259
|
+
```mermaid
|
|
1260
|
+
graph TD
|
|
1261
|
+
A[Raw CSV] --> B[Parse First 5 Lines]
|
|
1262
|
+
B --> C{Header Match?}
|
|
1263
|
+
|
|
1264
|
+
C -->|RBC: Description 1| D[RBC Preset]
|
|
1265
|
+
C -->|Contains CAD$| E[TD Preset]
|
|
1266
|
+
C -->|No Match| F{Headerless Pattern?}
|
|
1267
|
+
|
|
1268
|
+
F -->|MM/DD/YYYY + 4+ columns| G[TD Headerless Preset]
|
|
1269
|
+
F -->|No Pattern| H[Default Auto-Detect]
|
|
1270
|
+
|
|
1271
|
+
D --> I[Apply Preset]
|
|
1272
|
+
E --> I
|
|
1273
|
+
G --> I
|
|
1274
|
+
H --> I
|
|
1275
|
+
|
|
1276
|
+
I --> J[PapaParse with Detected Format]
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
### 5.2 Date Parsing Priority
|
|
1280
|
+
|
|
1281
|
+
```mermaid
|
|
1282
|
+
graph TD
|
|
1283
|
+
A[Raw Date String] --> B{ISO Format?}
|
|
1284
|
+
B -->|YYYY-MM-DD| C[Direct Parse UTC]
|
|
1285
|
+
B -->|No| D{Format Hint from Preset?}
|
|
1286
|
+
|
|
1287
|
+
D -->|YMD| E[Apply YYYY-MM-DD Pattern]
|
|
1288
|
+
D -->|MDY| F[Apply MM/DD/YYYY Pattern]
|
|
1289
|
+
D -->|DMY| G[Apply DD/MM/YYYY Pattern]
|
|
1290
|
+
D -->|No Hint| H[chrono-node Fallback]
|
|
1291
|
+
|
|
1292
|
+
C --> I[formatLocalDate]
|
|
1293
|
+
E --> I
|
|
1294
|
+
F --> I
|
|
1295
|
+
G --> I
|
|
1296
|
+
H --> I
|
|
1297
|
+
|
|
1298
|
+
I --> J[YYYY-MM-DD String]
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
**Why This Priority?**
|
|
1302
|
+
|
|
1303
|
+
1. **ISO format first** - Unambiguous, no parsing errors
|
|
1304
|
+
2. **Preset format hint** - Handles ambiguous dates like 02/03/2025 correctly
|
|
1305
|
+
3. **chrono-node fallback** - Handles natural language and weird formats
|
|
1306
|
+
|
|
1307
|
+
### 5.3 Amount Parsing Edge Cases
|
|
1308
|
+
|
|
1309
|
+
**Supported Formats:**
|
|
1310
|
+
|
|
1311
|
+
| Input | Detected Format | Output (Milliunits) |
|
|
1312
|
+
|-------|-----------------|---------------------|
|
|
1313
|
+
| `45.23` | Standard | 45230 |
|
|
1314
|
+
| `$45.23` | USD Symbol | 45230 |
|
|
1315
|
+
| `CAD 45.23` | Currency Code | 45230 |
|
|
1316
|
+
| `1,234.56` | Thousands Separator | 1234560 |
|
|
1317
|
+
| `1.234,56` | European Format | 1234560 |
|
|
1318
|
+
| `(45.23)` | Parentheses Negative | -45230 |
|
|
1319
|
+
| `-45.23` | Explicit Negative | -45230 |
|
|
1320
|
+
|
|
1321
|
+
**Debit/Credit Column Handling:**
|
|
1322
|
+
|
|
1323
|
+
```typescript
|
|
1324
|
+
if (Math.abs(debitMilliunits) > 0 && Math.abs(creditMilliunits) > 0) {
|
|
1325
|
+
// WARNING: Both columns populated - use debit
|
|
1326
|
+
rowWarnings.push(`Both Debit (${debit}) and Credit (${credit}) have values - using Debit`);
|
|
1327
|
+
warnings.push({ row: rowNum, message: rowWarnings[0] });
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
if (Math.abs(debitMilliunits) > 0) {
|
|
1331
|
+
amountMilliunits = -Math.abs(debitMilliunits); // Debits are outflows
|
|
1332
|
+
} else if (Math.abs(creditMilliunits) > 0) {
|
|
1333
|
+
amountMilliunits = Math.abs(creditMilliunits); // Credits are inflows
|
|
1334
|
+
} else {
|
|
1335
|
+
amountMilliunits = 0;
|
|
1336
|
+
}
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
### 5.4 Security Measures
|
|
1340
|
+
|
|
1341
|
+
**Input Validation:**
|
|
1342
|
+
|
|
1343
|
+
```typescript
|
|
1344
|
+
// File size limit: 10MB default
|
|
1345
|
+
const MAX_BYTES = options.maxBytes ?? 10 * 1024 * 1024;
|
|
1346
|
+
if (content.length > MAX_BYTES) {
|
|
1347
|
+
throw new Error(`File size exceeds limit of ${Math.round(MAX_BYTES / 1024 / 1024)}MB`);
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// Row limit: 10,000 default
|
|
1351
|
+
const maxRows = options.maxRows ?? 10000;
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
**Unicode Sanitization:**
|
|
1355
|
+
|
|
1356
|
+
```typescript
|
|
1357
|
+
// Remove malicious/confusing characters
|
|
1358
|
+
rawDesc = rawDesc
|
|
1359
|
+
.replace(/[\u0000-\u001F\u007F-\u009F]/g, '') // Control chars
|
|
1360
|
+
.replace(/[\u202A-\u202E\u2066-\u2069]/g, '') // Bidirectional overrides
|
|
1361
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '') // Zero-width chars
|
|
1362
|
+
.replace(/[\u2028-\u2029]/g, '') // Line/paragraph separators
|
|
1363
|
+
.substring(0, 500); // YNAB max memo length
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
---
|
|
1367
|
+
|
|
1368
|
+
## 6. Execution Engine
|
|
1369
|
+
|
|
1370
|
+
### 6.1 Execution Phases
|
|
1371
|
+
|
|
1372
|
+
The executor implements a **4-phase execution strategy** with early termination once balance aligns.
|
|
1373
|
+
|
|
1374
|
+
**Phase Overview:**
|
|
1375
|
+
|
|
1376
|
+
| Phase | Trigger | Purpose | Balance Check |
|
|
1377
|
+
|-------|---------|---------|---------------|
|
|
1378
|
+
| 1 | `auto_create_transactions` | Create missing bank transactions | After each create |
|
|
1379
|
+
| 2 | `auto_update_cleared_status` | Mark matched YNAB txns as cleared | After each update |
|
|
1380
|
+
| 3 | `auto_unclear_missing` | Un-clear YNAB txns not on statement | After each unclear |
|
|
1381
|
+
| 4 | Balance aligned | Mark all matched txns as reconciled | N/A |
|
|
1382
|
+
|
|
1383
|
+
**Early Termination:**
|
|
1384
|
+
|
|
1385
|
+
```typescript
|
|
1386
|
+
const recordAlignmentIfNeeded = (trigger: string, { log = true } = {}) => {
|
|
1387
|
+
if (balanceAligned) return true;
|
|
1388
|
+
|
|
1389
|
+
if (Math.abs(clearedDeltaMilli) <= balanceToleranceMilli) {
|
|
1390
|
+
balanceAligned = true;
|
|
1391
|
+
if (log) {
|
|
1392
|
+
actions_taken.push({
|
|
1393
|
+
type: 'balance_checkpoint',
|
|
1394
|
+
transaction: null,
|
|
1395
|
+
reason: `Cleared delta ${deltaDisplay} within ±${toleranceDisplay} after ${trigger} - halting`,
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
return false;
|
|
1401
|
+
};
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
### 6.2 Bulk vs Sequential Operations
|
|
1405
|
+
|
|
1406
|
+
**Decision Matrix:**
|
|
1407
|
+
|
|
1408
|
+
| Scenario | Strategy | Rationale |
|
|
1409
|
+
|----------|----------|-----------|
|
|
1410
|
+
| 2+ unmatched bank txns | Bulk create | API efficiency |
|
|
1411
|
+
| 1 unmatched bank txn | Sequential create | Simpler error handling |
|
|
1412
|
+
| Bulk API failure | Sequential fallback | Resilience |
|
|
1413
|
+
| Updates (cleared, date) | Always bulk (chunked) | YNAB supports batch updates |
|
|
1414
|
+
|
|
1415
|
+
**Bulk Create Flow:**
|
|
1416
|
+
|
|
1417
|
+
```typescript
|
|
1418
|
+
// Build batches until balance aligns or all processed
|
|
1419
|
+
while (nextBankIndex < orderedUnmatchedBank.length && !balanceAligned) {
|
|
1420
|
+
const batch: PreparedBulkCreateEntry[] = [];
|
|
1421
|
+
let projectedDelta = clearedDeltaMilli;
|
|
1422
|
+
|
|
1423
|
+
// Greedy batch: add transactions until balance would align
|
|
1424
|
+
while (nextBankIndex < orderedUnmatchedBank.length) {
|
|
1425
|
+
const entry = buildPreparedEntry(orderedUnmatchedBank[nextBankIndex]);
|
|
1426
|
+
batch.push(entry);
|
|
1427
|
+
nextBankIndex += 1;
|
|
1428
|
+
projectedDelta = addMilli(projectedDelta, entry.amountMilli);
|
|
1429
|
+
|
|
1430
|
+
if (Math.abs(projectedDelta) <= balanceToleranceMilli) {
|
|
1431
|
+
break; // This batch should align balance
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Process batch in chunks of MAX_BULK_CREATE_CHUNK (100)
|
|
1436
|
+
const chunks = chunkArray(batch, MAX_BULK_CREATE_CHUNK);
|
|
1437
|
+
for (const chunk of chunks) {
|
|
1438
|
+
try {
|
|
1439
|
+
await processBulkChunk(chunk, chunkIndex);
|
|
1440
|
+
} catch (error) {
|
|
1441
|
+
// Fallback to sequential
|
|
1442
|
+
await processSequentialEntries(chunk, { chunkIndex, fallbackError: error });
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
### 6.3 Correlation Tracking
|
|
1449
|
+
|
|
1450
|
+
**Problem:** Bulk API returns transactions in arbitrary order. How to match responses to requests?
|
|
1451
|
+
|
|
1452
|
+
**Solution:** Hash-based correlation keys
|
|
1453
|
+
|
|
1454
|
+
```typescript
|
|
1455
|
+
// Generate correlation key from transaction attributes
|
|
1456
|
+
export function generateCorrelationKey(txn: CorrelationPayload): string {
|
|
1457
|
+
const key = `${txn.account_id}|${txn.date}|${txn.amount}|${normalizePayee(txn.payee_name)}`;
|
|
1458
|
+
return createHash('sha256').update(key).digest('hex').slice(0, 16);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Correlate responses
|
|
1462
|
+
export function correlateResults(
|
|
1463
|
+
requests: CorrelationPayload[],
|
|
1464
|
+
response: BulkResponse,
|
|
1465
|
+
duplicateImportIds: Set<string>,
|
|
1466
|
+
): CorrelationResult[] {
|
|
1467
|
+
const results: CorrelationResult[] = [];
|
|
1468
|
+
|
|
1469
|
+
for (let i = 0; i < requests.length; i++) {
|
|
1470
|
+
const request = requests[i];
|
|
1471
|
+
const correlationKey = generateCorrelationKey(request);
|
|
1472
|
+
|
|
1473
|
+
// Try to find in created transactions
|
|
1474
|
+
const created = response.transactions?.find(t =>
|
|
1475
|
+
generateCorrelationKey(toCorrelationPayload(t)) === correlationKey
|
|
1476
|
+
);
|
|
1477
|
+
|
|
1478
|
+
if (created) {
|
|
1479
|
+
results.push({ status: 'created', request_index: i, transaction_id: created.id, correlation_key: correlationKey });
|
|
1480
|
+
} else if (duplicateImportIds.has(request.import_id ?? '')) {
|
|
1481
|
+
results.push({ status: 'duplicate', request_index: i, correlation_key: correlationKey });
|
|
1482
|
+
} else {
|
|
1483
|
+
results.push({ status: 'failed', request_index: i, correlation_key: correlationKey, error: 'Not in response' });
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
return results;
|
|
1488
|
+
}
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
### 6.4 Error Recovery
|
|
1492
|
+
|
|
1493
|
+
**Retry Strategy:**
|
|
1494
|
+
|
|
1495
|
+
```mermaid
|
|
1496
|
+
graph TD
|
|
1497
|
+
A[Bulk Chunk Failure] --> B[normalizeYnabError]
|
|
1498
|
+
B --> C{FATAL_STATUS_CODES?}
|
|
1499
|
+
|
|
1500
|
+
C -->|400/401/403/404/429/500/503| D[Propagate Error]
|
|
1501
|
+
C -->|Other| E[Log + Continue]
|
|
1502
|
+
|
|
1503
|
+
E --> F[Sequential Fallback]
|
|
1504
|
+
F --> G[Process Individually]
|
|
1505
|
+
|
|
1506
|
+
G --> H{Individual Success?}
|
|
1507
|
+
H -->|Yes| I[Record Success]
|
|
1508
|
+
H -->|No| J{FATAL?}
|
|
1509
|
+
|
|
1510
|
+
J -->|Yes| D
|
|
1511
|
+
J -->|No| K[Record Failure + Continue]
|
|
1512
|
+
|
|
1513
|
+
I --> L[Update Metrics]
|
|
1514
|
+
K --> L
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
**Fatal vs Recoverable Errors:**
|
|
1518
|
+
|
|
1519
|
+
```typescript
|
|
1520
|
+
const FATAL_YNAB_STATUS_CODES = new Set([
|
|
1521
|
+
400, // Bad Request - malformed payload
|
|
1522
|
+
401, // Unauthorized - invalid token
|
|
1523
|
+
403, // Forbidden - insufficient permissions
|
|
1524
|
+
404, // Not Found - budget/account doesn't exist
|
|
1525
|
+
429, // Too Many Requests - rate limited
|
|
1526
|
+
500, // Internal Server Error - YNAB backend issue
|
|
1527
|
+
503, // Service Unavailable - YNAB maintenance
|
|
1528
|
+
]);
|
|
1529
|
+
```
|
|
1530
|
+
|
|
1531
|
+
---
|
|
1532
|
+
|
|
1533
|
+
## 7. Type System
|
|
1534
|
+
|
|
1535
|
+
### 7.1 Core Types
|
|
1536
|
+
|
|
1537
|
+
**Canonical Transaction Types:**
|
|
1538
|
+
|
|
1539
|
+
```typescript
|
|
1540
|
+
// File: src/types/reconciliation.ts
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* Bank transaction from CSV parsing.
|
|
1544
|
+
* CRITICAL: amount is in MILLIUNITS (integers, 1000 = $1.00)
|
|
1545
|
+
*/
|
|
1546
|
+
export interface BankTransaction {
|
|
1547
|
+
id: string; // UUID
|
|
1548
|
+
date: string; // YYYY-MM-DD
|
|
1549
|
+
amount: number; // Milliunits (integer)
|
|
1550
|
+
payee: string;
|
|
1551
|
+
memo?: string;
|
|
1552
|
+
sourceRow: number; // CSV row number (1-indexed)
|
|
1553
|
+
raw: {
|
|
1554
|
+
date: string;
|
|
1555
|
+
amount: string;
|
|
1556
|
+
description: string;
|
|
1557
|
+
};
|
|
1558
|
+
warnings?: string[];
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
/**
|
|
1562
|
+
* Normalized YNAB transaction for matching.
|
|
1563
|
+
* CRITICAL: amount is in MILLIUNITS (same as YNAB API)
|
|
1564
|
+
*/
|
|
1565
|
+
export interface NormalizedYNABTransaction {
|
|
1566
|
+
id: string;
|
|
1567
|
+
date: string; // YYYY-MM-DD
|
|
1568
|
+
amount: number; // Milliunits (integer)
|
|
1569
|
+
payee: string | null;
|
|
1570
|
+
memo: string | null;
|
|
1571
|
+
categoryName: string | null;
|
|
1572
|
+
cleared: 'cleared' | 'uncleared' | 'reconciled';
|
|
1573
|
+
approved: boolean;
|
|
1574
|
+
}
|
|
1575
|
+
```
|
|
1576
|
+
|
|
1577
|
+
**Match Result Types:**
|
|
1578
|
+
|
|
1579
|
+
```typescript
|
|
1580
|
+
// File: src/tools/reconciliation/types.ts
|
|
1581
|
+
|
|
1582
|
+
export type MatchConfidence = 'high' | 'medium' | 'low' | 'none';
|
|
1583
|
+
|
|
1584
|
+
export interface TransactionMatch {
|
|
1585
|
+
bankTransaction: BankTransaction;
|
|
1586
|
+
ynabTransaction?: YNABTransaction;
|
|
1587
|
+
candidates?: MatchCandidate[];
|
|
1588
|
+
confidence: MatchConfidence;
|
|
1589
|
+
confidenceScore: number; // 0-100
|
|
1590
|
+
matchReason: string;
|
|
1591
|
+
topConfidence?: number;
|
|
1592
|
+
actionHint?: string;
|
|
1593
|
+
recommendation?: string;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
export interface MatchCandidate {
|
|
1597
|
+
ynab_transaction: YNABTransaction;
|
|
1598
|
+
confidence: number;
|
|
1599
|
+
match_reason: string;
|
|
1600
|
+
explanation: string;
|
|
1601
|
+
}
|
|
1602
|
+
```
|
|
1603
|
+
|
|
1604
|
+
**Recommendation Types:**
|
|
1605
|
+
|
|
1606
|
+
```typescript
|
|
1607
|
+
// File: src/tools/reconciliation/types.ts
|
|
1608
|
+
|
|
1609
|
+
export type ActionableRecommendation =
|
|
1610
|
+
| CreateTransactionRecommendation
|
|
1611
|
+
| UpdateClearedRecommendation
|
|
1612
|
+
| ReviewDuplicateRecommendation
|
|
1613
|
+
| ManualReviewRecommendation;
|
|
1614
|
+
|
|
1615
|
+
export interface CreateTransactionRecommendation {
|
|
1616
|
+
id: string;
|
|
1617
|
+
action_type: 'create_transaction';
|
|
1618
|
+
priority: 'high' | 'medium' | 'low';
|
|
1619
|
+
confidence: number; // 0-1
|
|
1620
|
+
message: string;
|
|
1621
|
+
reason: string;
|
|
1622
|
+
estimated_impact: MoneyValue;
|
|
1623
|
+
account_id: string;
|
|
1624
|
+
parameters: {
|
|
1625
|
+
account_id: string;
|
|
1626
|
+
date: string;
|
|
1627
|
+
amount: number; // Milliunits
|
|
1628
|
+
payee_name: string;
|
|
1629
|
+
memo?: string;
|
|
1630
|
+
cleared: 'cleared' | 'uncleared';
|
|
1631
|
+
approved: boolean;
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
```
|
|
1635
|
+
|
|
1636
|
+
### 7.2 Analysis Result Type
|
|
1637
|
+
|
|
1638
|
+
```typescript
|
|
1639
|
+
export interface ReconciliationAnalysis {
|
|
1640
|
+
success: true;
|
|
1641
|
+
phase: 'analysis';
|
|
1642
|
+
summary: ReconciliationSummary;
|
|
1643
|
+
auto_matches: TransactionMatch[];
|
|
1644
|
+
suggested_matches: TransactionMatch[];
|
|
1645
|
+
unmatched_bank: BankTransaction[];
|
|
1646
|
+
unmatched_ynab: YNABTransaction[];
|
|
1647
|
+
balance_info: BalanceInfo;
|
|
1648
|
+
next_steps: string[];
|
|
1649
|
+
insights: ReconciliationInsight[];
|
|
1650
|
+
recommendations?: ActionableRecommendation[];
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
export interface ReconciliationSummary {
|
|
1654
|
+
statement_date_range: string;
|
|
1655
|
+
bank_transactions_count: number;
|
|
1656
|
+
ynab_transactions_count: number;
|
|
1657
|
+
auto_matched: number;
|
|
1658
|
+
suggested_matches: number;
|
|
1659
|
+
unmatched_bank: number;
|
|
1660
|
+
unmatched_ynab: number;
|
|
1661
|
+
current_cleared_balance: MoneyValue;
|
|
1662
|
+
target_statement_balance: MoneyValue;
|
|
1663
|
+
discrepancy: MoneyValue;
|
|
1664
|
+
discrepancy_explanation: string;
|
|
1665
|
+
}
|
|
1666
|
+
```
|
|
1667
|
+
|
|
1668
|
+
---
|
|
1669
|
+
|
|
1670
|
+
## 8. Integration Points
|
|
1671
|
+
|
|
1672
|
+
### 8.1 YNAB API Integration
|
|
1673
|
+
|
|
1674
|
+
**Delta Fetching:**
|
|
1675
|
+
|
|
1676
|
+
```typescript
|
|
1677
|
+
// Fetch transactions with delta support for efficiency
|
|
1678
|
+
const transactionsResult = forceFullRefresh
|
|
1679
|
+
? await deltaFetcher.fetchTransactionsByAccountFull(budget_id, account_id, since_date)
|
|
1680
|
+
: await deltaFetcher.fetchTransactionsByAccount(budget_id, account_id, since_date);
|
|
1681
|
+
|
|
1682
|
+
// Result includes metadata
|
|
1683
|
+
{
|
|
1684
|
+
data: TransactionDetail[],
|
|
1685
|
+
wasCached: boolean,
|
|
1686
|
+
usedDelta: boolean,
|
|
1687
|
+
serverKnowledge: number,
|
|
1688
|
+
}
|
|
1689
|
+
```
|
|
1690
|
+
|
|
1691
|
+
**Bulk Operations:**
|
|
1692
|
+
|
|
1693
|
+
```typescript
|
|
1694
|
+
// Create multiple transactions
|
|
1695
|
+
const response = await ynabAPI.transactions.createTransactions(budgetId, {
|
|
1696
|
+
transactions: [
|
|
1697
|
+
{ account_id, date, amount, payee_name, cleared: 'cleared', approved: true, import_id },
|
|
1698
|
+
// ... more transactions
|
|
1699
|
+
],
|
|
1700
|
+
});
|
|
1701
|
+
|
|
1702
|
+
// Update multiple transactions
|
|
1703
|
+
const response = await ynabAPI.transactions.updateTransactions(budgetId, {
|
|
1704
|
+
transactions: [
|
|
1705
|
+
{ id: txn_id_1, cleared: 'cleared' },
|
|
1706
|
+
{ id: txn_id_2, date: '2025-09-20' },
|
|
1707
|
+
// ... more updates
|
|
1708
|
+
],
|
|
1709
|
+
});
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
### 8.2 MCP Tool Interface
|
|
1713
|
+
|
|
1714
|
+
**Request Schema:**
|
|
1715
|
+
|
|
1716
|
+
```typescript
|
|
1717
|
+
export const ReconcileAccountSchema = z.object({
|
|
1718
|
+
budget_id: z.string().min(1),
|
|
1719
|
+
account_id: z.string().min(1),
|
|
1720
|
+
|
|
1721
|
+
// CSV Input (one required)
|
|
1722
|
+
csv_file_path: z.string().optional(),
|
|
1723
|
+
csv_data: z.string().optional(),
|
|
1724
|
+
|
|
1725
|
+
csv_format: z.object({
|
|
1726
|
+
date_column: z.union([z.string(), z.number()]).optional(),
|
|
1727
|
+
amount_column: z.union([z.string(), z.number()]).optional(),
|
|
1728
|
+
debit_column: z.union([z.string(), z.number()]).optional(),
|
|
1729
|
+
credit_column: z.union([z.string(), z.number()]).optional(),
|
|
1730
|
+
description_column: z.union([z.string(), z.number()]).optional(),
|
|
1731
|
+
date_format: z.string().optional(),
|
|
1732
|
+
has_header: z.boolean().optional(),
|
|
1733
|
+
delimiter: z.string().optional(),
|
|
1734
|
+
}).optional(),
|
|
1735
|
+
|
|
1736
|
+
// Statement Information
|
|
1737
|
+
statement_balance: z.number(),
|
|
1738
|
+
statement_start_date: z.string().optional(),
|
|
1739
|
+
statement_end_date: z.string().optional(),
|
|
1740
|
+
statement_date: z.string().optional(),
|
|
1741
|
+
|
|
1742
|
+
// Matching Configuration
|
|
1743
|
+
date_tolerance_days: z.number().min(0).max(7).default(7),
|
|
1744
|
+
amount_tolerance_cents: z.number().min(0).max(100).default(1),
|
|
1745
|
+
auto_match_threshold: z.number().min(0).max(100).default(85),
|
|
1746
|
+
suggestion_threshold: z.number().min(0).max(100).default(60),
|
|
1747
|
+
|
|
1748
|
+
// Automation Toggles
|
|
1749
|
+
auto_create_transactions: z.boolean().default(false),
|
|
1750
|
+
auto_update_cleared_status: z.boolean().default(false),
|
|
1751
|
+
auto_unclear_missing: z.boolean().default(true),
|
|
1752
|
+
auto_adjust_dates: z.boolean().default(false),
|
|
1753
|
+
invert_bank_amounts: z.boolean().optional(),
|
|
1754
|
+
dry_run: z.boolean().default(true),
|
|
1755
|
+
|
|
1756
|
+
// Response Options
|
|
1757
|
+
include_structured_data: z.boolean().default(false),
|
|
1758
|
+
force_full_refresh: z.boolean().default(true),
|
|
1759
|
+
});
|
|
1760
|
+
```
|
|
1761
|
+
|
|
1762
|
+
**Response Format:**
|
|
1763
|
+
|
|
1764
|
+
```typescript
|
|
1765
|
+
{
|
|
1766
|
+
content: [
|
|
1767
|
+
{
|
|
1768
|
+
type: 'text',
|
|
1769
|
+
text: responseFormatter.format({
|
|
1770
|
+
human: formatHumanReadableReport(analysis, execution),
|
|
1771
|
+
structured: { // Only if include_structured_data = true
|
|
1772
|
+
version: '2.0',
|
|
1773
|
+
analysis: ReconciliationAnalysis,
|
|
1774
|
+
execution: ExecutionResult,
|
|
1775
|
+
audit_metadata: { ... },
|
|
1776
|
+
},
|
|
1777
|
+
}),
|
|
1778
|
+
},
|
|
1779
|
+
],
|
|
1780
|
+
}
|
|
1781
|
+
```
|
|
1782
|
+
|
|
1783
|
+
---
|
|
1784
|
+
|
|
1785
|
+
## 9. Performance Characteristics
|
|
1786
|
+
|
|
1787
|
+
### 9.1 Complexity Analysis
|
|
1788
|
+
|
|
1789
|
+
| Operation | Complexity | Notes |
|
|
1790
|
+
|-----------|------------|-------|
|
|
1791
|
+
| CSV Parsing | O(N) | N = rows; PapaParse streams large files |
|
|
1792
|
+
| YNAB Normalization | O(M) | M = YNAB transactions |
|
|
1793
|
+
| Basic Matching | O(N × M) | N = bank txns, M = YNAB txns |
|
|
1794
|
+
| Payee Fuzzy Matching | O(L²) per pair | L = string length; fuzzball optimized |
|
|
1795
|
+
| Bulk Create | O(N/100) API calls | Chunked by MAX_BULK_CREATE_CHUNK |
|
|
1796
|
+
| Bulk Update | O(M/100) API calls | Chunked by MAX_BULK_UPDATE_CHUNK |
|
|
1797
|
+
|
|
1798
|
+
### 9.2 Scale Limits
|
|
1799
|
+
|
|
1800
|
+
| CSV Size | Expected Performance | Recommendations |
|
|
1801
|
+
|----------|---------------------|------------------|
|
|
1802
|
+
| <100 txns | <500ms | Default settings |
|
|
1803
|
+
| 100-500 txns | <2s | Default settings |
|
|
1804
|
+
| 500-1000 txns | 2-10s | Consider chunking by month |
|
|
1805
|
+
| >1000 txns | May timeout | Process in batches |
|
|
1806
|
+
|
|
1807
|
+
### 9.3 Optimization Strategies
|
|
1808
|
+
|
|
1809
|
+
**Current Optimizations:**
|
|
1810
|
+
|
|
1811
|
+
1. **Integer Arithmetic** - No floating-point comparisons
|
|
1812
|
+
2. **Early Termination** - Stop when balance aligns
|
|
1813
|
+
3. **Bulk Operations** - Minimize API calls
|
|
1814
|
+
4. **Delta Fetching** - Only fetch changed transactions
|
|
1815
|
+
|
|
1816
|
+
**Future Optimizations:**
|
|
1817
|
+
|
|
1818
|
+
1. **Amount Bucketing** - Index YNAB transactions by amount for O(1) lookup
|
|
1819
|
+
2. **Date Windowing** - Only compare transactions within ±14 days
|
|
1820
|
+
3. **Streaming CSV** - Parse in chunks for large files
|
|
1821
|
+
4. **Parallel Matching** - Use worker threads for CPU-bound fuzzy matching
|
|
1822
|
+
|
|
1823
|
+
### 9.4 Memory Usage
|
|
1824
|
+
|
|
1825
|
+
**Estimated Memory Footprint:**
|
|
1826
|
+
|
|
1827
|
+
```
|
|
1828
|
+
CSV (500 txns): ~100 KB raw text
|
|
1829
|
+
BankTransaction[]: ~200 KB (400 bytes/txn × 500)
|
|
1830
|
+
YNAB API (1000): ~500 KB raw JSON
|
|
1831
|
+
NormalizedYNAB[]: ~400 KB (400 bytes/txn × 1000)
|
|
1832
|
+
MatchResults[]: ~300 KB (600 bytes/match × 500)
|
|
1833
|
+
Total: ~1.5 MB for 500-txn reconciliation
|
|
1834
|
+
```
|
|
1835
|
+
|
|
1836
|
+
**Memory Safety:**
|
|
1837
|
+
|
|
1838
|
+
```typescript
|
|
1839
|
+
// Security limits in csvParser.ts
|
|
1840
|
+
const MAX_BYTES = options.maxBytes ?? 10 * 1024 * 1024; // 10MB
|
|
1841
|
+
const maxRows = options.maxRows ?? 10000;
|
|
1842
|
+
|
|
1843
|
+
// PapaParse preview limits rows in memory
|
|
1844
|
+
const parsed = Papa.parse(content, {
|
|
1845
|
+
preview: maxRows + (hasHeader ? 1 : 0),
|
|
1846
|
+
skipEmptyLines: true,
|
|
1847
|
+
});
|
|
1848
|
+
```
|
|
1849
|
+
|
|
1850
|
+
---
|
|
1851
|
+
|
|
1852
|
+
## 10. Testing Strategy
|
|
1853
|
+
|
|
1854
|
+
### 10.1 Test Coverage
|
|
1855
|
+
|
|
1856
|
+
**Unit Tests:**
|
|
1857
|
+
|
|
1858
|
+
| Component | Test File | Coverage Focus |
|
|
1859
|
+
|-----------|-----------|----------------|
|
|
1860
|
+
| csvParser | csvParser.test.ts | Format detection, amount parsing, date parsing |
|
|
1861
|
+
| matcher | matcher.test.ts | Scoring algorithm, candidate filtering, tie-breaking |
|
|
1862
|
+
| analyzer | analyzer.test.ts | Orchestration, insight generation, balance calculation |
|
|
1863
|
+
| executor | executor.test.ts | Bulk operations, correlation, error handling |
|
|
1864
|
+
| recommendationEngine | recommendationEngine.test.ts | Recommendation generation, prioritization |
|
|
1865
|
+
|
|
1866
|
+
**Integration Tests:**
|
|
1867
|
+
|
|
1868
|
+
| Scenario | Test File | Purpose |
|
|
1869
|
+
|----------|-----------|---------|
|
|
1870
|
+
| TD Bank CSV | csvParser.integration.test.ts | Real-world TD format |
|
|
1871
|
+
| RBC Debit/Credit | csvParser.integration.test.ts | Debit/credit column handling |
|
|
1872
|
+
| Bulk Create | executor.integration.test.ts | Bulk API with mocked YNAB |
|
|
1873
|
+
| End-to-End | reconciliation.e2e.test.ts | Full flow with real YNAB API |
|
|
1874
|
+
|
|
1875
|
+
### 10.2 Test Data
|
|
1876
|
+
|
|
1877
|
+
**Fixtures:**
|
|
1878
|
+
|
|
1879
|
+
```
|
|
1880
|
+
test-exports/csv/
|
|
1881
|
+
├── td-credit-card.csv # TD headerless format
|
|
1882
|
+
├── rbc-checking.csv # RBC debit/credit columns
|
|
1883
|
+
├── wealthsimple-cash.csv # Wealthsimple headered format
|
|
1884
|
+
├── scotiabank-savings.csv # Scotiabank format
|
|
1885
|
+
├── tangerine-checking.csv # Tangerine format
|
|
1886
|
+
└── edge-cases/
|
|
1887
|
+
├── european-numbers.csv # 1.234,56 format
|
|
1888
|
+
├── both-debit-credit.csv # Ambiguous columns
|
|
1889
|
+
└── malformed.csv # Missing fields, bad dates
|
|
1890
|
+
```
|
|
1891
|
+
|
|
1892
|
+
### 10.3 Accuracy Metrics
|
|
1893
|
+
|
|
1894
|
+
**Evaluation Dataset:**
|
|
1895
|
+
|
|
1896
|
+
```typescript
|
|
1897
|
+
// test-exports/csv/labeled/
|
|
1898
|
+
{
|
|
1899
|
+
"csv_file": "td-credit-card.csv",
|
|
1900
|
+
"ground_truth": [
|
|
1901
|
+
{
|
|
1902
|
+
"bank_row": 2,
|
|
1903
|
+
"ynab_transaction_id": "abc123",
|
|
1904
|
+
"expected_confidence": "high",
|
|
1905
|
+
"notes": "Exact amount and payee match"
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
"bank_row": 3,
|
|
1909
|
+
"ynab_transaction_id": null,
|
|
1910
|
+
"expected_confidence": "none",
|
|
1911
|
+
"notes": "New transaction, should create"
|
|
1912
|
+
}
|
|
1913
|
+
]
|
|
1914
|
+
}
|
|
1915
|
+
```
|
|
1916
|
+
|
|
1917
|
+
**Benchmark Metrics:**
|
|
1918
|
+
|
|
1919
|
+
```bash
|
|
1920
|
+
npm run benchmark:reconciliation -- --dataset=test-exports/csv/labeled/
|
|
1921
|
+
|
|
1922
|
+
Output:
|
|
1923
|
+
Auto-Match Precision: 95.2% (20/21 correct)
|
|
1924
|
+
Auto-Match Recall: 90.9% (20/22 true matches)
|
|
1925
|
+
Overall Match Rate: 96.0% (48/50 matched at any level)
|
|
1926
|
+
False Positive Rate: 4.8% (1/21 incorrect)
|
|
1927
|
+
P95 Latency: 1.2s (500-txn CSV)
|
|
1928
|
+
```
|
|
1929
|
+
|
|
1930
|
+
---
|
|
1931
|
+
|
|
1932
|
+
## Appendix A: Sequence Diagrams
|
|
1933
|
+
|
|
1934
|
+
### A.1 Create Transaction Flow
|
|
1935
|
+
|
|
1936
|
+
```mermaid
|
|
1937
|
+
sequenceDiagram
|
|
1938
|
+
participant E as executor.ts
|
|
1939
|
+
participant B as BankTransaction[]
|
|
1940
|
+
participant C as Correlation
|
|
1941
|
+
participant Y as YNAB API
|
|
1942
|
+
|
|
1943
|
+
E->>B: Filter unmatched_bank
|
|
1944
|
+
B->>E: orderedUnmatchedBank (sorted by date desc)
|
|
1945
|
+
|
|
1946
|
+
loop For each batch until balance aligns
|
|
1947
|
+
E->>E: Build batch (greedy until projected balance aligns)
|
|
1948
|
+
E->>E: Chunk batch by MAX_BULK_CREATE_CHUNK (100)
|
|
1949
|
+
|
|
1950
|
+
loop For each chunk
|
|
1951
|
+
E->>C: Generate import_id for each txn
|
|
1952
|
+
C-->>E: import_id = YNAB:bulk:{hash}
|
|
1953
|
+
|
|
1954
|
+
E->>C: Generate correlation_key
|
|
1955
|
+
C-->>E: correlation_key = {hash}
|
|
1956
|
+
|
|
1957
|
+
E->>Y: POST /budgets/{id}/transactions (bulk)
|
|
1958
|
+
|
|
1959
|
+
alt Bulk Success
|
|
1960
|
+
Y-->>E: BulkResponse { transactions[], duplicate_import_ids[] }
|
|
1961
|
+
E->>C: correlateResults(requests, response, duplicates)
|
|
1962
|
+
|
|
1963
|
+
loop For each correlated result
|
|
1964
|
+
alt Status: created
|
|
1965
|
+
C-->>E: { status: 'created', transaction_id, correlation_key }
|
|
1966
|
+
E->>E: Record success action
|
|
1967
|
+
E->>E: Update clearedDeltaMilli
|
|
1968
|
+
else Status: duplicate
|
|
1969
|
+
C-->>E: { status: 'duplicate', correlation_key }
|
|
1970
|
+
E->>E: Record duplicate warning
|
|
1971
|
+
E->>E: Increment duplicates_detected
|
|
1972
|
+
else Status: failed
|
|
1973
|
+
C-->>E: { status: 'failed', correlation_key, error }
|
|
1974
|
+
E->>E: Record failure action
|
|
1975
|
+
E->>E: Increment transaction_failures
|
|
1976
|
+
end
|
|
1977
|
+
end
|
|
1978
|
+
|
|
1979
|
+
E->>E: Check if balance aligned
|
|
1980
|
+
else Bulk Failure
|
|
1981
|
+
Y-->>E: Error response
|
|
1982
|
+
E->>E: Increment bulk_chunk_failures
|
|
1983
|
+
E->>E: Log fallback action
|
|
1984
|
+
|
|
1985
|
+
loop For each txn in chunk (Sequential Fallback)
|
|
1986
|
+
E->>Y: POST /budgets/{id}/transactions (single)
|
|
1987
|
+
|
|
1988
|
+
alt Success
|
|
1989
|
+
Y-->>E: { transaction: {...} }
|
|
1990
|
+
E->>E: Record success action
|
|
1991
|
+
E->>E: Update clearedDeltaMilli
|
|
1992
|
+
else Failure
|
|
1993
|
+
Y-->>E: Error
|
|
1994
|
+
E->>E: normalizeYnabError
|
|
1995
|
+
|
|
1996
|
+
alt Fatal Error (400/401/403/404/429/500/503)
|
|
1997
|
+
E->>E: Throw error (halt execution)
|
|
1998
|
+
else Recoverable Error
|
|
1999
|
+
E->>E: Record failure action
|
|
2000
|
+
E->>E: Increment transaction_failures
|
|
2001
|
+
E->>E: Continue to next txn
|
|
2002
|
+
end
|
|
2003
|
+
end
|
|
2004
|
+
|
|
2005
|
+
E->>E: Check if balance aligned
|
|
2006
|
+
end
|
|
2007
|
+
end
|
|
2008
|
+
end
|
|
2009
|
+
end
|
|
2010
|
+
```
|
|
2011
|
+
|
|
2012
|
+
### A.2 Match Scoring Flow
|
|
2013
|
+
|
|
2014
|
+
```mermaid
|
|
2015
|
+
sequenceDiagram
|
|
2016
|
+
participant M as matcher.ts
|
|
2017
|
+
participant B as BankTransaction
|
|
2018
|
+
participant Y as NormalizedYNABTransaction[]
|
|
2019
|
+
participant F as fuzzball
|
|
2020
|
+
|
|
2021
|
+
M->>Y: Loop through YNAB transactions
|
|
2022
|
+
|
|
2023
|
+
loop For each YNAB transaction
|
|
2024
|
+
M->>M: Check if already used
|
|
2025
|
+
|
|
2026
|
+
alt Already matched
|
|
2027
|
+
M->>M: Skip candidate
|
|
2028
|
+
else Not matched
|
|
2029
|
+
M->>M: Sign check (bankSign === ynabSign)
|
|
2030
|
+
|
|
2031
|
+
alt Signs differ
|
|
2032
|
+
M->>M: Skip candidate
|
|
2033
|
+
else Signs match
|
|
2034
|
+
M->>M: Calculate amountDiff = |bank - ynab|
|
|
2035
|
+
|
|
2036
|
+
alt amountDiff > tolerance
|
|
2037
|
+
M->>M: Skip candidate
|
|
2038
|
+
else Within tolerance
|
|
2039
|
+
M->>M: Calculate amount score (0-100)
|
|
2040
|
+
|
|
2041
|
+
Note over M: Amount Scoring:<br/>diff === 0 → 100<br/>diff <= tolerance → 95<br/>diff <= $1 → 80-100<br/>else → 60-0
|
|
2042
|
+
|
|
2043
|
+
M->>M: Calculate date score (0-100)
|
|
2044
|
+
|
|
2045
|
+
Note over M: Date Scoring:<br/>same day → 100<br/>1 day → 95<br/>≤ tolerance → 90-50<br/>else → 50-0
|
|
2046
|
+
|
|
2047
|
+
M->>F: calculatePayeeScore(bankPayee, ynabPayee)
|
|
2048
|
+
|
|
2049
|
+
F->>F: token_set_ratio
|
|
2050
|
+
F->>F: token_sort_ratio
|
|
2051
|
+
F->>F: partial_ratio
|
|
2052
|
+
F->>F: WRatio
|
|
2053
|
+
|
|
2054
|
+
F-->>M: max(scores)
|
|
2055
|
+
|
|
2056
|
+
M->>M: combined = amount×0.5 + date×0.15 + payee×0.35
|
|
2057
|
+
|
|
2058
|
+
M->>M: Apply bonuses
|
|
2059
|
+
|
|
2060
|
+
Note over M: Bonuses:<br/>amount === 100 → +10<br/>date === 100 → +5<br/>payee >= 95 → +10
|
|
2061
|
+
|
|
2062
|
+
M->>M: combined = min(100, combined + bonuses)
|
|
2063
|
+
|
|
2064
|
+
alt combined >= minimumCandidateScore (40)
|
|
2065
|
+
M->>M: Add to candidates[]
|
|
2066
|
+
else Below threshold
|
|
2067
|
+
M->>M: Skip candidate
|
|
2068
|
+
end
|
|
2069
|
+
end
|
|
2070
|
+
end
|
|
2071
|
+
end
|
|
2072
|
+
end
|
|
2073
|
+
|
|
2074
|
+
M->>M: Sort candidates by combined score (desc)
|
|
2075
|
+
M->>M: Tie-break: prefer uncleared, then closer date
|
|
2076
|
+
|
|
2077
|
+
alt Top candidate >= autoMatchThreshold (85)
|
|
2078
|
+
M-->>M: confidence = 'high'
|
|
2079
|
+
M->>M: Mark YNAB transaction as used
|
|
2080
|
+
else Top >= suggestedMatchThreshold (60)
|
|
2081
|
+
M-->>M: confidence = 'medium'
|
|
2082
|
+
else Top >= minimumCandidateScore (40)
|
|
2083
|
+
M-->>M: confidence = 'low'
|
|
2084
|
+
else No candidates
|
|
2085
|
+
M-->>M: confidence = 'none'
|
|
2086
|
+
end
|
|
2087
|
+
|
|
2088
|
+
M-->>M: Return MatchResult { bankTransaction, bestMatch, candidates, confidence, confidenceScore }
|
|
2089
|
+
```
|
|
2090
|
+
|
|
2091
|
+
---
|
|
2092
|
+
|
|
2093
|
+
## Appendix B: Configuration Examples
|
|
2094
|
+
|
|
2095
|
+
### B.1 Conservative Matching
|
|
2096
|
+
|
|
2097
|
+
```json
|
|
2098
|
+
{
|
|
2099
|
+
"date_tolerance_days": 3,
|
|
2100
|
+
"amount_tolerance_cents": 0,
|
|
2101
|
+
"auto_match_threshold": 95,
|
|
2102
|
+
"suggestion_threshold": 75,
|
|
2103
|
+
"auto_create_transactions": false,
|
|
2104
|
+
"auto_update_cleared_status": false,
|
|
2105
|
+
"dry_run": true
|
|
2106
|
+
}
|
|
2107
|
+
```
|
|
2108
|
+
|
|
2109
|
+
### B.2 Aggressive Matching
|
|
2110
|
+
|
|
2111
|
+
```json
|
|
2112
|
+
{
|
|
2113
|
+
"date_tolerance_days": 14,
|
|
2114
|
+
"amount_tolerance_cents": 5,
|
|
2115
|
+
"auto_match_threshold": 75,
|
|
2116
|
+
"suggestion_threshold": 50,
|
|
2117
|
+
"auto_create_transactions": true,
|
|
2118
|
+
"auto_update_cleared_status": true,
|
|
2119
|
+
"auto_unclear_missing": true,
|
|
2120
|
+
"dry_run": false
|
|
2121
|
+
}
|
|
2122
|
+
```
|
|
2123
|
+
|
|
2124
|
+
### B.3 Manual Review Only
|
|
2125
|
+
|
|
2126
|
+
```json
|
|
2127
|
+
{
|
|
2128
|
+
"auto_create_transactions": false,
|
|
2129
|
+
"auto_update_cleared_status": false,
|
|
2130
|
+
"auto_unclear_missing": false,
|
|
2131
|
+
"dry_run": true,
|
|
2132
|
+
"include_structured_data": true
|
|
2133
|
+
}
|
|
2134
|
+
```
|
|
2135
|
+
|
|
2136
|
+
---
|
|
2137
|
+
|
|
2138
|
+
## Appendix C: Troubleshooting
|
|
2139
|
+
|
|
2140
|
+
### C.1 Common Issues
|
|
2141
|
+
|
|
2142
|
+
| Issue | Likely Cause | Solution |
|
|
2143
|
+
|-------|-------------|----------|
|
|
2144
|
+
| Low match rate (<50%) | CSV format not detected | Specify `csv_format.preset` explicitly |
|
|
2145
|
+
| All matches "none" | Sign inversion needed | Set `invert_bank_amounts: true` |
|
|
2146
|
+
| Date parsing errors | Ambiguous date format | Specify `csv_format.date_format` |
|
|
2147
|
+
| Amount parsing errors | European number format | Parser auto-detects, check warnings |
|
|
2148
|
+
| Balance never aligns | Missing transactions | Check `unmatched_bank` and `unmatched_ynab` |
|
|
2149
|
+
| Bulk create failures | Import ID collisions | Check `bulk_operation_details.duplicates_detected` |
|
|
2150
|
+
|
|
2151
|
+
### C.2 Diagnostic Output
|
|
2152
|
+
|
|
2153
|
+
```typescript
|
|
2154
|
+
{
|
|
2155
|
+
include_diagnostics: true
|
|
2156
|
+
}
|
|
2157
|
+
```
|
|
2158
|
+
|
|
2159
|
+
Returns:
|
|
2160
|
+
|
|
2161
|
+
```json
|
|
2162
|
+
{
|
|
2163
|
+
"diagnostics": {
|
|
2164
|
+
"csvParsing": {
|
|
2165
|
+
"detectedDelimiter": ",",
|
|
2166
|
+
"detectedColumns": ["Date", "Description", "Amount"],
|
|
2167
|
+
"totalRows": 50,
|
|
2168
|
+
"validRows": 48,
|
|
2169
|
+
"errors": [
|
|
2170
|
+
{ "row": 5, "field": "date", "message": "Could not parse date: '99/99/9999'", "rawValue": "99/99/9999" }
|
|
2171
|
+
],
|
|
2172
|
+
"warnings": [
|
|
2173
|
+
{ "row": 12, "message": "Both Debit ($50.00) and Credit ($25.00) have values - using Debit" }
|
|
2174
|
+
]
|
|
2175
|
+
},
|
|
2176
|
+
"matchingDetails": [
|
|
2177
|
+
{
|
|
2178
|
+
"bankTxn": { "date": "2025-09-15", "amount": -45230, "payee": "Shell Gas" },
|
|
2179
|
+
"bestMatch": {
|
|
2180
|
+
"ynabTxn": { "date": "2025-09-15", "amount": -45230, "payee": "Shell" },
|
|
2181
|
+
"scores": { "amount": 100, "date": 100, "payee": 85, "combined": 100 }
|
|
2182
|
+
},
|
|
2183
|
+
"confidence": "high"
|
|
2184
|
+
}
|
|
2185
|
+
]
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
```
|
|
2189
|
+
|
|
2190
|
+
---
|
|
2191
|
+
|
|
2192
|
+
## Appendix D: Future Enhancements
|
|
2193
|
+
|
|
2194
|
+
### D.1 Roadmap
|
|
2195
|
+
|
|
2196
|
+
| Priority | Feature | Description | Complexity |
|
|
2197
|
+
|----------|---------|-------------|------------|
|
|
2198
|
+
| P0 | Split Transaction Detection | Match 1 bank txn to multiple YNAB txns | High |
|
|
2199
|
+
| P1 | Merchant Learning | Cache successful payee mappings per user | Medium |
|
|
2200
|
+
| P1 | Adaptive Thresholds | Learn from user confirmations/rejections | Medium |
|
|
2201
|
+
| P2 | Vector Embeddings | Semantic merchant matching with embeddings | High |
|
|
2202
|
+
| P2 | Recurring Pattern Detection | Use historical patterns to boost confidence | Medium |
|
|
2203
|
+
| P3 | Multi-Currency Support | Handle FX transactions and conversions | High |
|
|
2204
|
+
|
|
2205
|
+
### D.2 Split Transaction Detection
|
|
2206
|
+
|
|
2207
|
+
**Design Sketch:**
|
|
2208
|
+
|
|
2209
|
+
```typescript
|
|
2210
|
+
// Detect when one bank transaction = multiple YNAB transactions
|
|
2211
|
+
function findCombinationMatches(
|
|
2212
|
+
bankTxn: BankTransaction,
|
|
2213
|
+
ynabTransactions: NormalizedYNABTransaction[],
|
|
2214
|
+
maxCombinationSize: number = 3,
|
|
2215
|
+
): CombinationMatch[] {
|
|
2216
|
+
// Generate all combinations of size 2 to maxCombinationSize
|
|
2217
|
+
const combinations = generateCombinations(ynabTransactions, maxCombinationSize);
|
|
2218
|
+
|
|
2219
|
+
for (const combo of combinations) {
|
|
2220
|
+
const totalAmount = combo.reduce((sum, txn) => sum + txn.amount, 0);
|
|
2221
|
+
|
|
2222
|
+
if (Math.abs(totalAmount - bankTxn.amount) <= AMOUNT_TOLERANCE) {
|
|
2223
|
+
// Found matching combination
|
|
2224
|
+
return {
|
|
2225
|
+
bankTransaction: bankTxn,
|
|
2226
|
+
ynabTransactions: combo,
|
|
2227
|
+
confidence: calculateCombinationConfidence(combo, bankTxn),
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
return [];
|
|
2233
|
+
}
|
|
2234
|
+
```
|
|
2235
|
+
|
|
2236
|
+
---
|
|
2237
|
+
|
|
2238
|
+
**End of Document**
|
|
2239
|
+
|
|
2240
|
+
---
|
|
2241
|
+
|
|
2242
|
+
## Document Metadata
|
|
2243
|
+
|
|
2244
|
+
- **File Path:** `C:\Users\ksutk\projects\ynab-mcpb\docs\technical\reconciliation-system-architecture.md`
|
|
2245
|
+
- **Related Files:**
|
|
2246
|
+
- Implementation: `C:\Users\ksutk\projects\ynab-mcpb\src\tools\reconciliation\`
|
|
2247
|
+
- Existing Docs: `C:\Users\ksutk\projects\ynab-mcpb\docs\reconciliation-flow.md`
|
|
2248
|
+
- Design Doc: `C:\Users\ksutk\projects\ynab-mcpb\docs\plans\reconciliation-v2-redesign.md`
|
|
2249
|
+
- **Version:** 2.0
|
|
2250
|
+
- **Status:** Active Implementation
|
|
2251
|
+
- **Last Verified:** 2025-11-30
|