@dizzlkheinz/ynab-mcpb 0.18.3 → 0.18.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/bundle/index.cjs +40 -40
  3. package/dist/tools/reconcileAdapter.js +3 -0
  4. package/dist/tools/reconciliation/analyzer.js +72 -7
  5. package/dist/tools/reconciliation/reportFormatter.js +26 -2
  6. package/dist/tools/reconciliation/types.d.ts +3 -0
  7. package/dist/tools/transactionSchemas.d.ts +309 -0
  8. package/dist/tools/transactionSchemas.js +215 -0
  9. package/dist/tools/transactionTools.d.ts +3 -281
  10. package/dist/tools/transactionTools.js +4 -559
  11. package/dist/tools/transactionUtils.d.ts +31 -0
  12. package/dist/tools/transactionUtils.js +349 -0
  13. package/docs/plans/2025-12-25-transaction-tools-refactor-design.md +211 -0
  14. package/docs/plans/2025-12-25-transaction-tools-refactor.md +905 -0
  15. package/package.json +4 -2
  16. package/scripts/run-all-tests.js +196 -0
  17. package/src/tools/__tests__/transactionSchemas.test.ts +1188 -0
  18. package/src/tools/__tests__/transactionUtils.test.ts +989 -0
  19. package/src/tools/reconcileAdapter.ts +6 -0
  20. package/src/tools/reconciliation/__tests__/adapter.causes.test.ts +22 -8
  21. package/src/tools/reconciliation/__tests__/adapter.test.ts +3 -0
  22. package/src/tools/reconciliation/__tests__/analyzer.test.ts +65 -0
  23. package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +3 -0
  24. package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +4 -1
  25. package/src/tools/reconciliation/__tests__/scenarios/adapterCurrency.scenario.test.ts +3 -0
  26. package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +5 -1
  27. package/src/tools/reconciliation/__tests__/schemaUrl.test.ts +22 -8
  28. package/src/tools/reconciliation/analyzer.ts +127 -11
  29. package/src/tools/reconciliation/reportFormatter.ts +39 -2
  30. package/src/tools/reconciliation/types.ts +6 -0
  31. package/src/tools/transactionSchemas.ts +453 -0
  32. package/src/tools/transactionTools.ts +102 -823
  33. package/src/tools/transactionUtils.ts +536 -0
