@dizzlkheinz/ynab-mcpb 0.26.2 → 0.26.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/CLAUDE.md +1 -1
- package/dist/bundle/index.cjs +91 -93
- package/dist/tools/reconciliation/executor.js +5 -12
- package/dist/tools/reconciliation/index.d.ts +9 -12
- package/dist/tools/reconciliation/index.js +98 -90
- package/dist/tools/schemas/outputs/reconciliationOutputs.d.ts +81 -770
- package/dist/tools/schemas/outputs/reconciliationOutputs.js +38 -66
- package/package.json +1 -1
- package/src/__tests__/performance.test.ts +2 -4
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +2 -4
- package/src/tools/reconciliation/__tests__/executor.test.ts +6 -10
- package/src/tools/reconciliation/__tests__/index.test.ts +10 -6
- package/src/tools/reconciliation/__tests__/reconciliation.delta.integration.test.ts +1 -110
- package/src/tools/reconciliation/executor.ts +6 -13
- package/src/tools/reconciliation/index.ts +152 -137
- package/src/tools/schemas/outputs/__tests__/discrepancyDirection.test.ts +146 -312
- package/src/tools/schemas/outputs/reconciliationOutputs.ts +47 -84
|
@@ -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
|
});
|
|
@@ -495,6 +495,36 @@ export const ExecutionActionRecordSchema = z.discriminatedUnion("type", [
|
|
|
495
495
|
reason: z.string(),
|
|
496
496
|
bulk_chunk_index: z.number(),
|
|
497
497
|
}),
|
|
498
|
+
// Bulk update chunk failure
|
|
499
|
+
z.object({
|
|
500
|
+
type: z.literal("batch_update_failed"),
|
|
501
|
+
transaction: z.null(),
|
|
502
|
+
reason: z.string(),
|
|
503
|
+
}),
|
|
504
|
+
// Bulk reconcile chunk failure
|
|
505
|
+
z.object({
|
|
506
|
+
type: z.literal("batch_reconcile_failed"),
|
|
507
|
+
transaction: z.null(),
|
|
508
|
+
reason: z.string(),
|
|
509
|
+
}),
|
|
510
|
+
// All matched transactions marked reconciled
|
|
511
|
+
z.object({
|
|
512
|
+
type: z.literal("reconciliation_complete"),
|
|
513
|
+
transaction: z.null(),
|
|
514
|
+
reason: z.string(),
|
|
515
|
+
}),
|
|
516
|
+
// Diagnostic: STEP 3 entry metadata
|
|
517
|
+
z.object({
|
|
518
|
+
type: z.literal("diagnostic_step3_entry"),
|
|
519
|
+
transaction: z.null(),
|
|
520
|
+
reason: z.string(),
|
|
521
|
+
}),
|
|
522
|
+
// Diagnostic: unmatched YNAB transaction details
|
|
523
|
+
z.object({
|
|
524
|
+
type: z.literal("diagnostic_unmatched_ynab"),
|
|
525
|
+
transaction: z.record(z.string(), z.unknown()),
|
|
526
|
+
reason: z.string(),
|
|
527
|
+
}),
|
|
498
528
|
]);
|
|
499
529
|
|
|
500
530
|
export type ExecutionActionRecord = z.infer<typeof ExecutionActionRecordSchema>;
|
|
@@ -693,41 +723,25 @@ export const CsvFormatMetadataSchema = z.object({
|
|
|
693
723
|
|
|
694
724
|
export type CsvFormatMetadata = z.infer<typeof CsvFormatMetadataSchema>;
|
|
695
725
|
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
name: z.string().optional(),
|
|
704
|
-
}),
|
|
705
|
-
summary: ReconciliationSummarySchema,
|
|
706
|
-
balance: BalanceInfoSchema.extend({
|
|
707
|
-
discrepancy_direction: z.enum(["balanced", "ynab_higher", "bank_higher"]),
|
|
708
|
-
}),
|
|
709
|
-
insights: z.array(ReconciliationInsightSchema),
|
|
710
|
-
next_steps: z.array(z.string()),
|
|
711
|
-
matches: z.object({
|
|
712
|
-
auto: z.array(TransactionMatchSchema),
|
|
713
|
-
suggested: z.array(TransactionMatchSchema),
|
|
714
|
-
}),
|
|
715
|
-
unmatched: z.object({
|
|
716
|
-
bank: z.array(BankTransactionSchema),
|
|
717
|
-
ynab: z.array(YNABTransactionSimpleSchema),
|
|
718
|
-
ynab_outside_date_range: z.array(YNABTransactionSimpleSchema).optional(),
|
|
719
|
-
}),
|
|
720
|
-
recommendations: z.array(ActionableRecommendationSchema).optional(),
|
|
721
|
-
csv_format: CsvFormatMetadataSchema.optional(),
|
|
722
|
-
execution: ExecutionResultSchema.optional(),
|
|
723
|
-
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(),
|
|
724
733
|
});
|
|
725
734
|
|
|
735
|
+
export type ExecutionSummaryOutput = z.infer<
|
|
736
|
+
typeof ExecutionSummaryOutputSchema
|
|
737
|
+
>;
|
|
738
|
+
|
|
726
739
|
export const StructuredReconciliationUnmatchedOnlySchema = z
|
|
727
740
|
.object({
|
|
728
741
|
unmatched_bank: z.array(BankTransactionSchema),
|
|
729
742
|
unmatched_ynab: z.array(YNABTransactionSimpleSchema),
|
|
730
743
|
suggestions: z.array(TransactionMatchSchema),
|
|
744
|
+
execution_summary: ExecutionSummaryOutputSchema.optional(),
|
|
731
745
|
})
|
|
732
746
|
.strict();
|
|
733
747
|
|
|
@@ -736,62 +750,11 @@ export type StructuredReconciliationUnmatchedOnly = z.infer<
|
|
|
736
750
|
>;
|
|
737
751
|
|
|
738
752
|
export const ReconcileAccountOutputSchema = z
|
|
739
|
-
.
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
structured: z.union([
|
|
745
|
-
StructuredReconciliationDataBaseSchema,
|
|
746
|
-
StructuredReconciliationUnmatchedOnlySchema,
|
|
747
|
-
]),
|
|
748
|
-
})
|
|
749
|
-
.strict(),
|
|
750
|
-
// Human narrative only (default mode) - check this SECOND
|
|
751
|
-
z
|
|
752
|
-
.object({
|
|
753
|
-
human: z.string(),
|
|
754
|
-
})
|
|
755
|
-
.strict(),
|
|
756
|
-
])
|
|
757
|
-
.refine(
|
|
758
|
-
(data) => {
|
|
759
|
-
// Only validate if this is the structured variant (has 'structured' property)
|
|
760
|
-
if (
|
|
761
|
-
"structured" in data &&
|
|
762
|
-
data.structured &&
|
|
763
|
-
"balance" in data.structured &&
|
|
764
|
-
typeof data.structured.balance === "object" &&
|
|
765
|
-
data.structured.balance !== null
|
|
766
|
-
) {
|
|
767
|
-
const discrepancyAmount = data.structured.balance.discrepancy.value;
|
|
768
|
-
const direction = data.structured.balance.discrepancy_direction;
|
|
769
|
-
|
|
770
|
-
// If absolute discrepancy < 0.01, direction must be 'balanced'
|
|
771
|
-
if (Math.abs(discrepancyAmount) < 0.01) {
|
|
772
|
-
return direction === "balanced";
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
// If discrepancy > 0, direction must be 'ynab_higher'
|
|
776
|
-
if (discrepancyAmount > 0) {
|
|
777
|
-
return direction === "ynab_higher";
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// If discrepancy < 0, direction must be 'bank_higher'
|
|
781
|
-
if (discrepancyAmount < 0) {
|
|
782
|
-
return direction === "bank_higher";
|
|
783
|
-
}
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Human-only variant always passes validation
|
|
787
|
-
return true;
|
|
788
|
-
},
|
|
789
|
-
{
|
|
790
|
-
message:
|
|
791
|
-
"Discrepancy direction mismatch: direction must match the numeric discrepancy amount",
|
|
792
|
-
path: ["balance", "discrepancy_direction"],
|
|
793
|
-
},
|
|
794
|
-
);
|
|
753
|
+
.object({
|
|
754
|
+
human: z.string(),
|
|
755
|
+
structured: StructuredReconciliationUnmatchedOnlySchema,
|
|
756
|
+
})
|
|
757
|
+
.strict();
|
|
795
758
|
|
|
796
759
|
export type ReconcileAccountOutput = z.infer<
|
|
797
760
|
typeof ReconcileAccountOutputSchema
|