@dizzlkheinz/ynab-mcpb 0.26.3 → 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 +13 -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 +41 -790
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +13 -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 +17 -84
|
@@ -4,335 +4,169 @@ import {
|
|
|
4
4
|
ReconcileAccountOutputSchema,
|
|
5
5
|
} from "../reconciliationOutputs.js";
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
describe("ReconcileAccountOutputSchema - discrepancy_direction validation", () => {
|
|
16
|
-
const makeMoneyValue = (dollarAmount: number) => ({
|
|
17
|
-
value_milliunits: Math.round(dollarAmount * 1000),
|
|
18
|
-
value: dollarAmount,
|
|
19
|
-
value_display: `$${Math.abs(dollarAmount).toFixed(2)}`,
|
|
20
|
-
currency: "USD",
|
|
21
|
-
direction: (dollarAmount > 0
|
|
22
|
-
? "credit"
|
|
23
|
-
: dollarAmount < 0
|
|
24
|
-
? "debit"
|
|
25
|
-
: "balanced") as "credit" | "debit" | "balanced",
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const createMinimalStructuredOutput = (
|
|
29
|
-
discrepancyAmount: number,
|
|
30
|
-
direction: "balanced" | "ynab_higher" | "bank_higher",
|
|
31
|
-
) => ({
|
|
32
|
-
human: "Reconciliation complete",
|
|
33
|
-
structured: {
|
|
34
|
-
version: "1.0.0",
|
|
35
|
-
schema_url: "https://example.com/schema",
|
|
36
|
-
generated_at: "2025-11-18T10:00:00Z",
|
|
37
|
-
account: {
|
|
38
|
-
id: "acct-123",
|
|
39
|
-
name: "Checking",
|
|
40
|
-
},
|
|
41
|
-
summary: {
|
|
42
|
-
statement_date_range: "2025-10-01 to 2025-10-31",
|
|
43
|
-
bank_transactions_count: 50,
|
|
44
|
-
ynab_transactions_count: 50,
|
|
45
|
-
auto_matched: 50,
|
|
46
|
-
suggested_matches: 0,
|
|
47
|
-
unmatched_bank: 0,
|
|
48
|
-
unmatched_ynab: 0,
|
|
49
|
-
current_cleared_balance: makeMoneyValue(1000),
|
|
50
|
-
target_statement_balance: makeMoneyValue(1000 + discrepancyAmount),
|
|
51
|
-
discrepancy: makeMoneyValue(discrepancyAmount),
|
|
52
|
-
discrepancy_explanation: "Test",
|
|
53
|
-
},
|
|
54
|
-
balance: {
|
|
55
|
-
current_cleared: makeMoneyValue(1000),
|
|
56
|
-
current_uncleared: makeMoneyValue(0),
|
|
57
|
-
current_total: makeMoneyValue(1000),
|
|
58
|
-
target_statement: makeMoneyValue(1000 + discrepancyAmount),
|
|
59
|
-
discrepancy: makeMoneyValue(discrepancyAmount),
|
|
60
|
-
on_track: Math.abs(discrepancyAmount) < 0.01,
|
|
61
|
-
discrepancy_direction: direction,
|
|
7
|
+
describe("ReconcileAccountOutputSchema", () => {
|
|
8
|
+
it("accepts valid unmatched_only structured output", () => {
|
|
9
|
+
const output = {
|
|
10
|
+
human: "Reconciliation complete",
|
|
11
|
+
structured: {
|
|
12
|
+
unmatched_bank: [],
|
|
13
|
+
unmatched_ynab: [],
|
|
14
|
+
suggestions: [],
|
|
62
15
|
},
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
auto: [],
|
|
67
|
-
suggested: [],
|
|
68
|
-
},
|
|
69
|
-
unmatched: {
|
|
70
|
-
bank: [],
|
|
71
|
-
ynab: [],
|
|
72
|
-
},
|
|
73
|
-
},
|
|
16
|
+
};
|
|
17
|
+
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
18
|
+
expect(result.success).toBe(true);
|
|
74
19
|
});
|
|
75
20
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
expect(result.success).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should accept direction "balanced" when amount is 0.001', () => {
|
|
84
|
-
const output = createMinimalStructuredOutput(0.001, "balanced");
|
|
85
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
86
|
-
expect(result.success).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('should accept direction "balanced" when amount is -0.009', () => {
|
|
90
|
-
const output = createMinimalStructuredOutput(-0.009, "balanced");
|
|
91
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
92
|
-
expect(result.success).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('should reject direction "ynab_higher" when amount is 0', () => {
|
|
96
|
-
const output = createMinimalStructuredOutput(0, "ynab_higher");
|
|
97
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
98
|
-
expect(result.success).toBe(false);
|
|
99
|
-
if (!result.success) {
|
|
100
|
-
expect(result.error.issues[0]?.path).toEqual([
|
|
101
|
-
"balance",
|
|
102
|
-
"discrepancy_direction",
|
|
103
|
-
]);
|
|
104
|
-
expect(result.error.issues[0]?.message).toContain(
|
|
105
|
-
"Discrepancy direction mismatch",
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should reject direction "bank_higher" when amount is 0.005', () => {
|
|
111
|
-
const output = createMinimalStructuredOutput(0.005, "bank_higher");
|
|
112
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
113
|
-
expect(result.success).toBe(false);
|
|
114
|
-
if (!result.success) {
|
|
115
|
-
expect(result.error.issues[0]?.path).toEqual([
|
|
116
|
-
"balance",
|
|
117
|
-
"discrepancy_direction",
|
|
118
|
-
]);
|
|
119
|
-
}
|
|
120
|
-
});
|
|
21
|
+
it("rejects output without structured field", () => {
|
|
22
|
+
const output = { human: "Reconciliation complete" };
|
|
23
|
+
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
24
|
+
expect(result.success).toBe(false);
|
|
121
25
|
});
|
|
122
26
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
it('should reject direction "balanced" when amount is 100', () => {
|
|
137
|
-
const output = createMinimalStructuredOutput(100, "balanced");
|
|
138
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
139
|
-
expect(result.success).toBe(false);
|
|
140
|
-
if (!result.success) {
|
|
141
|
-
expect(result.error.issues[0]?.path).toEqual([
|
|
142
|
-
"balance",
|
|
143
|
-
"discrepancy_direction",
|
|
144
|
-
]);
|
|
145
|
-
expect(result.error.issues[0]?.message).toContain(
|
|
146
|
-
"Discrepancy direction mismatch",
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('should reject direction "bank_higher" when amount is 50', () => {
|
|
152
|
-
const output = createMinimalStructuredOutput(50, "bank_higher");
|
|
153
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
154
|
-
expect(result.success).toBe(false);
|
|
155
|
-
if (!result.success) {
|
|
156
|
-
expect(result.error.issues[0]?.path).toEqual([
|
|
157
|
-
"balance",
|
|
158
|
-
"discrepancy_direction",
|
|
159
|
-
]);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
27
|
+
it("rejects output with unknown fields in structured", () => {
|
|
28
|
+
const output = {
|
|
29
|
+
human: "Reconciliation complete",
|
|
30
|
+
structured: {
|
|
31
|
+
unmatched_bank: [],
|
|
32
|
+
unmatched_ynab: [],
|
|
33
|
+
suggestions: [],
|
|
34
|
+
extra_field: "not allowed",
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
38
|
+
expect(result.success).toBe(false);
|
|
162
39
|
});
|
|
163
40
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
"balance",
|
|
184
|
-
"discrepancy_direction",
|
|
185
|
-
]);
|
|
186
|
-
expect(result.error.issues[0]?.message).toContain(
|
|
187
|
-
"Discrepancy direction mismatch",
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should reject direction "ynab_higher" when amount is -50', () => {
|
|
193
|
-
const output = createMinimalStructuredOutput(-50, "ynab_higher");
|
|
194
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
195
|
-
expect(result.success).toBe(false);
|
|
196
|
-
if (!result.success) {
|
|
197
|
-
expect(result.error.issues[0]?.path).toEqual([
|
|
198
|
-
"balance",
|
|
199
|
-
"discrepancy_direction",
|
|
200
|
-
]);
|
|
201
|
-
}
|
|
202
|
-
});
|
|
41
|
+
it("accepts output with execution_summary", () => {
|
|
42
|
+
const output = {
|
|
43
|
+
human: "Reconciliation complete",
|
|
44
|
+
structured: {
|
|
45
|
+
unmatched_bank: [],
|
|
46
|
+
unmatched_ynab: [],
|
|
47
|
+
suggestions: [],
|
|
48
|
+
execution_summary: {
|
|
49
|
+
transactions_created: 5,
|
|
50
|
+
transactions_updated: 2,
|
|
51
|
+
dates_adjusted: 0,
|
|
52
|
+
dry_run: false,
|
|
53
|
+
balance_status: "balanced",
|
|
54
|
+
recommendations: ["Review matched transactions"],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
59
|
+
expect(result.success).toBe(true);
|
|
203
60
|
});
|
|
204
61
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should reject "balanced" for exactly 0.01', () => {
|
|
219
|
-
const output = createMinimalStructuredOutput(0.01, "balanced");
|
|
220
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
221
|
-
expect(result.success).toBe(false);
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('should reject "balanced" for exactly -0.01', () => {
|
|
225
|
-
const output = createMinimalStructuredOutput(-0.01, "balanced");
|
|
226
|
-
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
227
|
-
expect(result.success).toBe(false);
|
|
228
|
-
});
|
|
62
|
+
it("accepts output without execution_summary", () => {
|
|
63
|
+
const output = {
|
|
64
|
+
human: "Reconciliation complete",
|
|
65
|
+
structured: {
|
|
66
|
+
unmatched_bank: [],
|
|
67
|
+
unmatched_ynab: [],
|
|
68
|
+
suggestions: [],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
const result = ReconcileAccountOutputSchema.safeParse(output);
|
|
72
|
+
expect(result.success).toBe(true);
|
|
229
73
|
});
|
|
74
|
+
});
|
|
230
75
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
76
|
+
describe("MoneyValueSchema - non-finite value validation", () => {
|
|
77
|
+
it("should reject NaN value", () => {
|
|
78
|
+
const invalid = {
|
|
79
|
+
value_milliunits: 0,
|
|
80
|
+
value: Number.NaN,
|
|
81
|
+
value_display: "$NaN",
|
|
82
|
+
currency: "USD",
|
|
83
|
+
direction: "balanced",
|
|
84
|
+
};
|
|
85
|
+
const result = MoneyValueSchema.safeParse(invalid);
|
|
86
|
+
expect(result.success).toBe(false);
|
|
87
|
+
if (!result.success) {
|
|
88
|
+
expect(result.error.issues[0]?.path).toEqual(["value"]);
|
|
89
|
+
}
|
|
239
90
|
});
|
|
240
91
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("should reject positive Infinity value", () => {
|
|
258
|
-
const invalid = {
|
|
259
|
-
value_milliunits: 0,
|
|
260
|
-
value: Number.POSITIVE_INFINITY,
|
|
261
|
-
value_display: "$Infinity",
|
|
262
|
-
currency: "USD",
|
|
263
|
-
direction: "credit",
|
|
264
|
-
};
|
|
265
|
-
const result = MoneyValueSchema.safeParse(invalid);
|
|
266
|
-
expect(result.success).toBe(false);
|
|
267
|
-
if (!result.success) {
|
|
268
|
-
expect(result.error.issues[0]?.path).toEqual(["value"]);
|
|
269
|
-
}
|
|
270
|
-
});
|
|
92
|
+
it("should reject positive Infinity value", () => {
|
|
93
|
+
const invalid = {
|
|
94
|
+
value_milliunits: 0,
|
|
95
|
+
value: Number.POSITIVE_INFINITY,
|
|
96
|
+
value_display: "$Infinity",
|
|
97
|
+
currency: "USD",
|
|
98
|
+
direction: "credit",
|
|
99
|
+
};
|
|
100
|
+
const result = MoneyValueSchema.safeParse(invalid);
|
|
101
|
+
expect(result.success).toBe(false);
|
|
102
|
+
if (!result.success) {
|
|
103
|
+
expect(result.error.issues[0]?.path).toEqual(["value"]);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
271
106
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
107
|
+
it("should reject negative Infinity value", () => {
|
|
108
|
+
const invalid = {
|
|
109
|
+
value_milliunits: 0,
|
|
110
|
+
value: Number.NEGATIVE_INFINITY,
|
|
111
|
+
value_display: "-$Infinity",
|
|
112
|
+
currency: "USD",
|
|
113
|
+
direction: "debit",
|
|
114
|
+
};
|
|
115
|
+
const result = MoneyValueSchema.safeParse(invalid);
|
|
116
|
+
expect(result.success).toBe(false);
|
|
117
|
+
if (!result.success) {
|
|
118
|
+
expect(result.error.issues[0]?.path).toEqual(["value"]);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
286
121
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
122
|
+
it("should reject non-integer value_milliunits", () => {
|
|
123
|
+
const invalid = {
|
|
124
|
+
value_milliunits: 25.5,
|
|
125
|
+
value: 0.0255,
|
|
126
|
+
value_display: "$0.03",
|
|
127
|
+
currency: "USD",
|
|
128
|
+
direction: "credit",
|
|
129
|
+
};
|
|
130
|
+
const result = MoneyValueSchema.safeParse(invalid);
|
|
131
|
+
expect(result.success).toBe(false);
|
|
132
|
+
if (!result.success) {
|
|
133
|
+
expect(result.error.issues[0]?.path).toEqual(["value_milliunits"]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
301
136
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
137
|
+
it("should accept finite positive amounts", () => {
|
|
138
|
+
const valid = {
|
|
139
|
+
value_milliunits: 25500,
|
|
140
|
+
value: 25.5,
|
|
141
|
+
value_display: "$25.50",
|
|
142
|
+
currency: "USD",
|
|
143
|
+
direction: "credit",
|
|
144
|
+
};
|
|
145
|
+
const result = MoneyValueSchema.safeParse(valid);
|
|
146
|
+
expect(result.success).toBe(true);
|
|
147
|
+
});
|
|
313
148
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
149
|
+
it("should accept finite negative amounts", () => {
|
|
150
|
+
const valid = {
|
|
151
|
+
value_milliunits: -25500,
|
|
152
|
+
value: -25.5,
|
|
153
|
+
value_display: "-$25.50",
|
|
154
|
+
currency: "USD",
|
|
155
|
+
direction: "debit",
|
|
156
|
+
};
|
|
157
|
+
const result = MoneyValueSchema.safeParse(valid);
|
|
158
|
+
expect(result.success).toBe(true);
|
|
159
|
+
});
|
|
325
160
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
});
|
|
161
|
+
it("should accept zero", () => {
|
|
162
|
+
const valid = {
|
|
163
|
+
value_milliunits: 0,
|
|
164
|
+
value: 0,
|
|
165
|
+
value_display: "$0.00",
|
|
166
|
+
currency: "USD",
|
|
167
|
+
direction: "balanced",
|
|
168
|
+
};
|
|
169
|
+
const result = MoneyValueSchema.safeParse(valid);
|
|
170
|
+
expect(result.success).toBe(true);
|
|
337
171
|
});
|
|
338
172
|
});
|
|
@@ -723,41 +723,25 @@ export const CsvFormatMetadataSchema = z.object({
|
|
|
723
723
|
|
|
724
724
|
export type CsvFormatMetadata = z.infer<typeof CsvFormatMetadataSchema>;
|
|
725
725
|
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
name: z.string().optional(),
|
|
734
|
-
}),
|
|
735
|
-
summary: ReconciliationSummarySchema,
|
|
736
|
-
balance: BalanceInfoSchema.extend({
|
|
737
|
-
discrepancy_direction: z.enum(["balanced", "ynab_higher", "bank_higher"]),
|
|
738
|
-
}),
|
|
739
|
-
insights: z.array(ReconciliationInsightSchema),
|
|
740
|
-
next_steps: z.array(z.string()),
|
|
741
|
-
matches: z.object({
|
|
742
|
-
auto: z.array(TransactionMatchSchema),
|
|
743
|
-
suggested: z.array(TransactionMatchSchema),
|
|
744
|
-
}),
|
|
745
|
-
unmatched: z.object({
|
|
746
|
-
bank: z.array(BankTransactionSchema),
|
|
747
|
-
ynab: z.array(YNABTransactionSimpleSchema),
|
|
748
|
-
ynab_outside_date_range: z.array(YNABTransactionSimpleSchema).optional(),
|
|
749
|
-
}),
|
|
750
|
-
recommendations: z.array(ActionableRecommendationSchema).optional(),
|
|
751
|
-
csv_format: CsvFormatMetadataSchema.optional(),
|
|
752
|
-
execution: ExecutionResultSchema.optional(),
|
|
753
|
-
audit: AuditMetadataSchema.optional(),
|
|
726
|
+
export const ExecutionSummaryOutputSchema = z.object({
|
|
727
|
+
transactions_created: z.number().int(),
|
|
728
|
+
transactions_updated: z.number().int(),
|
|
729
|
+
dates_adjusted: z.number().int(),
|
|
730
|
+
dry_run: z.boolean(),
|
|
731
|
+
balance_status: z.enum(["balanced", "unbalanced", "not_verified"]).optional(),
|
|
732
|
+
recommendations: z.array(z.string()).optional(),
|
|
754
733
|
});
|
|
755
734
|
|
|
735
|
+
export type ExecutionSummaryOutput = z.infer<
|
|
736
|
+
typeof ExecutionSummaryOutputSchema
|
|
737
|
+
>;
|
|
738
|
+
|
|
756
739
|
export const StructuredReconciliationUnmatchedOnlySchema = z
|
|
757
740
|
.object({
|
|
758
741
|
unmatched_bank: z.array(BankTransactionSchema),
|
|
759
742
|
unmatched_ynab: z.array(YNABTransactionSimpleSchema),
|
|
760
743
|
suggestions: z.array(TransactionMatchSchema),
|
|
744
|
+
execution_summary: ExecutionSummaryOutputSchema.optional(),
|
|
761
745
|
})
|
|
762
746
|
.strict();
|
|
763
747
|
|
|
@@ -766,62 +750,11 @@ export type StructuredReconciliationUnmatchedOnly = z.infer<
|
|
|
766
750
|
>;
|
|
767
751
|
|
|
768
752
|
export const ReconcileAccountOutputSchema = z
|
|
769
|
-
.
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
structured: z.union([
|
|
775
|
-
StructuredReconciliationDataBaseSchema,
|
|
776
|
-
StructuredReconciliationUnmatchedOnlySchema,
|
|
777
|
-
]),
|
|
778
|
-
})
|
|
779
|
-
.strict(),
|
|
780
|
-
// Human narrative only (default mode) - check this SECOND
|
|
781
|
-
z
|
|
782
|
-
.object({
|
|
783
|
-
human: z.string(),
|
|
784
|
-
})
|
|
785
|
-
.strict(),
|
|
786
|
-
])
|
|
787
|
-
.refine(
|
|
788
|
-
(data) => {
|
|
789
|
-
// Only validate if this is the structured variant (has 'structured' property)
|
|
790
|
-
if (
|
|
791
|
-
"structured" in data &&
|
|
792
|
-
data.structured &&
|
|
793
|
-
"balance" in data.structured &&
|
|
794
|
-
typeof data.structured.balance === "object" &&
|
|
795
|
-
data.structured.balance !== null
|
|
796
|
-
) {
|
|
797
|
-
const discrepancyAmount = data.structured.balance.discrepancy.value;
|
|
798
|
-
const direction = data.structured.balance.discrepancy_direction;
|
|
799
|
-
|
|
800
|
-
// If absolute discrepancy < 0.01, direction must be 'balanced'
|
|
801
|
-
if (Math.abs(discrepancyAmount) < 0.01) {
|
|
802
|
-
return direction === "balanced";
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
// If discrepancy > 0, direction must be 'ynab_higher'
|
|
806
|
-
if (discrepancyAmount > 0) {
|
|
807
|
-
return direction === "ynab_higher";
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// If discrepancy < 0, direction must be 'bank_higher'
|
|
811
|
-
if (discrepancyAmount < 0) {
|
|
812
|
-
return direction === "bank_higher";
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
// Human-only variant always passes validation
|
|
817
|
-
return true;
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
message:
|
|
821
|
-
"Discrepancy direction mismatch: direction must match the numeric discrepancy amount",
|
|
822
|
-
path: ["balance", "discrepancy_direction"],
|
|
823
|
-
},
|
|
824
|
-
);
|
|
753
|
+
.object({
|
|
754
|
+
human: z.string(),
|
|
755
|
+
structured: StructuredReconciliationUnmatchedOnlySchema,
|
|
756
|
+
})
|
|
757
|
+
.strict();
|
|
825
758
|
|
|
826
759
|
export type ReconcileAccountOutput = z.infer<
|
|
827
760
|
typeof ReconcileAccountOutputSchema
|