@@ -0,0 +1,453 @@
1
+ import { z } from 'zod/v4';
2
+ import * as ynab from 'ynab';
3
+
4
+ /**
5
+ * Transaction Schemas and Types
6
+ *
7
+ * This module contains all Zod schemas and TypeScript types for transaction-related tools.
8
+ * Extracted from transactionTools.ts for better code organization.
9
+ */
10
+
11
+ // ============================================================================
12
+ // List Transactions
13
+ // ============================================================================
14
+
15
+ /**
16
+ * Schema for ynab:list_transactions tool parameters
17
+ */
18
+ export const ListTransactionsSchema = z
19
+ .object({
20
+ budget_id: z.string().min(1, 'Budget ID is required'),
21
+ account_id: z.string().optional(),
22
+ category_id: z.string().optional(),
23
+ since_date: z
24
+ .string()
25
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in ISO format (YYYY-MM-DD)')
26
+ .optional(),
27
+ type: z.enum(['uncategorized', 'unapproved']).optional(),
28
+ })
29
+ .strict();
30
+
31
+ export type ListTransactionsParams = z.infer<typeof ListTransactionsSchema>;
32
+
33
+ // ============================================================================
34
+ // Get Transaction
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Schema for ynab:get_transaction tool parameters
39
+ */
40
+ export const GetTransactionSchema = z
41
+ .object({
42
+ budget_id: z.string().min(1, 'Budget ID is required'),
43
+ transaction_id: z.string().min(1, 'Transaction ID is required'),
44
+ })
45
+ .strict();
46
+
47
+ export type GetTransactionParams = z.infer<typeof GetTransactionSchema>;
48
+
49
+ // ============================================================================
50
+ // Create Transaction
51
+ // ============================================================================
52
+
53
+ /**
54
+ * Schema for ynab:create_transaction tool parameters
55
+ */
56
+ export const CreateTransactionSchema = z
57
+ .object({
58
+ budget_id: z.string().min(1, 'Budget ID is required'),
59
+ account_id: z.string().min(1, 'Account ID is required'),
60
+ amount: z.number().int('Amount must be an integer in milliunits'),
61
+ date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in ISO format (YYYY-MM-DD)'),
62
+ payee_name: z.string().optional(),
63
+ payee_id: z.string().optional(),
64
+ category_id: z.string().optional(),
65
+ memo: z.string().optional(),
66
+ cleared: z.enum(['cleared', 'uncleared', 'reconciled']).optional(),
67
+ approved: z.boolean().optional(),
68
+ flag_color: z.enum(['red', 'orange', 'yellow', 'green', 'blue', 'purple']).optional(),
69
+ import_id: z.string().min(1, 'Import ID cannot be empty').optional(),
70
+ dry_run: z.boolean().optional(),
71
+ subtransactions: z
72
+ .array(
73
+ z
74
+ .object({
75
+ amount: z.number().int('Subtransaction amount must be an integer in milliunits'),
76
+ payee_name: z.string().optional(),
77
+ payee_id: z.string().optional(),
78
+ category_id: z.string().optional(),
79
+ memo: z.string().optional(),
80
+ })
81
+ .strict(),
82
+ )
83
+ .min(1, 'At least one subtransaction is required when provided')
84
+ .optional(),
85
+ })
86
+ .strict()
87
+ .superRefine((data, ctx) => {
88
+ if (data.subtransactions && data.subtransactions.length > 0) {
89
+ const total = data.subtransactions.reduce((sum, sub) => sum + sub.amount, 0);
90
+ if (total !== data.amount) {
91
+ ctx.addIssue({
92
+ code: z.ZodIssueCode.custom,
93
+ message: 'Amount must equal the sum of subtransaction amounts',
94
+ path: ['amount'],
95
+ });
96
+ }
97
+ }
98
+ });
99
+
100
+ export type CreateTransactionParams = z.infer<typeof CreateTransactionSchema>;
101
+
102
+ /**
103
+ * Schema for subtransaction input
104
+ */
105
+ export interface SubtransactionInput {
106
+ amount: number;
107
+ payee_name?: string;
108
+ payee_id?: string;
109
+ category_id?: string;
110
+ memo?: string;
111
+ }
112
+
113
+ // ============================================================================
114
+ // Create Transactions (Bulk)
115
+ // ============================================================================
116
+
117
+ const BulkTransactionInputSchemaBase = CreateTransactionSchema.pick({
118
+ account_id: true,
119
+ amount: true,
120
+ date: true,
121
+ payee_name: true,
122
+ payee_id: true,
123
+ category_id: true,
124
+ memo: true,
125
+ cleared: true,
126
+ approved: true,
127
+ flag_color: true,
128
+ import_id: true,
129
+ });
130
+
131
+ export type BulkTransactionInput = Omit<
132
+ CreateTransactionParams,
133
+ 'budget_id' | 'dry_run' | 'subtransactions'
134
+ >;
135
+
136
+ // Schema for bulk transaction creation - subtransactions are not supported
137
+ // The .strict() modifier automatically rejects any fields not in the schema
138
+ const BulkTransactionInputSchema = BulkTransactionInputSchemaBase.strict();
139
+
140
+ export const CreateTransactionsSchema = z
141
+ .object({
142
+ budget_id: z.string().min(1, 'Budget ID is required'),
143
+ transactions: z
144
+ .array(BulkTransactionInputSchema)
145
+ .min(1, 'At least one transaction is required')
146
+ .max(100, 'A maximum of 100 transactions may be created at once'),
147
+ dry_run: z.boolean().optional(),
148
+ })
149
+ .strict();
150
+
151
+ export type CreateTransactionsParams = z.infer<typeof CreateTransactionsSchema>;
152
+
153
+ export interface BulkTransactionResult {
154
+ request_index: number;
155
+ status: 'created' | 'duplicate' | 'failed';
156
+ transaction_id?: string | undefined;
157
+ correlation_key: string;
158
+ error_code?: string | undefined;
159
+ error?: string | undefined;
160
+ }
161
+
162
+ export interface BulkCreateResponse {
163
+ success: boolean;
164
+ server_knowledge?: number;
165
+ summary: {
166
+ total_requested: number;
167
+ created: number;
168
+ duplicates: number;
169
+ failed: number;
170
+ };
171
+ results: BulkTransactionResult[];
172
+ transactions?: ynab.TransactionDetail[];
173
+ duplicate_import_ids?: string[];
174
+ message?: string;
175
+ mode?: 'full' | 'summary' | 'ids_only';
176
+ }
177
+
178
+ // ============================================================================
179
+ // Create Receipt Split Transaction
180
+ // ============================================================================
181
+
182
+ const ReceiptSplitItemSchema = z
183
+ .object({
184
+ name: z.string().min(1, 'Item name is required'),
185
+ amount: z.number().finite('Item amount must be a finite number'),
186
+ quantity: z
187
+ .number()
188
+ .finite('Quantity must be a finite number')
189
+ .positive('Quantity must be greater than zero')
190
+ .optional(),
191
+ memo: z.string().optional(),
192
+ })
193
+ .strict();
194
+
195
+ const ReceiptSplitCategorySchema = z
196
+ .object({
197
+ category_id: z.string().min(1, 'Category ID is required'),
198
+ category_name: z.string().optional(),
199
+ items: z.array(ReceiptSplitItemSchema).min(1, 'Each category must include at least one item'),
200
+ })
201
+ .strict();
202
+
203
+ export const CreateReceiptSplitTransactionSchema = z
204
+ .object({
205
+ budget_id: z.string().min(1, 'Budget ID is required'),
206
+ account_id: z.string().min(1, 'Account ID is required'),
207
+ payee_name: z.string().min(1, 'Payee name is required'),
208
+ date: z
209
+ .string()
210
+ .regex(/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/, 'Date must be in ISO format (YYYY-MM-DD)')
211
+ .optional(),
212
+ memo: z.string().optional(),
213
+ receipt_subtotal: z
214
+ .number()
215
+ .finite('Receipt subtotal must be a finite number')
216
+ .refine((value) => value >= 0, 'Receipt subtotal must be zero or greater')
217
+ .optional(),
218
+ receipt_tax: z.number().finite('Receipt tax must be a finite number'),
219
+ receipt_total: z
220
+ .number()
221
+ .finite('Receipt total must be a finite number')
222
+ .refine((value) => value > 0, 'Receipt total must be greater than zero'),
223
+ categories: z
224
+ .array(ReceiptSplitCategorySchema)
225
+ .min(1, 'At least one categorized group is required to create a split transaction'),
226
+ cleared: z.enum(['cleared', 'uncleared', 'reconciled']).optional(),
227
+ approved: z.boolean().optional(),
228
+ flag_color: z.enum(['red', 'orange', 'yellow', 'green', 'blue', 'purple']).optional(),
229
+ dry_run: z.boolean().optional(),
230
+ })
231
+ .strict()
232
+ .superRefine((data, ctx) => {
233
+ const itemsSubtotal = data.categories
234
+ .flatMap((category) => category.items)
235
+ .reduce((sum, item) => sum + item.amount, 0);
236
+
237
+ if (data.receipt_subtotal !== undefined) {
238
+ const delta = Math.abs(data.receipt_subtotal - itemsSubtotal);
239
+ if (delta > 0.01) {
240
+ ctx.addIssue({
241
+ code: z.ZodIssueCode.custom,
242
+ message: `Receipt subtotal (${data.receipt_subtotal.toFixed(2)}) does not match categorized items total (${itemsSubtotal.toFixed(2)})`,
243
+ path: ['receipt_subtotal'],
244
+ });
245
+ }
246
+ }
247
+
248
+ const expectedTotal = itemsSubtotal + data.receipt_tax;
249
+ const deltaTotal = Math.abs(expectedTotal - data.receipt_total);
250
+ if (deltaTotal > 0.01) {
251
+ ctx.addIssue({
252
+ code: z.ZodIssueCode.custom,
253
+ message: `Receipt total (${data.receipt_total.toFixed(2)}) does not match subtotal plus tax (${expectedTotal.toFixed(2)})`,
254
+ path: ['receipt_total'],
255
+ });
256
+ }
257
+ });
258
+
259
+ export type CreateReceiptSplitTransactionParams = z.infer<
260
+ typeof CreateReceiptSplitTransactionSchema
261
+ >;
262
+
263
+ /**
264
+ * Interface for receipt category calculation
265
+ */
266
+ export interface ReceiptCategoryCalculation {
267
+ category_id: string;
268
+ category_name: string | undefined;
269
+ subtotal_milliunits: number;
270
+ tax_milliunits: number;
271
+ items: {
272
+ name: string;
273
+ amount_milliunits: number;
274
+ quantity: number | undefined;
275
+ memo: string | undefined;
276
+ }[];
277
+ }
278
+
279
+ // ============================================================================
280
+ // Update Transaction
281
+ // ============================================================================
282
+
283
+ /**
284
+ * Schema for ynab:update_transaction tool parameters
285
+ */
286
+ export const UpdateTransactionSchema = z
287
+ .object({
288
+ budget_id: z.string().min(1, 'Budget ID is required'),
289
+ transaction_id: z.string().min(1, 'Transaction ID is required'),
290
+ account_id: z.string().optional(),
291
+ amount: z.number().int('Amount must be an integer in milliunits').optional(),
292
+ date: z
293
+ .string()
294
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in ISO format (YYYY-MM-DD)')
295
+ .optional(),
296
+ payee_name: z.string().optional(),
297
+ payee_id: z.string().optional(),
298
+ category_id: z.string().optional(),
299
+ memo: z.string().optional(),
300
+ cleared: z.enum(['cleared', 'uncleared', 'reconciled']).optional(),
301
+ approved: z.boolean().optional(),
302
+ flag_color: z.enum(['red', 'orange', 'yellow', 'green', 'blue', 'purple']).optional(),
303
+ dry_run: z.boolean().optional(),
304
+ })
305
+ .strict();
306
+
307
+ export type UpdateTransactionParams = z.infer<typeof UpdateTransactionSchema>;
308
+
309
+ // ============================================================================
310
+ // Update Transactions (Bulk)
311
+ // ============================================================================
312
+
313
+ /**
314
+ * Schema for bulk transaction updates - each item in the array
315
+ * Note: account_id is intentionally excluded as account moves are not supported in bulk updates
316
+ */
317
+ const BulkUpdateTransactionInputSchema = z
318
+ .object({
319
+ id: z.string().min(1, 'Transaction ID is required'),
320
+ amount: z.number().int('Amount must be an integer in milliunits').optional(),
321
+ date: z
322
+ .string()
323
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in ISO format (YYYY-MM-DD)')
324
+ .optional(),
325
+ payee_name: z.string().optional(),
326
+ payee_id: z.string().optional(),
327
+ category_id: z.string().optional(),
328
+ memo: z.string().optional(),
329
+ cleared: z.enum(['cleared', 'uncleared', 'reconciled']).optional(),
330
+ approved: z.boolean().optional(),
331
+ flag_color: z.enum(['red', 'orange', 'yellow', 'green', 'blue', 'purple']).optional(),
332
+ // Metadata fields for cache invalidation
333
+ original_account_id: z.string().optional(),
334
+ original_date: z
335
+ .string()
336
+ .regex(/^\d{4}-\d{2}-\d{2}$/, 'Date must be in ISO format (YYYY-MM-DD)')
337
+ .optional(),
338
+ })
339
+ .strict();
340
+
341
+ export type BulkUpdateTransactionInput = z.infer<typeof BulkUpdateTransactionInputSchema>;
342
+
343
+ /**
344
+ * Schema for ynab:update_transactions tool parameters
345
+ */
346
+ export const UpdateTransactionsSchema = z
347
+ .object({
348
+ budget_id: z.string().min(1, 'Budget ID is required'),
349
+ transactions: z
350
+ .array(BulkUpdateTransactionInputSchema)
351
+ .min(1, 'At least one transaction is required')
352
+ .max(100, 'A maximum of 100 transactions may be updated at once'),
353
+ dry_run: z.boolean().optional(),
354
+ })
355
+ .strict();
356
+
357
+ export type UpdateTransactionsParams = z.infer<typeof UpdateTransactionsSchema>;
358
+
359
+ export interface BulkUpdateResult {
360
+ request_index: number;
361
+ status: 'updated' | 'failed';
362
+ transaction_id: string;
363
+ correlation_key: string;
364
+ error_code?: string;
365
+ error?: string;
366
+ }
367
+
368
+ export interface BulkUpdateResponse {
369
+ success: boolean;
370
+ server_knowledge?: number;
371
+ summary: {
372
+ total_requested: number;
373
+ updated: number;
374
+ failed: number;
375
+ };
376
+ results: BulkUpdateResult[];
377
+ transactions?: ynab.TransactionDetail[];
378
+ message?: string;
379
+ mode?: 'full' | 'summary' | 'ids_only';
380
+ }
381
+
382
+ // ============================================================================
383
+ // Delete Transaction
384
+ // ============================================================================
385
+
386
+ /**
387
+ * Schema for ynab:delete_transaction tool parameters
388
+ */
389
+ export const DeleteTransactionSchema = z
390
+ .object({
391
+ budget_id: z.string().min(1, 'Budget ID is required'),
392
+ transaction_id: z.string().min(1, 'Transaction ID is required'),
393
+ dry_run: z.boolean().optional(),
394
+ })
395
+ .strict();
396
+
397
+ export type DeleteTransactionParams = z.infer<typeof DeleteTransactionSchema>;
398
+
399
+ // ============================================================================
400
+ // Correlation & Utility Types
401
+ // ============================================================================
402
+
403
+ /**
404
+ * Type for correlation payload used in bulk operations
405
+ */
406
+ export interface CorrelationPayload {
407
+ account_id?: string;
408
+ date?: string;
409
+ amount?: number;
410
+ payee_id?: string | null;
411
+ payee_name?: string | null;
412
+ category_id?: string | null;
413
+ memo?: string | null;
414
+ cleared?: ynab.TransactionClearedStatus;
415
+ approved?: boolean;
416
+ flag_color?: ynab.TransactionFlagColor | null;
417
+ import_id?: string | null;
418
+ }
419
+
420
+ /**
421
+ * Interface for correlation payload input (with optional fields)
422
+ */
423
+ export interface CorrelationPayloadInput {
424
+ account_id?: string | undefined;
425
+ date?: string | undefined;
426
+ amount?: number | undefined;
427
+ payee_id?: string | null | undefined;
428
+ payee_name?: string | null | undefined;
429
+ category_id?: string | null | undefined;
430
+ memo?: string | null | undefined;
431
+ cleared?: ynab.TransactionClearedStatus | undefined;
432
+ approved?: boolean | undefined;
433
+ flag_color?: ynab.TransactionFlagColor | null | undefined;
434
+ import_id?: string | null | undefined;
435
+ }
436
+
437
+ /**
438
+ * Interface for category source (used in cache invalidation)
439
+ */
440
+ export interface CategorySource {
441
+ category_id?: string | null;
442
+ subtransactions?: { category_id?: string | null }[] | null | undefined;
443
+ }
444
+
445
+ /**
446
+ * Interface for transaction cache invalidation options
447
+ */
448
+ export interface TransactionCacheInvalidationOptions {
449
+ affectedCategoryIds?: Set<string>;
450
+ invalidateAllCategories?: boolean;
451
+ accountTotalsChanged?: boolean;
452
+ invalidateMonths?: boolean;
453
+ }