@armor/zuora-mcp 0.0.0-development

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 (47) hide show
  1. package/.env.example +16 -0
  2. package/README.md +249 -0
  3. package/dist/cli.d.ts +15 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +73 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/config.d.ts +26 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +56 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/index.d.ts +22 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +148 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/prompts.d.ts +11 -0
  16. package/dist/prompts.d.ts.map +1 -0
  17. package/dist/prompts.js +236 -0
  18. package/dist/prompts.js.map +1 -0
  19. package/dist/resources.d.ts +11 -0
  20. package/dist/resources.d.ts.map +1 -0
  21. package/dist/resources.js +526 -0
  22. package/dist/resources.js.map +1 -0
  23. package/dist/setup.d.ts +12 -0
  24. package/dist/setup.d.ts.map +1 -0
  25. package/dist/setup.js +188 -0
  26. package/dist/setup.js.map +1 -0
  27. package/dist/token-manager.d.ts +34 -0
  28. package/dist/token-manager.d.ts.map +1 -0
  29. package/dist/token-manager.js +103 -0
  30. package/dist/token-manager.js.map +1 -0
  31. package/dist/tools.d.ts +1096 -0
  32. package/dist/tools.d.ts.map +1 -0
  33. package/dist/tools.js +2841 -0
  34. package/dist/tools.js.map +1 -0
  35. package/dist/types.d.ts +758 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +5 -0
  38. package/dist/types.js.map +1 -0
  39. package/dist/zoql-helpers.d.ts +68 -0
  40. package/dist/zoql-helpers.d.ts.map +1 -0
  41. package/dist/zoql-helpers.js +154 -0
  42. package/dist/zoql-helpers.js.map +1 -0
  43. package/dist/zuora-client.d.ts +184 -0
  44. package/dist/zuora-client.d.ts.map +1 -0
  45. package/dist/zuora-client.js +583 -0
  46. package/dist/zuora-client.js.map +1 -0
  47. package/package.json +60 -0
package/dist/tools.js ADDED
@@ -0,0 +1,2841 @@
1
+ /**
2
+ * MCP Tool Definitions for Zuora Billing Operations
3
+ * Provides account, invoice, subscription, payment, and ZOQL query tools
4
+ */
5
+ import { z } from "zod";
6
+ import { queryAll, escapeZoql, todayString, addDays, subtractDays, daysBetween, getString, getNumber, collectIds, queryWithBatchedIds, } from "./zoql-helpers.js";
7
+ // ==================== Helpers ====================
8
+ function isValidCalendarDate(dateStr) {
9
+ const [year, month, day] = dateStr.split("-").map(Number);
10
+ const date = new Date(year, month - 1, day);
11
+ return (date.getFullYear() === year &&
12
+ date.getMonth() === month - 1 &&
13
+ date.getDate() === day);
14
+ }
15
+ // ==================== Shared Schemas ====================
16
+ /** Contact fields for account creation (name + email required) */
17
+ const contactSchema = z.object({
18
+ firstName: z.string().min(1).max(100).describe("First name"),
19
+ lastName: z.string().min(1).max(100).describe("Last name"),
20
+ workEmail: z.string().email("Must be a valid email address").describe("Work email address"),
21
+ address1: z.string().max(255).optional().describe("Street address line 1"),
22
+ city: z.string().max(100).optional().describe("City"),
23
+ state: z.string().max(100).optional().describe("State or province"),
24
+ postalCode: z.string().max(20).optional().describe("Postal/ZIP code"),
25
+ country: z.string().max(100).optional().describe("Country (ISO 3166 name or code)"),
26
+ });
27
+ /** Contact fields for account updates (all fields optional for partial updates) */
28
+ const partialContactSchema = contactSchema.partial();
29
+ // ==================== Input Schemas ====================
30
+ export const schemas = {
31
+ // Account Tools
32
+ getAccount: z.object({
33
+ accountKey: z
34
+ .string()
35
+ .min(1)
36
+ .describe("Account ID or account number (e.g., 2c92c0f8..., or A00000001)"),
37
+ }),
38
+ getAccountSummary: z.object({
39
+ accountKey: z
40
+ .string()
41
+ .min(1)
42
+ .describe("Account ID or account number"),
43
+ }),
44
+ // Invoice Tools
45
+ getInvoice: z.object({
46
+ invoiceId: z
47
+ .string()
48
+ .min(1)
49
+ .describe("Invoice ID (e.g., 2c92c0f8...)"),
50
+ }),
51
+ listInvoices: z.object({
52
+ accountKey: z
53
+ .string()
54
+ .min(1)
55
+ .describe("Account ID or account number to list invoices for"),
56
+ page: z
57
+ .number()
58
+ .int()
59
+ .min(1)
60
+ .default(1)
61
+ .describe("Page number (default: 1)"),
62
+ pageSize: z
63
+ .number()
64
+ .int()
65
+ .min(1)
66
+ .max(40)
67
+ .default(20)
68
+ .describe("Records per page (default: 20, max: 40)"),
69
+ }),
70
+ // Subscription Tools
71
+ getSubscription: z.object({
72
+ subscriptionKey: z
73
+ .string()
74
+ .min(1)
75
+ .describe("Subscription key or ID (e.g., A-S00000001)"),
76
+ }),
77
+ listSubscriptions: z.object({
78
+ accountKey: z
79
+ .string()
80
+ .min(1)
81
+ .describe("Account ID or account number"),
82
+ }),
83
+ // Payment Tools
84
+ getPayment: z.object({
85
+ paymentId: z.string().min(1).describe("Payment ID"),
86
+ }),
87
+ listPayments: z.object({
88
+ accountKey: z
89
+ .string()
90
+ .optional()
91
+ .describe("Account key to filter payments (optional, lists all if omitted)"),
92
+ page: z
93
+ .number()
94
+ .int()
95
+ .min(1)
96
+ .default(1)
97
+ .describe("Page number (default: 1)"),
98
+ pageSize: z
99
+ .number()
100
+ .int()
101
+ .min(1)
102
+ .max(40)
103
+ .default(20)
104
+ .describe("Records per page (default: 20, max: 40)"),
105
+ }),
106
+ // ZOQL Query Tools
107
+ executeZoqlQuery: z.object({
108
+ zoqlQuery: z
109
+ .string()
110
+ .min(1)
111
+ .max(10000)
112
+ .refine((q) => /^\s*SELECT\s+/i.test(q), {
113
+ message: "ZOQL query must begin with SELECT",
114
+ })
115
+ .describe("ZOQL (Zuora Object Query Language) query string. " +
116
+ "Syntax: SELECT field1, field2 FROM ObjectName WHERE condition. " +
117
+ "Key objects: Account, Invoice, Payment, Subscription, RatePlan, " +
118
+ "RatePlanCharge, Product, ProductRatePlan, Contact. " +
119
+ "Limitations: No JOINs. Max 2000 records per call. " +
120
+ "Use continue_zoql_query with queryLocator for pagination. " +
121
+ "Example: SELECT Id, AccountNumber, Balance FROM Account WHERE Status = 'Active'"),
122
+ }),
123
+ continueZoqlQuery: z.object({
124
+ queryLocator: z
125
+ .string()
126
+ .min(1)
127
+ .describe("Query locator from a previous ZOQL query result for pagination"),
128
+ }),
129
+ // Product Catalog Tools
130
+ listProducts: z.object({
131
+ page: z
132
+ .number()
133
+ .int()
134
+ .min(1)
135
+ .default(1)
136
+ .describe("Page number (default: 1)"),
137
+ pageSize: z
138
+ .number()
139
+ .int()
140
+ .min(1)
141
+ .max(40)
142
+ .default(20)
143
+ .describe("Records per page (default: 20, max: 40)"),
144
+ }),
145
+ // Phase 2: Extended Read Tools
146
+ getInvoiceFiles: z.object({
147
+ invoiceId: z
148
+ .string()
149
+ .min(1)
150
+ .describe("Invoice ID to retrieve associated PDF files for"),
151
+ }),
152
+ getCreditMemo: z.object({
153
+ creditMemoId: z
154
+ .string()
155
+ .min(1)
156
+ .describe("Credit memo ID (e.g., 2c92c0f8...)"),
157
+ }),
158
+ listCreditMemos: z.object({
159
+ accountId: z
160
+ .string()
161
+ .optional()
162
+ .describe("Zuora account UUID (e.g., 2c92c0f8...) to filter credit memos. " +
163
+ "Use get_account to retrieve the account ID from an account number. " +
164
+ "Optional; lists all if omitted."),
165
+ page: z
166
+ .number()
167
+ .int()
168
+ .min(1)
169
+ .default(1)
170
+ .describe("Page number (default: 1)"),
171
+ pageSize: z
172
+ .number()
173
+ .int()
174
+ .min(1)
175
+ .max(40)
176
+ .default(20)
177
+ .describe("Records per page (default: 20, max: 40)"),
178
+ }),
179
+ searchAccounts: z.object({
180
+ field: z
181
+ .enum([
182
+ "Name",
183
+ "AccountNumber",
184
+ "Status",
185
+ "Currency",
186
+ "Balance",
187
+ ])
188
+ .describe("Account field to search by (Name, AccountNumber, Status, Currency, Balance)"),
189
+ value: z
190
+ .string()
191
+ .min(1)
192
+ .max(255)
193
+ .refine((v) => /[^%_]/.test(v), {
194
+ message: "Value must contain at least one non-wildcard character",
195
+ })
196
+ .describe("Value to search for"),
197
+ operator: z
198
+ .enum(["=", "!=", "LIKE", ">", "<", ">=", "<="])
199
+ .default("=")
200
+ .describe("Comparison operator (default: =). Use LIKE for partial name matches with % wildcard."),
201
+ }),
202
+ listUsage: z.object({
203
+ accountKey: z
204
+ .string()
205
+ .min(1)
206
+ .describe("Account ID or account number to retrieve usage records for"),
207
+ page: z
208
+ .number()
209
+ .int()
210
+ .min(1)
211
+ .default(1)
212
+ .describe("Page number (default: 1)"),
213
+ pageSize: z
214
+ .number()
215
+ .int()
216
+ .min(1)
217
+ .max(40)
218
+ .default(20)
219
+ .describe("Records per page (default: 20, max: 40)"),
220
+ }),
221
+ // User Management Tools
222
+ listUsers: z.object({
223
+ startIndex: z
224
+ .number()
225
+ .int()
226
+ .min(1)
227
+ .default(1)
228
+ .describe("1-based start index for SCIM pagination (default: 1)"),
229
+ count: z
230
+ .number()
231
+ .int()
232
+ .min(1)
233
+ .max(100)
234
+ .default(100)
235
+ .describe("Number of users to return per page (default: 100, max: 100)"),
236
+ filter: z
237
+ .string()
238
+ .max(500)
239
+ .optional()
240
+ .describe("SCIM filter string (optional). " +
241
+ "Examples: status eq \"Active\", userName eq \"user@example.com\""),
242
+ }),
243
+ getUser: z.object({
244
+ userId: z
245
+ .string()
246
+ .uuid("User ID must be a valid UUID")
247
+ .describe("Zuora user ID (UUID format)"),
248
+ }),
249
+ // Bill Run Read Tools
250
+ getBillRun: z.object({
251
+ billRunId: z
252
+ .string()
253
+ .min(1)
254
+ .describe("Bill run ID (e.g., 2c92c0f8...)"),
255
+ }),
256
+ listBillRuns: z.object({
257
+ page: z
258
+ .number()
259
+ .int()
260
+ .min(1)
261
+ .default(1)
262
+ .describe("Page number (default: 1)"),
263
+ pageSize: z
264
+ .number()
265
+ .int()
266
+ .min(1)
267
+ .max(40)
268
+ .default(20)
269
+ .describe("Records per page (default: 20, max: 40)"),
270
+ }),
271
+ // Contact Read Tools
272
+ getContact: z.object({
273
+ contactId: z
274
+ .string()
275
+ .min(1)
276
+ .describe("Contact ID (e.g., 2c92c0f8...)"),
277
+ }),
278
+ // Describe API
279
+ describeObject: z.object({
280
+ objectType: z
281
+ .string()
282
+ .min(1)
283
+ .max(100)
284
+ .regex(/^[A-Z][a-zA-Z]*$/, "Object type must be PascalCase (e.g., Account, Invoice, Subscription)")
285
+ .describe("Zuora object type name in PascalCase (e.g., Account, Invoice, Subscription, " +
286
+ "Payment, RatePlan, RatePlanCharge, Product, ProductRatePlan, BillRun, Contact)"),
287
+ }),
288
+ // Phase 3: Write Operations
289
+ createPayment: z.object({
290
+ accountId: z
291
+ .string()
292
+ .min(1)
293
+ .describe("Zuora account UUID to create the payment for"),
294
+ amount: z
295
+ .number()
296
+ .positive()
297
+ .max(1_000_000)
298
+ .refine((n) => Math.round(n * 100) === n * 100, "Amount must have at most 2 decimal places")
299
+ .describe("Payment amount (positive, max 1,000,000, up to 2 decimal places)"),
300
+ effectiveDate: z
301
+ .string()
302
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
303
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
304
+ .describe("Payment effective date in YYYY-MM-DD format"),
305
+ type: z
306
+ .enum(["Electronic", "External"])
307
+ .describe("Payment type: Electronic (gateway) or External (check, wire, etc.)"),
308
+ paymentMethodId: z
309
+ .string()
310
+ .optional()
311
+ .describe("Payment method ID. Required for Electronic type. " +
312
+ "Use get_account to find available payment methods."),
313
+ comment: z
314
+ .string()
315
+ .max(1000)
316
+ .optional()
317
+ .describe("Optional comment for the payment record"),
318
+ idempotencyKey: z
319
+ .string()
320
+ .uuid("Idempotency key must be a valid UUID")
321
+ .describe("UUID to prevent duplicate payments on retries. " +
322
+ "Generate a new UUID v4 for each distinct payment intent."),
323
+ }).superRefine((data, ctx) => {
324
+ if (data.type === "Electronic" && !data.paymentMethodId) {
325
+ ctx.addIssue({
326
+ code: z.ZodIssueCode.custom,
327
+ path: ["paymentMethodId"],
328
+ message: "paymentMethodId is required when type is Electronic",
329
+ });
330
+ }
331
+ }),
332
+ applyPayment: z.object({
333
+ paymentId: z
334
+ .string()
335
+ .min(1)
336
+ .describe("ID of the unapplied payment to apply to invoices"),
337
+ invoices: z
338
+ .array(z.object({
339
+ invoiceId: z.string().min(1).describe("Invoice ID to apply payment to"),
340
+ amount: z
341
+ .number()
342
+ .positive()
343
+ .max(1_000_000)
344
+ .refine((n) => Math.round(n * 100) === n * 100, "Amount must have at most 2 decimal places")
345
+ .describe("Amount to apply to this invoice"),
346
+ }))
347
+ .min(1)
348
+ .max(50)
349
+ .describe("Invoice allocations: which invoices to apply the payment to and how much. " +
350
+ "Max 50 per call; split larger batches into multiple calls."),
351
+ effectiveDate: z
352
+ .string()
353
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
354
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
355
+ .optional()
356
+ .describe("Effective date for the application in YYYY-MM-DD format (optional)"),
357
+ }),
358
+ createInvoice: z.object({
359
+ accountId: z
360
+ .string()
361
+ .min(1)
362
+ .describe("Zuora account UUID to create the invoice for"),
363
+ invoiceDate: z
364
+ .string()
365
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
366
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
367
+ .describe("Invoice date in YYYY-MM-DD format"),
368
+ dueDate: z
369
+ .string()
370
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
371
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
372
+ .optional()
373
+ .describe("Due date in YYYY-MM-DD format (optional, uses payment terms if omitted)"),
374
+ idempotencyKey: z
375
+ .string()
376
+ .uuid("Idempotency key must be a valid UUID")
377
+ .describe("UUID to prevent duplicate invoice creation on retries. " +
378
+ "Generate a new UUID v4 for each distinct invoice intent."),
379
+ }),
380
+ postInvoice: z.object({
381
+ invoiceId: z
382
+ .string()
383
+ .min(1)
384
+ .describe("ID of the draft invoice to post. " +
385
+ "Once posted, the invoice is immutable and affects account balance."),
386
+ }),
387
+ cancelSubscription: z.object({
388
+ subscriptionKey: z
389
+ .string()
390
+ .min(1)
391
+ .describe("Subscription key or ID (e.g., A-S00000001) to cancel"),
392
+ cancellationPolicy: z
393
+ .enum(["EndOfCurrentTerm", "EndOfLastInvoicePeriod", "SpecificDate"])
394
+ .describe("When the cancellation takes effect: " +
395
+ "EndOfCurrentTerm (cancel at term end), " +
396
+ "EndOfLastInvoicePeriod (cancel at end of last billed period), " +
397
+ "SpecificDate (cancel on a specific date, requires cancellationEffectiveDate)"),
398
+ cancellationEffectiveDate: z
399
+ .string()
400
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
401
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
402
+ .optional()
403
+ .describe("Required when cancellationPolicy is SpecificDate. The date cancellation takes effect."),
404
+ invoiceCollect: z
405
+ .boolean()
406
+ .default(false)
407
+ .describe("Whether to generate and collect an invoice for the cancellation charges (default: false)"),
408
+ }).superRefine((data, ctx) => {
409
+ if (data.cancellationPolicy === "SpecificDate" && !data.cancellationEffectiveDate) {
410
+ ctx.addIssue({
411
+ code: z.ZodIssueCode.custom,
412
+ path: ["cancellationEffectiveDate"],
413
+ message: "cancellationEffectiveDate is required when cancellationPolicy is SpecificDate",
414
+ });
415
+ }
416
+ }),
417
+ // Phase 4: Advanced Operations
418
+ createSubscription: z.object({
419
+ accountKey: z
420
+ .string()
421
+ .min(1)
422
+ .describe("Account ID or account number to create the subscription for"),
423
+ contractEffectiveDate: z
424
+ .string()
425
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
426
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
427
+ .describe("Date the subscription contract takes effect in YYYY-MM-DD format"),
428
+ serviceActivationDate: z
429
+ .string()
430
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
431
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
432
+ .optional()
433
+ .describe("Date the subscription service is activated (optional). " +
434
+ "Required if the Zuora tenant has 'Require Service Activation' enabled."),
435
+ customerAcceptanceDate: z
436
+ .string()
437
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
438
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
439
+ .optional()
440
+ .describe("Date the customer accepted the subscription (optional). " +
441
+ "Required if the Zuora tenant has 'Require Customer Acceptance' enabled."),
442
+ termType: z
443
+ .enum(["TERMED", "EVERGREEN"])
444
+ .describe("Subscription term type: TERMED (fixed duration) or EVERGREEN (no end date)"),
445
+ initialTerm: z
446
+ .number()
447
+ .int()
448
+ .positive()
449
+ .max(1200)
450
+ .optional()
451
+ .describe("Initial term length (required for TERMED). Max 1200."),
452
+ initialTermPeriodType: z
453
+ .enum(["Month", "Year", "Day", "Week"])
454
+ .optional()
455
+ .describe("Period type for the initial term (required for TERMED)"),
456
+ renewalTerm: z
457
+ .number()
458
+ .int()
459
+ .positive()
460
+ .max(1200)
461
+ .optional()
462
+ .describe("Renewal term length (optional)"),
463
+ renewalTermPeriodType: z
464
+ .enum(["Month", "Year", "Day", "Week"])
465
+ .optional()
466
+ .describe("Period type for the renewal term"),
467
+ autoRenew: z
468
+ .boolean()
469
+ .default(false)
470
+ .describe("Whether the subscription auto-renews at term end (default: false)"),
471
+ subscribeToRatePlans: z
472
+ .array(z.object({
473
+ productRatePlanId: z
474
+ .string()
475
+ .min(1)
476
+ .describe("Product rate plan ID from the product catalog"),
477
+ }))
478
+ .min(1)
479
+ .max(50)
480
+ .describe("Rate plans to subscribe to. Use list_products to find productRatePlanIds. " +
481
+ "At least 1, max 50 per call."),
482
+ notes: z
483
+ .string()
484
+ .max(2000)
485
+ .optional()
486
+ .describe("Optional notes for the subscription"),
487
+ idempotencyKey: z
488
+ .string()
489
+ .uuid("Idempotency key must be a valid UUID")
490
+ .describe("UUID to prevent duplicate subscriptions on retries. " +
491
+ "Generate a new UUID v4 for each distinct subscription intent."),
492
+ }).superRefine((data, ctx) => {
493
+ if (data.termType === "TERMED" && !data.initialTerm) {
494
+ ctx.addIssue({
495
+ code: z.ZodIssueCode.custom,
496
+ path: ["initialTerm"],
497
+ message: "initialTerm is required when termType is TERMED",
498
+ });
499
+ }
500
+ if (data.termType === "TERMED" && !data.initialTermPeriodType) {
501
+ ctx.addIssue({
502
+ code: z.ZodIssueCode.custom,
503
+ path: ["initialTermPeriodType"],
504
+ message: "initialTermPeriodType is required when termType is TERMED",
505
+ });
506
+ }
507
+ if ((data.renewalTerm !== undefined) !== (data.renewalTermPeriodType !== undefined)) {
508
+ ctx.addIssue({
509
+ code: z.ZodIssueCode.custom,
510
+ path: [data.renewalTerm !== undefined ? "renewalTermPeriodType" : "renewalTerm"],
511
+ message: "renewalTerm and renewalTermPeriodType must be provided together",
512
+ });
513
+ }
514
+ }),
515
+ updateSubscription: z.object({
516
+ subscriptionKey: z
517
+ .string()
518
+ .min(1)
519
+ .describe("Subscription key or ID to update (e.g., A-S00000001)"),
520
+ autoRenew: z
521
+ .boolean()
522
+ .optional()
523
+ .describe("Whether the subscription auto-renews at term end"),
524
+ renewalTerm: z
525
+ .number()
526
+ .int()
527
+ .positive()
528
+ .max(1200)
529
+ .optional()
530
+ .describe("Renewal term length"),
531
+ renewalTermPeriodType: z
532
+ .enum(["Month", "Year", "Day", "Week"])
533
+ .optional()
534
+ .describe("Period type for the renewal term"),
535
+ notes: z
536
+ .string()
537
+ .max(2000)
538
+ .optional()
539
+ .describe("Notes for the subscription"),
540
+ }).superRefine((data, ctx) => {
541
+ const { subscriptionKey: _, ...updateFields } = data;
542
+ const hasUpdate = Object.values(updateFields).some((v) => v !== undefined);
543
+ if (!hasUpdate) {
544
+ ctx.addIssue({
545
+ code: z.ZodIssueCode.custom,
546
+ path: [],
547
+ message: "At least one field to update must be provided (autoRenew, renewalTerm, renewalTermPeriodType, or notes)",
548
+ });
549
+ }
550
+ if ((data.renewalTerm !== undefined) !== (data.renewalTermPeriodType !== undefined)) {
551
+ ctx.addIssue({
552
+ code: z.ZodIssueCode.custom,
553
+ path: [data.renewalTerm !== undefined ? "renewalTermPeriodType" : "renewalTerm"],
554
+ message: "renewalTerm and renewalTermPeriodType must be provided together",
555
+ });
556
+ }
557
+ }),
558
+ createAccount: z.object({
559
+ name: z
560
+ .string()
561
+ .min(1)
562
+ .max(255)
563
+ .describe("Account name"),
564
+ currency: z
565
+ .string()
566
+ .regex(/^[A-Z]{3}$/, "Currency must be a 3-letter ISO 4217 code (e.g., USD, EUR)")
567
+ .describe("ISO 4217 currency code (e.g., USD, EUR, GBP)"),
568
+ billCycleDay: z
569
+ .number()
570
+ .int()
571
+ .min(1)
572
+ .max(31)
573
+ .describe("Day of month for billing cycle (1-31). Zuora normalizes values beyond a month's last day."),
574
+ billToContact: contactSchema
575
+ .describe("Bill-to contact information (required)"),
576
+ soldToContact: contactSchema
577
+ .optional()
578
+ .describe("Sold-to contact information (optional, defaults to bill-to if omitted)"),
579
+ paymentTerm: z
580
+ .string()
581
+ .max(100)
582
+ .optional()
583
+ .describe("Payment terms (e.g., 'Net 30', 'Due Upon Receipt')"),
584
+ autoPay: z
585
+ .boolean()
586
+ .default(false)
587
+ .describe("Whether to auto-collect payments (default: false)"),
588
+ notes: z
589
+ .string()
590
+ .max(2000)
591
+ .optional()
592
+ .describe("Optional account notes"),
593
+ idempotencyKey: z
594
+ .string()
595
+ .uuid("Idempotency key must be a valid UUID")
596
+ .describe("UUID to prevent duplicate account creation on retries. " +
597
+ "Generate a new UUID v4 for each distinct account intent."),
598
+ }),
599
+ updateAccount: z.object({
600
+ accountKey: z
601
+ .string()
602
+ .min(1)
603
+ .describe("Account ID or account number to update"),
604
+ name: z
605
+ .string()
606
+ .min(1)
607
+ .max(255)
608
+ .optional()
609
+ .describe("Updated account name"),
610
+ notes: z
611
+ .string()
612
+ .max(2000)
613
+ .optional()
614
+ .describe("Updated account notes"),
615
+ autoPay: z
616
+ .boolean()
617
+ .optional()
618
+ .describe("Whether to auto-collect payments"),
619
+ paymentTerm: z
620
+ .string()
621
+ .max(100)
622
+ .optional()
623
+ .describe("Updated payment terms (e.g., 'Net 30')"),
624
+ billToContact: partialContactSchema
625
+ .optional()
626
+ .describe("Updated bill-to contact fields (only include fields to change)"),
627
+ soldToContact: partialContactSchema
628
+ .optional()
629
+ .describe("Updated sold-to contact fields (only include fields to change)"),
630
+ }).superRefine((data, ctx) => {
631
+ const { accountKey: _, ...updateFields } = data;
632
+ const hasUpdate = Object.values(updateFields).some((v) => v !== undefined);
633
+ if (!hasUpdate) {
634
+ ctx.addIssue({
635
+ code: z.ZodIssueCode.custom,
636
+ path: [],
637
+ message: "At least one field to update must be provided",
638
+ });
639
+ }
640
+ }),
641
+ createRefund: z.object({
642
+ paymentId: z
643
+ .string()
644
+ .min(1)
645
+ .describe("ID of the original payment to refund. " +
646
+ "Use get_payment to verify the payment exists and has sufficient applied amount."),
647
+ amount: z
648
+ .number()
649
+ .positive()
650
+ .max(1_000_000)
651
+ .refine((n) => Math.round(n * 100) === n * 100, "Amount must have at most 2 decimal places")
652
+ .describe("Refund amount (positive, max 1,000,000, up to 2 decimal places)"),
653
+ type: z
654
+ .enum(["Electronic", "External"])
655
+ .describe("Refund type: Electronic (refund via payment gateway) or External (manual refund record)"),
656
+ comment: z
657
+ .string()
658
+ .max(1000)
659
+ .optional()
660
+ .describe("Optional reason/comment for the refund"),
661
+ idempotencyKey: z
662
+ .string()
663
+ .uuid("Idempotency key must be a valid UUID")
664
+ .describe("UUID to prevent duplicate refunds on retries. " +
665
+ "Generate a new UUID v4 for each distinct refund intent."),
666
+ }),
667
+ // Bill Run Write Tools
668
+ createBillRun: z.object({
669
+ targetDate: z
670
+ .string()
671
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
672
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
673
+ .describe("Target date for the bill run in YYYY-MM-DD format. Invoices are generated for charges through this date."),
674
+ invoiceDate: z
675
+ .string()
676
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
677
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
678
+ .optional()
679
+ .describe("Invoice date in YYYY-MM-DD format (optional, defaults to targetDate)"),
680
+ autoPost: z
681
+ .boolean()
682
+ .default(false)
683
+ .describe("Automatically post invoices after generation (default: false)"),
684
+ autoEmail: z
685
+ .boolean()
686
+ .default(false)
687
+ .describe("Automatically email invoices after posting (default: false). Requires autoPost to be true."),
688
+ name: z
689
+ .string()
690
+ .max(255)
691
+ .optional()
692
+ .describe("Optional name/label for the bill run"),
693
+ idempotencyKey: z
694
+ .string()
695
+ .uuid("Idempotency key must be a valid UUID")
696
+ .describe("UUID to prevent duplicate bill runs on retries. " +
697
+ "Generate a new UUID v4 for each distinct bill run intent."),
698
+ }).superRefine((data, ctx) => {
699
+ if (data.autoEmail && !data.autoPost) {
700
+ ctx.addIssue({
701
+ code: z.ZodIssueCode.custom,
702
+ path: ["autoEmail"],
703
+ message: "autoEmail requires autoPost to be true — invoices must be posted before they can be emailed",
704
+ });
705
+ }
706
+ }),
707
+ // Contact Write Tools
708
+ createContact: z.object({
709
+ accountId: z
710
+ .string()
711
+ .min(1)
712
+ .describe("Zuora account UUID to create the contact for"),
713
+ firstName: z
714
+ .string()
715
+ .min(1)
716
+ .max(100)
717
+ .describe("Contact first name"),
718
+ lastName: z
719
+ .string()
720
+ .min(1)
721
+ .max(100)
722
+ .describe("Contact last name"),
723
+ workEmail: z
724
+ .string()
725
+ .email("Must be a valid email address")
726
+ .optional()
727
+ .describe("Work email address"),
728
+ personalEmail: z
729
+ .string()
730
+ .email("Must be a valid email address")
731
+ .optional()
732
+ .describe("Personal email address"),
733
+ workPhone: z.string().max(40).optional().describe("Work phone number"),
734
+ homePhone: z.string().max(40).optional().describe("Home phone number"),
735
+ mobilePhone: z.string().max(40).optional().describe("Mobile phone number"),
736
+ fax: z.string().max(40).optional().describe("Fax number"),
737
+ address1: z.string().max(255).optional().describe("Street address line 1"),
738
+ address2: z.string().max(255).optional().describe("Street address line 2"),
739
+ city: z.string().max(100).optional().describe("City"),
740
+ state: z.string().max(100).optional().describe("State or province"),
741
+ country: z.string().max(100).optional().describe("Country (ISO 3166 name or code)"),
742
+ postalCode: z.string().max(20).optional().describe("Postal/ZIP code"),
743
+ county: z.string().max(100).optional().describe("County"),
744
+ taxRegion: z.string().max(100).optional().describe("Tax region"),
745
+ description: z.string().max(2000).optional().describe("Contact description/notes"),
746
+ nickname: z.string().max(100).optional().describe("Contact nickname"),
747
+ idempotencyKey: z
748
+ .string()
749
+ .uuid("Idempotency key must be a valid UUID")
750
+ .describe("UUID to prevent duplicate contact creation on retries. " +
751
+ "Generate a new UUID v4 for each distinct contact intent."),
752
+ }),
753
+ updateContact: z.object({
754
+ contactId: z
755
+ .string()
756
+ .min(1)
757
+ .describe("Contact ID to update"),
758
+ firstName: z.string().min(1).max(100).optional().describe("Updated first name"),
759
+ lastName: z.string().min(1).max(100).optional().describe("Updated last name"),
760
+ workEmail: z.string().email("Must be a valid email address").optional().describe("Updated work email"),
761
+ personalEmail: z.string().email("Must be a valid email address").optional().describe("Updated personal email"),
762
+ workPhone: z.string().max(40).optional().describe("Updated work phone"),
763
+ homePhone: z.string().max(40).optional().describe("Updated home phone"),
764
+ mobilePhone: z.string().max(40).optional().describe("Updated mobile phone"),
765
+ fax: z.string().max(40).optional().describe("Updated fax number"),
766
+ address1: z.string().max(255).optional().describe("Updated street address line 1"),
767
+ address2: z.string().max(255).optional().describe("Updated street address line 2"),
768
+ city: z.string().max(100).optional().describe("Updated city"),
769
+ state: z.string().max(100).optional().describe("Updated state or province"),
770
+ country: z.string().max(100).optional().describe("Updated country"),
771
+ postalCode: z.string().max(20).optional().describe("Updated postal/ZIP code"),
772
+ county: z.string().max(100).optional().describe("Updated county"),
773
+ taxRegion: z.string().max(100).optional().describe("Updated tax region"),
774
+ description: z.string().max(2000).optional().describe("Updated description/notes"),
775
+ nickname: z.string().max(100).optional().describe("Updated nickname"),
776
+ }).superRefine((data, ctx) => {
777
+ const { contactId: _, ...updateFields } = data;
778
+ const hasUpdate = Object.values(updateFields).some((v) => v !== undefined);
779
+ if (!hasUpdate) {
780
+ ctx.addIssue({
781
+ code: z.ZodIssueCode.custom,
782
+ path: [],
783
+ message: "At least one field to update must be provided",
784
+ });
785
+ }
786
+ }),
787
+ // ==================== Composite Tool Schemas ====================
788
+ findAccountsByProduct: z.object({
789
+ productName: z
790
+ .string()
791
+ .min(1)
792
+ .max(255)
793
+ .describe("Product name to search for (partial match supported). " +
794
+ "Example: 'Security Analytics Log Retention 13 Months'"),
795
+ limit: z
796
+ .number()
797
+ .int()
798
+ .min(1)
799
+ .max(100)
800
+ .default(25)
801
+ .describe("Maximum accounts to return (default: 25, max: 100)"),
802
+ }),
803
+ getOverdueInvoices: z.object({
804
+ minBalance: z
805
+ .number()
806
+ .min(0)
807
+ .default(0)
808
+ .describe("Minimum invoice balance to include (default: 0)"),
809
+ limit: z
810
+ .number()
811
+ .int()
812
+ .min(1)
813
+ .max(200)
814
+ .default(50)
815
+ .describe("Maximum invoices to return (default: 50, max: 200)"),
816
+ }),
817
+ getExpiringSubscriptions: z.object({
818
+ daysAhead: z
819
+ .number()
820
+ .int()
821
+ .min(1)
822
+ .max(365)
823
+ .default(30)
824
+ .describe("Days to look ahead for expiring subscriptions (default: 30, max: 365)"),
825
+ limit: z
826
+ .number()
827
+ .int()
828
+ .min(1)
829
+ .max(200)
830
+ .default(50)
831
+ .describe("Maximum subscriptions to return (default: 50, max: 200)"),
832
+ }),
833
+ getAccountBillingOverview: z.object({
834
+ accountKey: z
835
+ .string()
836
+ .min(1)
837
+ .describe("Account number or ID (e.g., A00012345)"),
838
+ }),
839
+ getRevenueByProduct: z.object({
840
+ limit: z
841
+ .number()
842
+ .int()
843
+ .min(1)
844
+ .max(100)
845
+ .default(50)
846
+ .describe("Maximum products to return (default: 50, max: 100)"),
847
+ }),
848
+ getPaymentReconciliation: z.object({
849
+ startDate: z
850
+ .string()
851
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
852
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
853
+ .describe("Start date for payment period (YYYY-MM-DD)"),
854
+ endDate: z
855
+ .string()
856
+ .regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD format")
857
+ .refine(isValidCalendarDate, "Must be a valid calendar date")
858
+ .describe("End date for payment period (YYYY-MM-DD)"),
859
+ limit: z
860
+ .number()
861
+ .int()
862
+ .min(1)
863
+ .max(500)
864
+ .default(100)
865
+ .describe("Maximum payments to return (default: 100, max: 500)"),
866
+ }),
867
+ getRecentlyCancelledSubscriptions: z.object({
868
+ daysBack: z
869
+ .number()
870
+ .int()
871
+ .min(1)
872
+ .max(365)
873
+ .default(30)
874
+ .describe("Days to look back for cancellations (default: 30, max: 365)"),
875
+ limit: z
876
+ .number()
877
+ .int()
878
+ .min(1)
879
+ .max(200)
880
+ .default(50)
881
+ .describe("Maximum subscriptions to return (default: 50, max: 200)"),
882
+ }),
883
+ getInvoiceAgingReport: z.object({
884
+ limit: z
885
+ .number()
886
+ .int()
887
+ .min(1)
888
+ .max(500)
889
+ .default(200)
890
+ .describe("Maximum invoices to analyze (default: 200, max: 500)"),
891
+ }),
892
+ getAccountHealthScorecard: z.object({
893
+ limit: z
894
+ .number()
895
+ .int()
896
+ .min(1)
897
+ .max(100)
898
+ .default(20)
899
+ .describe("Maximum at-risk accounts to return (default: 20, max: 100)"),
900
+ }),
901
+ findInvoicesByProduct: z.object({
902
+ productName: z
903
+ .string()
904
+ .min(1)
905
+ .max(255)
906
+ .describe("Product name to search for (partial match supported). " +
907
+ "Example: 'Security Analytics'"),
908
+ limit: z
909
+ .number()
910
+ .int()
911
+ .min(1)
912
+ .max(200)
913
+ .default(50)
914
+ .describe("Maximum invoice items to return (default: 50, max: 200)"),
915
+ }),
916
+ };
917
+ // ==================== Tool Handlers ====================
918
+ export class ToolHandlers {
919
+ client;
920
+ constructor(client) {
921
+ this.client = client;
922
+ }
923
+ // --- Account Handlers ---
924
+ async getAccount(input) {
925
+ try {
926
+ const { accountKey } = schemas.getAccount.parse(input);
927
+ const account = await this.client.getAccount(accountKey);
928
+ return {
929
+ success: true,
930
+ message: `Retrieved account ${accountKey}`,
931
+ data: account,
932
+ };
933
+ }
934
+ catch (error) {
935
+ return {
936
+ success: false,
937
+ message: `Failed to get account: ${error instanceof Error ? error.message : String(error)}`,
938
+ };
939
+ }
940
+ }
941
+ async getAccountSummary(input) {
942
+ try {
943
+ const { accountKey } = schemas.getAccountSummary.parse(input);
944
+ const summary = await this.client.getAccountSummary(accountKey);
945
+ return {
946
+ success: true,
947
+ message: `Retrieved account summary for ${accountKey}`,
948
+ data: summary,
949
+ };
950
+ }
951
+ catch (error) {
952
+ return {
953
+ success: false,
954
+ message: `Failed to get account summary: ${error instanceof Error ? error.message : String(error)}`,
955
+ };
956
+ }
957
+ }
958
+ // --- Invoice Handlers ---
959
+ async getInvoice(input) {
960
+ try {
961
+ const { invoiceId } = schemas.getInvoice.parse(input);
962
+ const invoice = await this.client.getInvoice(invoiceId);
963
+ return {
964
+ success: true,
965
+ message: `Retrieved invoice ${invoice.invoiceNumber ?? invoiceId}`,
966
+ data: invoice,
967
+ };
968
+ }
969
+ catch (error) {
970
+ return {
971
+ success: false,
972
+ message: `Failed to get invoice: ${error instanceof Error ? error.message : String(error)}`,
973
+ };
974
+ }
975
+ }
976
+ async listInvoices(input) {
977
+ try {
978
+ const { accountKey, page, pageSize } = schemas.listInvoices.parse(input);
979
+ const result = await this.client.listInvoices(accountKey, page, pageSize);
980
+ const count = result.invoices?.length ?? 0;
981
+ return {
982
+ success: true,
983
+ message: `Found ${count} invoice(s) for account ${accountKey}`,
984
+ data: result,
985
+ };
986
+ }
987
+ catch (error) {
988
+ return {
989
+ success: false,
990
+ message: `Failed to list invoices: ${error instanceof Error ? error.message : String(error)}`,
991
+ };
992
+ }
993
+ }
994
+ // --- Subscription Handlers ---
995
+ async getSubscription(input) {
996
+ try {
997
+ const { subscriptionKey } = schemas.getSubscription.parse(input);
998
+ const subscription = await this.client.getSubscription(subscriptionKey);
999
+ return {
1000
+ success: true,
1001
+ message: `Retrieved subscription ${subscription.subscriptionNumber ?? subscriptionKey}`,
1002
+ data: subscription,
1003
+ };
1004
+ }
1005
+ catch (error) {
1006
+ return {
1007
+ success: false,
1008
+ message: `Failed to get subscription: ${error instanceof Error ? error.message : String(error)}`,
1009
+ };
1010
+ }
1011
+ }
1012
+ async listSubscriptions(input) {
1013
+ try {
1014
+ const { accountKey } = schemas.listSubscriptions.parse(input);
1015
+ const result = await this.client.listSubscriptionsByAccount(accountKey);
1016
+ const count = result.subscriptions?.length ?? 0;
1017
+ return {
1018
+ success: true,
1019
+ message: `Found ${count} subscription(s) for account ${accountKey}`,
1020
+ data: result,
1021
+ };
1022
+ }
1023
+ catch (error) {
1024
+ return {
1025
+ success: false,
1026
+ message: `Failed to list subscriptions: ${error instanceof Error ? error.message : String(error)}`,
1027
+ };
1028
+ }
1029
+ }
1030
+ // --- Payment Handlers ---
1031
+ async getPayment(input) {
1032
+ try {
1033
+ const { paymentId } = schemas.getPayment.parse(input);
1034
+ const payment = await this.client.getPayment(paymentId);
1035
+ return {
1036
+ success: true,
1037
+ message: `Retrieved payment ${payment.paymentNumber ?? paymentId}`,
1038
+ data: payment,
1039
+ };
1040
+ }
1041
+ catch (error) {
1042
+ return {
1043
+ success: false,
1044
+ message: `Failed to get payment: ${error instanceof Error ? error.message : String(error)}`,
1045
+ };
1046
+ }
1047
+ }
1048
+ async listPayments(input) {
1049
+ try {
1050
+ const { accountKey, page, pageSize } = schemas.listPayments.parse(input);
1051
+ const result = accountKey
1052
+ ? await this.client.listPaymentsByAccount(accountKey, page, pageSize)
1053
+ : await this.client.listPayments(page, pageSize);
1054
+ const count = result.payments?.length ?? 0;
1055
+ const scope = accountKey
1056
+ ? ` for account ${accountKey}`
1057
+ : "";
1058
+ return {
1059
+ success: true,
1060
+ message: `Found ${count} payment(s)${scope}`,
1061
+ data: result,
1062
+ };
1063
+ }
1064
+ catch (error) {
1065
+ return {
1066
+ success: false,
1067
+ message: `Failed to list payments: ${error instanceof Error ? error.message : String(error)}`,
1068
+ };
1069
+ }
1070
+ }
1071
+ // --- ZOQL Query Handlers ---
1072
+ async executeZoqlQuery(input) {
1073
+ try {
1074
+ const { zoqlQuery } = schemas.executeZoqlQuery.parse(input);
1075
+ const result = await this.client.executeQuery(zoqlQuery);
1076
+ const moreAvailable = result.queryLocator
1077
+ ? " (more available via continue_zoql_query)"
1078
+ : "";
1079
+ return {
1080
+ success: true,
1081
+ message: `Query returned ${result.records?.length ?? 0} record(s)${moreAvailable}`,
1082
+ data: result,
1083
+ };
1084
+ }
1085
+ catch (error) {
1086
+ return {
1087
+ success: false,
1088
+ message: `ZOQL query failed: ${error instanceof Error ? error.message : String(error)}`,
1089
+ };
1090
+ }
1091
+ }
1092
+ async continueZoqlQuery(input) {
1093
+ try {
1094
+ const { queryLocator } = schemas.continueZoqlQuery.parse(input);
1095
+ const result = await this.client.queryMore(queryLocator);
1096
+ const moreAvailable = result.queryLocator
1097
+ ? " (more available)"
1098
+ : "";
1099
+ return {
1100
+ success: true,
1101
+ message: `Continuation returned ${result.records?.length ?? 0} record(s)${moreAvailable}`,
1102
+ data: result,
1103
+ };
1104
+ }
1105
+ catch (error) {
1106
+ return {
1107
+ success: false,
1108
+ message: `ZOQL continuation failed: ${error instanceof Error ? error.message : String(error)}`,
1109
+ };
1110
+ }
1111
+ }
1112
+ // --- Product Catalog Handlers ---
1113
+ async listProducts(input) {
1114
+ try {
1115
+ const { page, pageSize } = schemas.listProducts.parse(input);
1116
+ const result = await this.client.listProducts(page, pageSize);
1117
+ const count = result.products?.length ?? 0;
1118
+ return {
1119
+ success: true,
1120
+ message: `Found ${count} product(s)`,
1121
+ data: result,
1122
+ };
1123
+ }
1124
+ catch (error) {
1125
+ return {
1126
+ success: false,
1127
+ message: `Failed to list products: ${error instanceof Error ? error.message : String(error)}`,
1128
+ };
1129
+ }
1130
+ }
1131
+ // --- Phase 2: Extended Read Handlers ---
1132
+ async getInvoiceFiles(input) {
1133
+ try {
1134
+ const { invoiceId } = schemas.getInvoiceFiles.parse(input);
1135
+ const result = await this.client.getInvoiceFiles(invoiceId);
1136
+ const count = result.invoiceFiles?.length ?? 0;
1137
+ return {
1138
+ success: true,
1139
+ message: `Found ${count} file(s) for invoice ${invoiceId}`,
1140
+ data: result,
1141
+ };
1142
+ }
1143
+ catch (error) {
1144
+ return {
1145
+ success: false,
1146
+ message: `Failed to get invoice files: ${error instanceof Error ? error.message : String(error)}`,
1147
+ };
1148
+ }
1149
+ }
1150
+ async getCreditMemo(input) {
1151
+ try {
1152
+ const { creditMemoId } = schemas.getCreditMemo.parse(input);
1153
+ const creditMemo = await this.client.getCreditMemo(creditMemoId);
1154
+ return {
1155
+ success: true,
1156
+ message: `Retrieved credit memo ${creditMemo.number ?? creditMemoId}`,
1157
+ data: creditMemo,
1158
+ };
1159
+ }
1160
+ catch (error) {
1161
+ return {
1162
+ success: false,
1163
+ message: `Failed to get credit memo: ${error instanceof Error ? error.message : String(error)}`,
1164
+ };
1165
+ }
1166
+ }
1167
+ async listCreditMemos(input) {
1168
+ try {
1169
+ const { accountId, page, pageSize } = schemas.listCreditMemos.parse(input);
1170
+ const result = await this.client.listCreditMemos(accountId, page, pageSize);
1171
+ const count = result.creditMemos?.length ?? 0;
1172
+ const scope = accountId ? ` for account ${accountId}` : "";
1173
+ return {
1174
+ success: true,
1175
+ message: `Found ${count} credit memo(s)${scope}`,
1176
+ data: result,
1177
+ };
1178
+ }
1179
+ catch (error) {
1180
+ return {
1181
+ success: false,
1182
+ message: `Failed to list credit memos: ${error instanceof Error ? error.message : String(error)}`,
1183
+ };
1184
+ }
1185
+ }
1186
+ async searchAccounts(input) {
1187
+ try {
1188
+ const { field, value, operator } = schemas.searchAccounts.parse(input);
1189
+ const result = await this.client.searchAccounts(field, value, operator);
1190
+ const count = result.records?.length ?? 0;
1191
+ return {
1192
+ success: true,
1193
+ message: `Found ${count} account(s) matching search criteria`,
1194
+ data: result,
1195
+ };
1196
+ }
1197
+ catch (error) {
1198
+ return {
1199
+ success: false,
1200
+ message: `Failed to search accounts: ${error instanceof Error ? error.message : String(error)}`,
1201
+ };
1202
+ }
1203
+ }
1204
+ async listUsage(input) {
1205
+ try {
1206
+ const { accountKey, page, pageSize } = schemas.listUsage.parse(input);
1207
+ const result = await this.client.listUsage(accountKey, page, pageSize);
1208
+ const count = result.usage?.length ?? 0;
1209
+ return {
1210
+ success: true,
1211
+ message: `Found ${count} usage record(s) for account ${accountKey}`,
1212
+ data: result,
1213
+ };
1214
+ }
1215
+ catch (error) {
1216
+ return {
1217
+ success: false,
1218
+ message: `Failed to get usage: ${error instanceof Error ? error.message : String(error)}`,
1219
+ };
1220
+ }
1221
+ }
1222
+ // --- User Management Handlers ---
1223
+ async listUsers(input) {
1224
+ try {
1225
+ const { startIndex, count, filter } = schemas.listUsers.parse(input);
1226
+ const result = await this.client.listUsers(startIndex, count, filter);
1227
+ const userCount = result.Resources?.length ?? 0;
1228
+ const filterNote = filter ? ` (filter: ${filter})` : "";
1229
+ return {
1230
+ success: true,
1231
+ message: `Found ${userCount} user(s) of ${result.totalResults} total${filterNote}`,
1232
+ data: result,
1233
+ };
1234
+ }
1235
+ catch (error) {
1236
+ return {
1237
+ success: false,
1238
+ message: `Failed to list users: ${error instanceof Error ? error.message : String(error)}`,
1239
+ };
1240
+ }
1241
+ }
1242
+ async getUser(input) {
1243
+ try {
1244
+ const { userId } = schemas.getUser.parse(input);
1245
+ const user = await this.client.getUser(userId);
1246
+ return {
1247
+ success: true,
1248
+ message: `Retrieved user ${user.userName ?? userId}`,
1249
+ data: user,
1250
+ };
1251
+ }
1252
+ catch (error) {
1253
+ return {
1254
+ success: false,
1255
+ message: `Failed to get user: ${error instanceof Error ? error.message : String(error)}`,
1256
+ };
1257
+ }
1258
+ }
1259
+ // --- Bill Run Handlers ---
1260
+ async getBillRun(input) {
1261
+ try {
1262
+ const { billRunId } = schemas.getBillRun.parse(input);
1263
+ const billRun = await this.client.getBillRun(billRunId);
1264
+ return {
1265
+ success: true,
1266
+ message: `Retrieved bill run ${billRun.billRunNumber ?? billRunId} (status: ${billRun.status})`,
1267
+ data: billRun,
1268
+ };
1269
+ }
1270
+ catch (error) {
1271
+ return {
1272
+ success: false,
1273
+ message: `Failed to get bill run: ${error instanceof Error ? error.message : String(error)}`,
1274
+ };
1275
+ }
1276
+ }
1277
+ async listBillRuns(input) {
1278
+ try {
1279
+ const { page, pageSize } = schemas.listBillRuns.parse(input);
1280
+ const result = await this.client.listBillRuns(page, pageSize);
1281
+ const count = result.billRuns?.length ?? 0;
1282
+ return {
1283
+ success: true,
1284
+ message: `Found ${count} bill run(s)`,
1285
+ data: result,
1286
+ };
1287
+ }
1288
+ catch (error) {
1289
+ return {
1290
+ success: false,
1291
+ message: `Failed to list bill runs: ${error instanceof Error ? error.message : String(error)}`,
1292
+ };
1293
+ }
1294
+ }
1295
+ // --- Contact Handlers ---
1296
+ async getContact(input) {
1297
+ try {
1298
+ const { contactId } = schemas.getContact.parse(input);
1299
+ const contact = await this.client.getContact(contactId);
1300
+ return {
1301
+ success: true,
1302
+ message: `Retrieved contact ${contact.firstName} ${contact.lastName} (${contactId})`,
1303
+ data: contact,
1304
+ };
1305
+ }
1306
+ catch (error) {
1307
+ return {
1308
+ success: false,
1309
+ message: `Failed to get contact: ${error instanceof Error ? error.message : String(error)}`,
1310
+ };
1311
+ }
1312
+ }
1313
+ // --- Describe API Handlers ---
1314
+ async describeObject(input) {
1315
+ try {
1316
+ const { objectType } = schemas.describeObject.parse(input);
1317
+ const result = await this.client.describeObject(objectType);
1318
+ return {
1319
+ success: true,
1320
+ message: `Described ${result.objectName}: ${result.fieldCount} field(s)`,
1321
+ data: result,
1322
+ };
1323
+ }
1324
+ catch (error) {
1325
+ return {
1326
+ success: false,
1327
+ message: `Failed to describe object: ${error instanceof Error ? error.message : String(error)}`,
1328
+ };
1329
+ }
1330
+ }
1331
+ // --- Phase 3: Write Operation Handlers ---
1332
+ async createPayment(input) {
1333
+ try {
1334
+ const { idempotencyKey, ...paymentData } = schemas.createPayment.parse(input);
1335
+ const result = await this.client.createPayment(paymentData, idempotencyKey);
1336
+ return {
1337
+ success: true,
1338
+ message: `Created payment ${result.paymentNumber} successfully`,
1339
+ data: result,
1340
+ };
1341
+ }
1342
+ catch (error) {
1343
+ return {
1344
+ success: false,
1345
+ message: `Failed to create payment: ${error instanceof Error ? error.message : String(error)}`,
1346
+ };
1347
+ }
1348
+ }
1349
+ async applyPayment(input) {
1350
+ try {
1351
+ const { paymentId, invoices, effectiveDate } = schemas.applyPayment.parse(input);
1352
+ const result = await this.client.applyPayment(paymentId, {
1353
+ invoices,
1354
+ effectiveDate,
1355
+ });
1356
+ return {
1357
+ success: true,
1358
+ message: `Applied payment ${paymentId} (applied: ${result.appliedAmount}) successfully`,
1359
+ data: result,
1360
+ };
1361
+ }
1362
+ catch (error) {
1363
+ return {
1364
+ success: false,
1365
+ message: `Failed to apply payment: ${error instanceof Error ? error.message : String(error)}`,
1366
+ };
1367
+ }
1368
+ }
1369
+ async createInvoice(input) {
1370
+ try {
1371
+ const { idempotencyKey, ...invoiceData } = schemas.createInvoice.parse(input);
1372
+ const result = await this.client.createInvoice(invoiceData, idempotencyKey);
1373
+ return {
1374
+ success: true,
1375
+ message: `Created draft invoice ${result.invoiceNumber} for account ${invoiceData.accountId}`,
1376
+ data: result,
1377
+ };
1378
+ }
1379
+ catch (error) {
1380
+ return {
1381
+ success: false,
1382
+ message: `Failed to create invoice: ${error instanceof Error ? error.message : String(error)}`,
1383
+ };
1384
+ }
1385
+ }
1386
+ async postInvoice(input) {
1387
+ try {
1388
+ const { invoiceId } = schemas.postInvoice.parse(input);
1389
+ const result = await this.client.postInvoice(invoiceId);
1390
+ return {
1391
+ success: true,
1392
+ message: `Posted invoice ${result.invoiceNumber} (status: ${result.status})`,
1393
+ data: result,
1394
+ };
1395
+ }
1396
+ catch (error) {
1397
+ return {
1398
+ success: false,
1399
+ message: `Failed to post invoice: ${error instanceof Error ? error.message : String(error)}`,
1400
+ };
1401
+ }
1402
+ }
1403
+ async cancelSubscription(input) {
1404
+ try {
1405
+ const { subscriptionKey, ...cancellationData } = schemas.cancelSubscription.parse(input);
1406
+ const result = await this.client.cancelSubscription(subscriptionKey, cancellationData);
1407
+ return {
1408
+ success: true,
1409
+ message: `Cancelled subscription ${subscriptionKey} (id: ${result.subscriptionId}) effective ${result.cancelledDate}`,
1410
+ data: result,
1411
+ };
1412
+ }
1413
+ catch (error) {
1414
+ return {
1415
+ success: false,
1416
+ message: `Failed to cancel subscription: ${error instanceof Error ? error.message : String(error)}`,
1417
+ };
1418
+ }
1419
+ }
1420
+ // --- Phase 4: Advanced Operation Handlers ---
1421
+ async createSubscription(input) {
1422
+ try {
1423
+ const { idempotencyKey, ...subscriptionData } = schemas.createSubscription.parse(input);
1424
+ const result = await this.client.createSubscription(subscriptionData, idempotencyKey);
1425
+ return {
1426
+ success: true,
1427
+ message: `Created subscription ${result.subscriptionNumber} for account ${subscriptionData.accountKey}`,
1428
+ data: result,
1429
+ };
1430
+ }
1431
+ catch (error) {
1432
+ return {
1433
+ success: false,
1434
+ message: `Failed to create subscription: ${error instanceof Error ? error.message : String(error)}`,
1435
+ };
1436
+ }
1437
+ }
1438
+ async updateSubscription(input) {
1439
+ try {
1440
+ const { subscriptionKey, ...updateData } = schemas.updateSubscription.parse(input);
1441
+ const result = await this.client.updateSubscription(subscriptionKey, updateData);
1442
+ return {
1443
+ success: true,
1444
+ message: `Updated subscription ${subscriptionKey} (id: ${result.subscriptionId})`,
1445
+ data: result,
1446
+ };
1447
+ }
1448
+ catch (error) {
1449
+ return {
1450
+ success: false,
1451
+ message: `Failed to update subscription: ${error instanceof Error ? error.message : String(error)}`,
1452
+ };
1453
+ }
1454
+ }
1455
+ async createAccount(input) {
1456
+ try {
1457
+ const { idempotencyKey, ...accountData } = schemas.createAccount.parse(input);
1458
+ const result = await this.client.createAccount(accountData, idempotencyKey);
1459
+ return {
1460
+ success: true,
1461
+ message: `Created account ${result.accountNumber} (id: ${result.accountId})`,
1462
+ data: result,
1463
+ };
1464
+ }
1465
+ catch (error) {
1466
+ return {
1467
+ success: false,
1468
+ message: `Failed to create account: ${error instanceof Error ? error.message : String(error)}`,
1469
+ };
1470
+ }
1471
+ }
1472
+ async updateAccount(input) {
1473
+ try {
1474
+ const { accountKey, ...updateData } = schemas.updateAccount.parse(input);
1475
+ const result = await this.client.updateAccount(accountKey, updateData);
1476
+ return {
1477
+ success: true,
1478
+ message: `Updated account ${accountKey} successfully`,
1479
+ data: result,
1480
+ };
1481
+ }
1482
+ catch (error) {
1483
+ return {
1484
+ success: false,
1485
+ message: `Failed to update account: ${error instanceof Error ? error.message : String(error)}`,
1486
+ };
1487
+ }
1488
+ }
1489
+ async createRefund(input) {
1490
+ try {
1491
+ const { idempotencyKey, paymentId, ...refundData } = schemas.createRefund.parse(input);
1492
+ const result = await this.client.createRefund(paymentId, refundData, idempotencyKey);
1493
+ return {
1494
+ success: true,
1495
+ message: `Created refund ${result.number} (status: ${result.status})`,
1496
+ data: result,
1497
+ };
1498
+ }
1499
+ catch (error) {
1500
+ return {
1501
+ success: false,
1502
+ message: `Failed to create refund: ${error instanceof Error ? error.message : String(error)}`,
1503
+ };
1504
+ }
1505
+ }
1506
+ // --- Bill Run Write Handlers ---
1507
+ async createBillRun(input) {
1508
+ try {
1509
+ const { idempotencyKey, ...billRunData } = schemas.createBillRun.parse(input);
1510
+ const result = await this.client.createBillRun(billRunData, idempotencyKey);
1511
+ return {
1512
+ success: true,
1513
+ message: `Created bill run ${result.billRunNumber} (status: ${result.status})`,
1514
+ data: result,
1515
+ };
1516
+ }
1517
+ catch (error) {
1518
+ return {
1519
+ success: false,
1520
+ message: `Failed to create bill run: ${error instanceof Error ? error.message : String(error)}`,
1521
+ };
1522
+ }
1523
+ }
1524
+ // --- Contact Write Handlers ---
1525
+ async createContact(input) {
1526
+ try {
1527
+ const { idempotencyKey, ...contactData } = schemas.createContact.parse(input);
1528
+ const result = await this.client.createContact(contactData, idempotencyKey);
1529
+ return {
1530
+ success: true,
1531
+ message: `Created contact (id: ${result.id}) for account ${contactData.accountId}`,
1532
+ data: result,
1533
+ };
1534
+ }
1535
+ catch (error) {
1536
+ return {
1537
+ success: false,
1538
+ message: `Failed to create contact: ${error instanceof Error ? error.message : String(error)}`,
1539
+ };
1540
+ }
1541
+ }
1542
+ async updateContact(input) {
1543
+ try {
1544
+ const { contactId, ...updateData } = schemas.updateContact.parse(input);
1545
+ const result = await this.client.updateContact(contactId, updateData);
1546
+ return {
1547
+ success: true,
1548
+ message: `Updated contact ${contactId} successfully`,
1549
+ data: result,
1550
+ };
1551
+ }
1552
+ catch (error) {
1553
+ return {
1554
+ success: false,
1555
+ message: `Failed to update contact: ${error instanceof Error ? error.message : String(error)}`,
1556
+ };
1557
+ }
1558
+ }
1559
+ // --- Composite Tool Handlers ---
1560
+ async findAccountsByProduct(input) {
1561
+ try {
1562
+ const { productName, limit } = schemas.findAccountsByProduct.parse(input);
1563
+ const safeName = escapeZoql(productName);
1564
+ // Step 1: Find ProductRatePlan IDs matching the product name
1565
+ const productRatePlans = await queryAll(this.client, `SELECT Id, Name FROM ProductRatePlan WHERE Name LIKE '%${safeName}%'`);
1566
+ if (productRatePlans.length === 0) {
1567
+ // Try searching by Product.Name instead
1568
+ const products = await queryAll(this.client, `SELECT Id, Name FROM Product WHERE Name LIKE '%${safeName}%'`);
1569
+ if (products.length === 0) {
1570
+ return {
1571
+ success: true,
1572
+ message: `No products found matching "${productName}"`,
1573
+ data: { accounts: [], totalFound: 0 },
1574
+ };
1575
+ }
1576
+ const productIds = collectIds(products, "Id");
1577
+ const prpFromProduct = await queryWithBatchedIds(this.client, "Id, Name", "ProductRatePlan", "ProductId", productIds);
1578
+ if (prpFromProduct.length === 0) {
1579
+ return {
1580
+ success: true,
1581
+ message: `Product found but no rate plans configured for "${productName}"`,
1582
+ data: { accounts: [], totalFound: 0 },
1583
+ };
1584
+ }
1585
+ productRatePlans.push(...prpFromProduct);
1586
+ }
1587
+ const prpIds = collectIds(productRatePlans, "Id");
1588
+ // Step 2: Find RatePlans using those ProductRatePlanIds → get SubscriptionIds
1589
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "ProductRatePlanId", prpIds);
1590
+ if (ratePlans.length === 0) {
1591
+ return {
1592
+ success: true,
1593
+ message: `No active subscriptions found for product "${productName}"`,
1594
+ data: { accounts: [], totalFound: 0 },
1595
+ };
1596
+ }
1597
+ const subscriptionIds = collectIds(ratePlans, "SubscriptionId");
1598
+ // Step 3: Get Subscriptions → AccountIds
1599
+ const subscriptions = await queryWithBatchedIds(this.client, "Id, AccountId, SubscriptionNumber, Status, TermEndDate, ContractedMrr", "Subscription", "Id", subscriptionIds, "Status = 'Active'");
1600
+ if (subscriptions.length === 0) {
1601
+ return {
1602
+ success: true,
1603
+ message: `No active subscriptions found for product "${productName}"`,
1604
+ data: { accounts: [], totalFound: 0 },
1605
+ };
1606
+ }
1607
+ const accountIds = collectIds(subscriptions, "AccountId");
1608
+ // Step 4: Get Account details
1609
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name, Status", "Account", "Id", accountIds);
1610
+ // Build lookup maps
1611
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1612
+ const subsByAccount = new Map();
1613
+ for (const sub of subscriptions) {
1614
+ const acctId = getString(sub, "AccountId") ?? "";
1615
+ const existing = subsByAccount.get(acctId) ?? [];
1616
+ existing.push(sub);
1617
+ subsByAccount.set(acctId, existing);
1618
+ }
1619
+ // Build rate plan name lookup by subscription
1620
+ const rpNameBySub = new Map();
1621
+ for (const rp of ratePlans) {
1622
+ const subId = getString(rp, "SubscriptionId") ?? "";
1623
+ const existing = rpNameBySub.get(subId) ?? [];
1624
+ const name = getString(rp, "Name");
1625
+ if (name)
1626
+ existing.push(name);
1627
+ rpNameBySub.set(subId, existing);
1628
+ }
1629
+ // Assemble results
1630
+ const results = [];
1631
+ for (const [acctId, acct] of accountMap) {
1632
+ if (!acctId)
1633
+ continue;
1634
+ const acctSubs = subsByAccount.get(acctId) ?? [];
1635
+ results.push({
1636
+ accountId: acctId,
1637
+ accountNumber: getString(acct, "AccountNumber") ?? "",
1638
+ accountName: getString(acct, "Name") ?? "",
1639
+ accountStatus: getString(acct, "Status") ?? "",
1640
+ subscriptions: acctSubs.map((sub) => ({
1641
+ subscriptionId: getString(sub, "Id") ?? "",
1642
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
1643
+ status: getString(sub, "Status") ?? "",
1644
+ termEndDate: getString(sub, "TermEndDate"),
1645
+ ratePlanName: (rpNameBySub.get(getString(sub, "Id") ?? "") ?? []).join(", "),
1646
+ })),
1647
+ });
1648
+ if (results.length >= limit)
1649
+ break;
1650
+ }
1651
+ return {
1652
+ success: true,
1653
+ message: `Found ${results.length} account(s) with active subscriptions for "${productName}"`,
1654
+ data: { accounts: results, totalFound: results.length },
1655
+ };
1656
+ }
1657
+ catch (error) {
1658
+ return {
1659
+ success: false,
1660
+ message: `Failed to find accounts by product: ${error instanceof Error ? error.message : String(error)}`,
1661
+ };
1662
+ }
1663
+ }
1664
+ async getOverdueInvoices(input) {
1665
+ try {
1666
+ const { minBalance, limit } = schemas.getOverdueInvoices.parse(input);
1667
+ const today = todayString();
1668
+ // Query overdue invoices: Posted, has balance, past due date
1669
+ const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, AccountId, InvoiceDate, DueDate, Amount, Balance ` +
1670
+ `FROM Invoice ` +
1671
+ `WHERE Status = 'Posted' AND Balance > ${Number(minBalance)} AND DueDate < '${today}'`, limit);
1672
+ if (invoices.length === 0) {
1673
+ return {
1674
+ success: true,
1675
+ message: "No overdue invoices found",
1676
+ data: { invoices: [], totalFound: 0, totalOverdueBalance: 0 },
1677
+ };
1678
+ }
1679
+ // Get account details for the overdue invoices
1680
+ const accountIds = collectIds(invoices, "AccountId");
1681
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1682
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1683
+ // Build results
1684
+ const results = invoices.map((inv) => {
1685
+ const acctId = getString(inv, "AccountId") ?? "";
1686
+ const acct = accountMap.get(acctId);
1687
+ const dueDate = getString(inv, "DueDate") ?? today;
1688
+ return {
1689
+ invoiceId: getString(inv, "Id") ?? "",
1690
+ invoiceNumber: getString(inv, "InvoiceNumber") ?? "",
1691
+ accountId: acctId,
1692
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
1693
+ accountName: getString(acct ?? {}, "Name") ?? "",
1694
+ invoiceDate: getString(inv, "InvoiceDate") ?? "",
1695
+ dueDate,
1696
+ amount: getNumber(inv, "Amount") ?? 0,
1697
+ balance: getNumber(inv, "Balance") ?? 0,
1698
+ daysPastDue: daysBetween(dueDate, today),
1699
+ };
1700
+ });
1701
+ // Sort by days past due descending
1702
+ results.sort((a, b) => b.daysPastDue - a.daysPastDue);
1703
+ const totalOverdueBalance = results.reduce((sum, r) => sum + r.balance, 0);
1704
+ return {
1705
+ success: true,
1706
+ message: `Found ${results.length} overdue invoice(s) totaling $${totalOverdueBalance.toFixed(2)}`,
1707
+ data: {
1708
+ invoices: results.slice(0, limit),
1709
+ totalFound: results.length,
1710
+ totalOverdueBalance,
1711
+ },
1712
+ };
1713
+ }
1714
+ catch (error) {
1715
+ return {
1716
+ success: false,
1717
+ message: `Failed to get overdue invoices: ${error instanceof Error ? error.message : String(error)}`,
1718
+ };
1719
+ }
1720
+ }
1721
+ async getExpiringSubscriptions(input) {
1722
+ try {
1723
+ const { daysAhead, limit } = schemas.getExpiringSubscriptions.parse(input);
1724
+ const today = todayString();
1725
+ const futureDate = addDays(new Date(), daysAhead);
1726
+ // Query active subscriptions expiring within the window
1727
+ const subscriptions = await queryAll(this.client, `SELECT Id, SubscriptionNumber, AccountId, Status, TermEndDate, AutoRenew, ContractedMrr ` +
1728
+ `FROM Subscription ` +
1729
+ `WHERE Status = 'Active' AND TermEndDate >= '${today}' AND TermEndDate <= '${futureDate}'`, limit * 2 // fetch extra to account for filtering
1730
+ );
1731
+ if (subscriptions.length === 0) {
1732
+ return {
1733
+ success: true,
1734
+ message: `No subscriptions expiring in the next ${daysAhead} days`,
1735
+ data: { subscriptions: [], totalFound: 0, totalMrrAtRisk: 0 },
1736
+ };
1737
+ }
1738
+ // Get account details
1739
+ const accountIds = collectIds(subscriptions, "AccountId");
1740
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1741
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1742
+ // Get rate plan names
1743
+ const subIds = collectIds(subscriptions, "Id");
1744
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "SubscriptionId", subIds);
1745
+ const rpNamesBySub = new Map();
1746
+ for (const rp of ratePlans) {
1747
+ const subId = getString(rp, "SubscriptionId") ?? "";
1748
+ const existing = rpNamesBySub.get(subId) ?? [];
1749
+ const name = getString(rp, "Name");
1750
+ if (name)
1751
+ existing.push(name);
1752
+ rpNamesBySub.set(subId, existing);
1753
+ }
1754
+ // Build results
1755
+ const results = subscriptions.map((sub) => {
1756
+ const acctId = getString(sub, "AccountId") ?? "";
1757
+ const acct = accountMap.get(acctId);
1758
+ const termEnd = getString(sub, "TermEndDate") ?? futureDate;
1759
+ const subId = getString(sub, "Id") ?? "";
1760
+ return {
1761
+ subscriptionId: subId,
1762
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
1763
+ accountId: acctId,
1764
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
1765
+ accountName: getString(acct ?? {}, "Name") ?? "",
1766
+ status: getString(sub, "Status") ?? "",
1767
+ termEndDate: termEnd,
1768
+ autoRenew: getString(sub, "AutoRenew") === "true",
1769
+ contractedMrr: getNumber(sub, "ContractedMrr") ?? 0,
1770
+ ratePlanNames: rpNamesBySub.get(subId) ?? [],
1771
+ daysUntilExpiry: daysBetween(today, termEnd),
1772
+ };
1773
+ });
1774
+ results.sort((a, b) => a.daysUntilExpiry - b.daysUntilExpiry);
1775
+ const totalMrrAtRisk = results
1776
+ .filter((r) => !r.autoRenew)
1777
+ .reduce((sum, r) => sum + r.contractedMrr, 0);
1778
+ return {
1779
+ success: true,
1780
+ message: `Found ${results.length} subscription(s) expiring in the next ${daysAhead} days (MRR at risk: $${totalMrrAtRisk.toFixed(2)})`,
1781
+ data: {
1782
+ subscriptions: results.slice(0, limit),
1783
+ totalFound: results.length,
1784
+ totalMrrAtRisk,
1785
+ },
1786
+ };
1787
+ }
1788
+ catch (error) {
1789
+ return {
1790
+ success: false,
1791
+ message: `Failed to get expiring subscriptions: ${error instanceof Error ? error.message : String(error)}`,
1792
+ };
1793
+ }
1794
+ }
1795
+ async getAccountBillingOverview(input) {
1796
+ try {
1797
+ const { accountKey } = schemas.getAccountBillingOverview.parse(input);
1798
+ // Step 1: Get account via REST API
1799
+ const account = await this.client.getAccount(accountKey);
1800
+ // Step 2: Get recent invoices via ZOQL
1801
+ const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, InvoiceDate, DueDate, Amount, Balance, Status ` +
1802
+ `FROM Invoice WHERE AccountId = '${escapeZoql(account.id)}' ` +
1803
+ `ORDER BY InvoiceDate DESC`, 20);
1804
+ // Step 3: Get recent payments via ZOQL
1805
+ const payments = await queryAll(this.client, `SELECT Id, PaymentNumber, Amount, EffectiveDate, Status ` +
1806
+ `FROM Payment WHERE AccountId = '${escapeZoql(account.id)}' ` +
1807
+ `ORDER BY EffectiveDate DESC`, 10);
1808
+ // Step 4: Get subscriptions via REST API
1809
+ const subResult = await this.client.listSubscriptionsByAccount(accountKey);
1810
+ const subscriptions = subResult.subscriptions ?? [];
1811
+ // Compute invoice summary
1812
+ const today = todayString();
1813
+ let totalOutstanding = 0;
1814
+ let overdueCount = 0;
1815
+ let overdueAmount = 0;
1816
+ for (const inv of invoices) {
1817
+ const balance = getNumber(inv, "Balance") ?? 0;
1818
+ const status = getString(inv, "Status");
1819
+ if (status === "Posted" && balance > 0) {
1820
+ totalOutstanding += balance;
1821
+ const dueDate = getString(inv, "DueDate") ?? "";
1822
+ if (dueDate && dueDate < today) {
1823
+ overdueCount++;
1824
+ overdueAmount += balance;
1825
+ }
1826
+ }
1827
+ }
1828
+ const activeSubscriptions = subscriptions.filter((s) => s.status === "Active");
1829
+ const totalMrr = activeSubscriptions.reduce((sum, s) => sum + (s.contractedMrr ?? 0), 0);
1830
+ const overview = {
1831
+ account: {
1832
+ id: account.id,
1833
+ accountNumber: account.accountNumber,
1834
+ name: account.name,
1835
+ status: account.status,
1836
+ balance: account.balance,
1837
+ currency: account.currency,
1838
+ autoPay: account.autoPay,
1839
+ paymentTerm: account.paymentTerm,
1840
+ },
1841
+ invoiceSummary: {
1842
+ totalOutstanding,
1843
+ overdueCount,
1844
+ overdueAmount,
1845
+ recentInvoices: invoices.slice(0, 10).map((inv) => ({
1846
+ invoiceNumber: getString(inv, "InvoiceNumber") ?? "",
1847
+ invoiceDate: getString(inv, "InvoiceDate") ?? "",
1848
+ dueDate: getString(inv, "DueDate") ?? "",
1849
+ amount: getNumber(inv, "Amount") ?? 0,
1850
+ balance: getNumber(inv, "Balance") ?? 0,
1851
+ status: getString(inv, "Status") ?? "",
1852
+ })),
1853
+ },
1854
+ paymentSummary: {
1855
+ recentPayments: payments.slice(0, 5).map((p) => ({
1856
+ paymentNumber: getString(p, "PaymentNumber") ?? "",
1857
+ amount: getNumber(p, "Amount") ?? 0,
1858
+ effectiveDate: getString(p, "EffectiveDate") ?? "",
1859
+ status: getString(p, "Status") ?? "",
1860
+ })),
1861
+ },
1862
+ subscriptionSummary: {
1863
+ activeCount: activeSubscriptions.length,
1864
+ totalMrr,
1865
+ subscriptions: activeSubscriptions.slice(0, 10).map((s) => ({
1866
+ subscriptionNumber: s.subscriptionNumber,
1867
+ status: s.status,
1868
+ termEndDate: s.termEndDate,
1869
+ contractedMrr: s.contractedMrr,
1870
+ ratePlanNames: s.ratePlans?.map((rp) => rp.ratePlanName) ?? [],
1871
+ })),
1872
+ },
1873
+ };
1874
+ return {
1875
+ success: true,
1876
+ message: `Billing overview for ${account.name} (${account.accountNumber}): balance $${account.balance}, ${overdueCount} overdue, ${activeSubscriptions.length} active subscriptions`,
1877
+ data: overview,
1878
+ };
1879
+ }
1880
+ catch (error) {
1881
+ return {
1882
+ success: false,
1883
+ message: `Failed to get billing overview: ${error instanceof Error ? error.message : String(error)}`,
1884
+ };
1885
+ }
1886
+ }
1887
+ async getRevenueByProduct(input) {
1888
+ try {
1889
+ const { limit } = schemas.getRevenueByProduct.parse(input);
1890
+ // Step 1: Get all active subscriptions with MRR
1891
+ const subscriptions = await queryAll(this.client, `SELECT Id, AccountId, SubscriptionNumber, ContractedMrr, TotalContractedValue, Status ` +
1892
+ `FROM Subscription WHERE Status = 'Active'`, 5000);
1893
+ if (subscriptions.length === 0) {
1894
+ return {
1895
+ success: true,
1896
+ message: "No active subscriptions found",
1897
+ data: { products: [], totalMrr: 0 },
1898
+ };
1899
+ }
1900
+ // Step 2: Get rate plans for subscriptions to resolve product names
1901
+ const subIds = collectIds(subscriptions, "Id");
1902
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, ProductRatePlanId, Name", "RatePlan", "SubscriptionId", subIds);
1903
+ // Step 3: Get ProductRatePlan → Product mapping
1904
+ const prpIds = collectIds(ratePlans, "ProductRatePlanId");
1905
+ const productRatePlans = await queryWithBatchedIds(this.client, "Id, ProductId, Name", "ProductRatePlan", "Id", prpIds);
1906
+ const productIds = collectIds(productRatePlans, "ProductId");
1907
+ const products = await queryWithBatchedIds(this.client, "Id, Name", "Product", "Id", productIds);
1908
+ // Build lookup maps
1909
+ const productNameMap = new Map(products.map((p) => [getString(p, "Id"), getString(p, "Name") ?? "Unknown"]));
1910
+ const prpToProduct = new Map(productRatePlans.map((prp) => [
1911
+ getString(prp, "Id"),
1912
+ productNameMap.get(getString(prp, "ProductId") ?? "") ?? getString(prp, "Name") ?? "Unknown",
1913
+ ]));
1914
+ // Map subscriptions to their rate plans and product names
1915
+ const subMap = new Map(subscriptions.map((s) => [getString(s, "Id"), s]));
1916
+ // Get account details for all subscriptions
1917
+ const accountIds = collectIds(subscriptions, "AccountId");
1918
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1919
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1920
+ // Group by product name
1921
+ const productData = new Map();
1922
+ for (const rp of ratePlans) {
1923
+ const prpId = getString(rp, "ProductRatePlanId") ?? "";
1924
+ const productName = prpToProduct.get(prpId) ?? getString(rp, "Name") ?? "Unknown";
1925
+ const subId = getString(rp, "SubscriptionId") ?? "";
1926
+ const sub = subMap.get(subId);
1927
+ if (!sub)
1928
+ continue;
1929
+ const acctId = getString(sub, "AccountId") ?? "";
1930
+ const acct = accountMap.get(acctId);
1931
+ const mrr = getNumber(sub, "ContractedMrr") ?? 0;
1932
+ const existing = productData.get(productName) ?? {
1933
+ subscriptionCount: 0,
1934
+ totalMrr: 0,
1935
+ totalTcv: 0,
1936
+ subscriptions: [],
1937
+ };
1938
+ existing.subscriptionCount++;
1939
+ existing.totalMrr += mrr;
1940
+ existing.totalTcv += getNumber(sub, "TotalContractedValue") ?? 0;
1941
+ existing.subscriptions.push({
1942
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
1943
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
1944
+ accountName: getString(acct ?? {}, "Name") ?? "",
1945
+ mrr,
1946
+ status: getString(sub, "Status") ?? "",
1947
+ });
1948
+ productData.set(productName, existing);
1949
+ }
1950
+ // Convert to sorted array
1951
+ const results = [...productData.entries()]
1952
+ .map(([name, data]) => ({
1953
+ productName: name,
1954
+ subscriptionCount: data.subscriptionCount,
1955
+ totalMrr: data.totalMrr,
1956
+ totalTcv: data.totalTcv,
1957
+ subscriptions: data.subscriptions,
1958
+ }))
1959
+ .sort((a, b) => b.totalMrr - a.totalMrr)
1960
+ .slice(0, limit);
1961
+ const totalMrr = results.reduce((sum, r) => sum + r.totalMrr, 0);
1962
+ return {
1963
+ success: true,
1964
+ message: `Revenue breakdown across ${results.length} product(s), total MRR: $${totalMrr.toFixed(2)}`,
1965
+ data: { products: results, totalMrr },
1966
+ };
1967
+ }
1968
+ catch (error) {
1969
+ return {
1970
+ success: false,
1971
+ message: `Failed to get revenue by product: ${error instanceof Error ? error.message : String(error)}`,
1972
+ };
1973
+ }
1974
+ }
1975
+ async getPaymentReconciliation(input) {
1976
+ try {
1977
+ const { startDate, endDate, limit } = schemas.getPaymentReconciliation.parse(input);
1978
+ // Query payments in the date range
1979
+ const payments = await queryAll(this.client, `SELECT Id, PaymentNumber, AccountId, Amount, EffectiveDate, Status, PaymentMethodType ` +
1980
+ `FROM Payment ` +
1981
+ `WHERE EffectiveDate >= '${escapeZoql(startDate)}' AND EffectiveDate <= '${escapeZoql(endDate)}'`, limit);
1982
+ if (payments.length === 0) {
1983
+ return {
1984
+ success: true,
1985
+ message: `No payments found between ${startDate} and ${endDate}`,
1986
+ data: {
1987
+ period: { startDate, endDate },
1988
+ summary: { totalPayments: 0, totalAmount: 0, byStatus: {} },
1989
+ payments: [],
1990
+ },
1991
+ };
1992
+ }
1993
+ // Get account details
1994
+ const accountIds = collectIds(payments, "AccountId");
1995
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
1996
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
1997
+ // Build summary
1998
+ const byStatus = {};
1999
+ let totalAmount = 0;
2000
+ const paymentResults = payments.map((p) => {
2001
+ const amount = getNumber(p, "Amount") ?? 0;
2002
+ const status = getString(p, "Status") ?? "Unknown";
2003
+ const acctId = getString(p, "AccountId") ?? "";
2004
+ const acct = accountMap.get(acctId);
2005
+ totalAmount += amount;
2006
+ const statusBucket = byStatus[status] ?? { count: 0, amount: 0 };
2007
+ statusBucket.count++;
2008
+ statusBucket.amount += amount;
2009
+ byStatus[status] = statusBucket;
2010
+ return {
2011
+ paymentNumber: getString(p, "PaymentNumber") ?? "",
2012
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2013
+ accountName: getString(acct ?? {}, "Name") ?? "",
2014
+ amount,
2015
+ effectiveDate: getString(p, "EffectiveDate") ?? "",
2016
+ status,
2017
+ paymentMethodType: getString(p, "PaymentMethodType") ?? "",
2018
+ };
2019
+ });
2020
+ const result = {
2021
+ period: { startDate, endDate },
2022
+ summary: {
2023
+ totalPayments: payments.length,
2024
+ totalAmount,
2025
+ byStatus,
2026
+ },
2027
+ payments: paymentResults,
2028
+ };
2029
+ return {
2030
+ success: true,
2031
+ message: `${payments.length} payment(s) totaling $${totalAmount.toFixed(2)} between ${startDate} and ${endDate}`,
2032
+ data: result,
2033
+ };
2034
+ }
2035
+ catch (error) {
2036
+ return {
2037
+ success: false,
2038
+ message: `Failed to get payment reconciliation: ${error instanceof Error ? error.message : String(error)}`,
2039
+ };
2040
+ }
2041
+ }
2042
+ async getRecentlyCancelledSubscriptions(input) {
2043
+ try {
2044
+ const { daysBack, limit } = schemas.getRecentlyCancelledSubscriptions.parse(input);
2045
+ const sinceDate = subtractDays(new Date(), daysBack);
2046
+ const subscriptions = await queryAll(this.client, `SELECT Id, SubscriptionNumber, AccountId, Status, TermEndDate, CancelledDate, ContractedMrr ` +
2047
+ `FROM Subscription ` +
2048
+ `WHERE Status = 'Cancelled' AND CancelledDate >= '${sinceDate}'`, limit * 2);
2049
+ if (subscriptions.length === 0) {
2050
+ return {
2051
+ success: true,
2052
+ message: `No subscriptions cancelled in the last ${daysBack} days`,
2053
+ data: { subscriptions: [], totalFound: 0, totalLostMrr: 0 },
2054
+ };
2055
+ }
2056
+ // Get account details
2057
+ const accountIds = collectIds(subscriptions, "AccountId");
2058
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2059
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2060
+ // Get rate plan names
2061
+ const subIds = collectIds(subscriptions, "Id");
2062
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "SubscriptionId", subIds);
2063
+ const rpNamesBySub = new Map();
2064
+ for (const rp of ratePlans) {
2065
+ const subId = getString(rp, "SubscriptionId") ?? "";
2066
+ const existing = rpNamesBySub.get(subId) ?? [];
2067
+ const name = getString(rp, "Name");
2068
+ if (name)
2069
+ existing.push(name);
2070
+ rpNamesBySub.set(subId, existing);
2071
+ }
2072
+ const results = subscriptions.map((sub) => {
2073
+ const acctId = getString(sub, "AccountId") ?? "";
2074
+ const acct = accountMap.get(acctId);
2075
+ const subId = getString(sub, "Id") ?? "";
2076
+ return {
2077
+ subscriptionId: subId,
2078
+ subscriptionNumber: getString(sub, "SubscriptionNumber") ?? "",
2079
+ accountId: acctId,
2080
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2081
+ accountName: getString(acct ?? {}, "Name") ?? "",
2082
+ status: getString(sub, "Status") ?? "",
2083
+ cancelledDate: getString(sub, "CancelledDate") ?? "",
2084
+ termEndDate: getString(sub, "TermEndDate") ?? "",
2085
+ contractedMrr: getNumber(sub, "ContractedMrr") ?? 0,
2086
+ ratePlanNames: rpNamesBySub.get(subId) ?? [],
2087
+ };
2088
+ });
2089
+ const totalLostMrr = results.reduce((sum, r) => sum + r.contractedMrr, 0);
2090
+ return {
2091
+ success: true,
2092
+ message: `Found ${results.length} cancelled subscription(s) in the last ${daysBack} days (lost MRR: $${totalLostMrr.toFixed(2)})`,
2093
+ data: {
2094
+ subscriptions: results.slice(0, limit),
2095
+ totalFound: results.length,
2096
+ totalLostMrr,
2097
+ },
2098
+ };
2099
+ }
2100
+ catch (error) {
2101
+ return {
2102
+ success: false,
2103
+ message: `Failed to get cancelled subscriptions: ${error instanceof Error ? error.message : String(error)}`,
2104
+ };
2105
+ }
2106
+ }
2107
+ async getInvoiceAgingReport(input) {
2108
+ try {
2109
+ const { limit } = schemas.getInvoiceAgingReport.parse(input);
2110
+ const today = todayString();
2111
+ // Query all posted invoices with outstanding balance
2112
+ const invoices = await queryAll(this.client, `SELECT Id, InvoiceNumber, AccountId, DueDate, Amount, Balance ` +
2113
+ `FROM Invoice ` +
2114
+ `WHERE Status = 'Posted' AND Balance > 0`, limit);
2115
+ if (invoices.length === 0) {
2116
+ return {
2117
+ success: true,
2118
+ message: "No outstanding invoices found",
2119
+ data: { buckets: [], totalOutstanding: 0 },
2120
+ };
2121
+ }
2122
+ // Get account details
2123
+ const accountIds = collectIds(invoices, "AccountId");
2124
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2125
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2126
+ // Define aging buckets
2127
+ const bucketDefs = [
2128
+ { label: "Current", minDays: -999999, maxDays: 0 },
2129
+ { label: "1-30 Days", minDays: 1, maxDays: 30 },
2130
+ { label: "31-60 Days", minDays: 31, maxDays: 60 },
2131
+ { label: "61-90 Days", minDays: 61, maxDays: 90 },
2132
+ { label: "90+ Days", minDays: 91, maxDays: null },
2133
+ ];
2134
+ const buckets = bucketDefs.map((def) => ({
2135
+ ...def,
2136
+ invoiceCount: 0,
2137
+ totalBalance: 0,
2138
+ invoices: [],
2139
+ }));
2140
+ let totalOutstanding = 0;
2141
+ for (const inv of invoices) {
2142
+ const dueDate = getString(inv, "DueDate") ?? today;
2143
+ const balance = getNumber(inv, "Balance") ?? 0;
2144
+ const daysPast = daysBetween(dueDate, today);
2145
+ const acctId = getString(inv, "AccountId") ?? "";
2146
+ const acct = accountMap.get(acctId);
2147
+ totalOutstanding += balance;
2148
+ const invoiceEntry = {
2149
+ invoiceId: getString(inv, "Id") ?? "",
2150
+ invoiceNumber: getString(inv, "InvoiceNumber") ?? "",
2151
+ accountName: getString(acct ?? {}, "Name") ?? "",
2152
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2153
+ balance,
2154
+ dueDate,
2155
+ daysPastDue: Math.max(0, daysPast),
2156
+ };
2157
+ for (const bucket of buckets) {
2158
+ const inMin = daysPast >= bucket.minDays;
2159
+ const inMax = bucket.maxDays === null || daysPast <= bucket.maxDays;
2160
+ if (inMin && inMax) {
2161
+ bucket.invoiceCount++;
2162
+ bucket.totalBalance += balance;
2163
+ bucket.invoices.push(invoiceEntry);
2164
+ break;
2165
+ }
2166
+ }
2167
+ }
2168
+ // Sort invoices within each bucket by balance descending
2169
+ for (const bucket of buckets) {
2170
+ bucket.invoices.sort((a, b) => b.balance - a.balance);
2171
+ }
2172
+ return {
2173
+ success: true,
2174
+ message: `AR aging report: ${invoices.length} outstanding invoice(s), total $${totalOutstanding.toFixed(2)}`,
2175
+ data: { buckets, totalOutstanding },
2176
+ };
2177
+ }
2178
+ catch (error) {
2179
+ return {
2180
+ success: false,
2181
+ message: `Failed to get invoice aging report: ${error instanceof Error ? error.message : String(error)}`,
2182
+ };
2183
+ }
2184
+ }
2185
+ async getAccountHealthScorecard(input) {
2186
+ try {
2187
+ const { limit } = schemas.getAccountHealthScorecard.parse(input);
2188
+ const today = todayString();
2189
+ const thirtyDaysAhead = addDays(new Date(), 30);
2190
+ const thirtyDaysBack = subtractDays(new Date(), 30);
2191
+ // Signal 1: Overdue invoices
2192
+ const overdueInvoices = await queryAll(this.client, `SELECT Id, AccountId, Balance, DueDate ` +
2193
+ `FROM Invoice ` +
2194
+ `WHERE Status = 'Posted' AND Balance > 0 AND DueDate < '${today}'`, 2000);
2195
+ // Signal 2: Expiring subscriptions (next 30 days, not auto-renewing)
2196
+ const expiringSubscriptions = await queryAll(this.client, `SELECT Id, AccountId, ContractedMrr, TermEndDate ` +
2197
+ `FROM Subscription ` +
2198
+ `WHERE Status = 'Active' AND TermEndDate >= '${today}' AND TermEndDate <= '${thirtyDaysAhead}' AND AutoRenew = 'false'`, 2000);
2199
+ // Signal 3: Recent payment failures
2200
+ const failedPayments = await queryAll(this.client, `SELECT Id, AccountId ` +
2201
+ `FROM Payment ` +
2202
+ `WHERE Status = 'Error' AND EffectiveDate >= '${thirtyDaysBack}'`, 2000);
2203
+ // Aggregate by account
2204
+ const accountScores = new Map();
2205
+ const ensureAccount = (acctId) => {
2206
+ if (!accountScores.has(acctId)) {
2207
+ accountScores.set(acctId, {
2208
+ overdueCount: 0,
2209
+ overdueBalance: 0,
2210
+ expiringCount: 0,
2211
+ expiringMrr: 0,
2212
+ paymentFailures: 0,
2213
+ });
2214
+ }
2215
+ return accountScores.get(acctId);
2216
+ };
2217
+ for (const inv of overdueInvoices) {
2218
+ const acctId = getString(inv, "AccountId");
2219
+ if (!acctId)
2220
+ continue;
2221
+ const score = ensureAccount(acctId);
2222
+ score.overdueCount++;
2223
+ score.overdueBalance += getNumber(inv, "Balance") ?? 0;
2224
+ }
2225
+ for (const sub of expiringSubscriptions) {
2226
+ const acctId = getString(sub, "AccountId");
2227
+ if (!acctId)
2228
+ continue;
2229
+ const score = ensureAccount(acctId);
2230
+ score.expiringCount++;
2231
+ score.expiringMrr += getNumber(sub, "ContractedMrr") ?? 0;
2232
+ }
2233
+ for (const pmt of failedPayments) {
2234
+ const acctId = getString(pmt, "AccountId");
2235
+ if (!acctId)
2236
+ continue;
2237
+ const score = ensureAccount(acctId);
2238
+ score.paymentFailures++;
2239
+ }
2240
+ // Calculate health scores (100 = healthy, 0 = critical)
2241
+ const accountIds = [...accountScores.keys()];
2242
+ if (accountIds.length === 0) {
2243
+ return {
2244
+ success: true,
2245
+ message: "No at-risk accounts found",
2246
+ data: { accounts: [], totalAtRisk: 0 },
2247
+ };
2248
+ }
2249
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2250
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2251
+ const results = [];
2252
+ for (const [acctId, scores] of accountScores) {
2253
+ const acct = accountMap.get(acctId);
2254
+ const riskFactors = [];
2255
+ let healthScore = 100;
2256
+ if (scores.overdueCount > 0) {
2257
+ healthScore -= Math.min(40, scores.overdueCount * 15);
2258
+ riskFactors.push(`${scores.overdueCount} overdue invoice(s), $${scores.overdueBalance.toFixed(2)} outstanding`);
2259
+ }
2260
+ if (scores.expiringCount > 0) {
2261
+ healthScore -= Math.min(30, scores.expiringCount * 10);
2262
+ riskFactors.push(`${scores.expiringCount} subscription(s) expiring soon, $${scores.expiringMrr.toFixed(2)} MRR at risk`);
2263
+ }
2264
+ if (scores.paymentFailures > 0) {
2265
+ healthScore -= Math.min(30, scores.paymentFailures * 15);
2266
+ riskFactors.push(`${scores.paymentFailures} recent payment failure(s)`);
2267
+ }
2268
+ results.push({
2269
+ accountId: acctId,
2270
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2271
+ accountName: getString(acct ?? {}, "Name") ?? "",
2272
+ healthScore: Math.max(0, healthScore),
2273
+ riskFactors,
2274
+ overdueInvoices: {
2275
+ count: scores.overdueCount,
2276
+ totalBalance: scores.overdueBalance,
2277
+ },
2278
+ expiringSubscriptions: {
2279
+ count: scores.expiringCount,
2280
+ totalMrr: scores.expiringMrr,
2281
+ },
2282
+ recentPaymentFailures: scores.paymentFailures,
2283
+ });
2284
+ }
2285
+ // Sort by health score ascending (worst first)
2286
+ results.sort((a, b) => a.healthScore - b.healthScore);
2287
+ return {
2288
+ success: true,
2289
+ message: `Found ${results.length} account(s) with risk signals`,
2290
+ data: {
2291
+ accounts: results.slice(0, limit),
2292
+ totalAtRisk: results.length,
2293
+ },
2294
+ };
2295
+ }
2296
+ catch (error) {
2297
+ return {
2298
+ success: false,
2299
+ message: `Failed to get account health scorecard: ${error instanceof Error ? error.message : String(error)}`,
2300
+ };
2301
+ }
2302
+ }
2303
+ async findInvoicesByProduct(input) {
2304
+ try {
2305
+ const { productName, limit } = schemas.findInvoicesByProduct.parse(input);
2306
+ const safeName = escapeZoql(productName);
2307
+ // Step 1: Find ProductRatePlan IDs matching the product name
2308
+ let productRatePlans = await queryAll(this.client, `SELECT Id, Name FROM ProductRatePlan WHERE Name LIKE '%${safeName}%'`);
2309
+ if (productRatePlans.length === 0) {
2310
+ // Try Product name
2311
+ const products = await queryAll(this.client, `SELECT Id, Name FROM Product WHERE Name LIKE '%${safeName}%'`);
2312
+ if (products.length === 0) {
2313
+ return {
2314
+ success: true,
2315
+ message: `No products found matching "${productName}"`,
2316
+ data: { invoiceItems: [], totalFound: 0 },
2317
+ };
2318
+ }
2319
+ const productIds = collectIds(products, "Id");
2320
+ productRatePlans = await queryWithBatchedIds(this.client, "Id, Name", "ProductRatePlan", "ProductId", productIds);
2321
+ if (productRatePlans.length === 0) {
2322
+ return {
2323
+ success: true,
2324
+ message: `No rate plans found for product "${productName}"`,
2325
+ data: { invoiceItems: [], totalFound: 0 },
2326
+ };
2327
+ }
2328
+ }
2329
+ const prpIds = collectIds(productRatePlans, "Id");
2330
+ // Step 2: Find RatePlans → RatePlanCharges
2331
+ const ratePlans = await queryWithBatchedIds(this.client, "Id, SubscriptionId, Name", "RatePlan", "ProductRatePlanId", prpIds);
2332
+ if (ratePlans.length === 0) {
2333
+ return {
2334
+ success: true,
2335
+ message: `No subscriptions found with product "${productName}"`,
2336
+ data: { invoiceItems: [], totalFound: 0 },
2337
+ };
2338
+ }
2339
+ const rpIds = collectIds(ratePlans, "Id");
2340
+ const ratePlanCharges = await queryWithBatchedIds(this.client, "Id, RatePlanId, Name", "RatePlanCharge", "RatePlanId", rpIds);
2341
+ if (ratePlanCharges.length === 0) {
2342
+ return {
2343
+ success: true,
2344
+ message: `No charges found for product "${productName}"`,
2345
+ data: { invoiceItems: [], totalFound: 0 },
2346
+ };
2347
+ }
2348
+ const rpcIds = collectIds(ratePlanCharges, "Id");
2349
+ // Step 3: Find InvoiceItems by RatePlanChargeId
2350
+ const invoiceItems = await queryWithBatchedIds(this.client, "Id, InvoiceId, ChargeAmount, ChargeName, ServiceStartDate, ServiceEndDate, SubscriptionId, RatePlanChargeId", "InvoiceItem", "RatePlanChargeId", rpcIds, undefined, limit);
2351
+ if (invoiceItems.length === 0) {
2352
+ return {
2353
+ success: true,
2354
+ message: `No invoice items found for product "${productName}"`,
2355
+ data: { invoiceItems: [], totalFound: 0 },
2356
+ };
2357
+ }
2358
+ // Step 4: Get Invoice details
2359
+ const invoiceIds = collectIds(invoiceItems, "InvoiceId");
2360
+ const invoices = await queryWithBatchedIds(this.client, "Id, InvoiceNumber, InvoiceDate, AccountId", "Invoice", "Id", invoiceIds);
2361
+ const invoiceMap = new Map(invoices.map((i) => [getString(i, "Id"), i]));
2362
+ // Step 5: Get Account details
2363
+ const accountIds = collectIds(invoices, "AccountId");
2364
+ const accounts = await queryWithBatchedIds(this.client, "Id, AccountNumber, Name", "Account", "Id", accountIds);
2365
+ const accountMap = new Map(accounts.map((a) => [getString(a, "Id"), a]));
2366
+ // Build rate plan name lookup
2367
+ const rpNameMap = new Map(ratePlans.map((rp) => [getString(rp, "Id"), getString(rp, "Name") ?? ""]));
2368
+ const rpcToRp = new Map(ratePlanCharges.map((rpc) => [getString(rpc, "Id"), getString(rpc, "RatePlanId") ?? ""]));
2369
+ // Get subscription number lookup
2370
+ const subIds = collectIds(invoiceItems, "SubscriptionId");
2371
+ const subs = await queryWithBatchedIds(this.client, "Id, SubscriptionNumber", "Subscription", "Id", subIds);
2372
+ const subNumberMap = new Map(subs.map((s) => [getString(s, "Id"), getString(s, "SubscriptionNumber") ?? ""]));
2373
+ // Assemble results
2374
+ const results = invoiceItems.map((item) => {
2375
+ const invId = getString(item, "InvoiceId") ?? "";
2376
+ const inv = invoiceMap.get(invId);
2377
+ const acctId = getString(inv ?? {}, "AccountId") ?? "";
2378
+ const acct = accountMap.get(acctId);
2379
+ const subId = getString(item, "SubscriptionId") ?? "";
2380
+ return {
2381
+ invoiceId: invId,
2382
+ invoiceNumber: getString(inv ?? {}, "InvoiceNumber") ?? "",
2383
+ invoiceDate: getString(inv ?? {}, "InvoiceDate") ?? "",
2384
+ accountNumber: getString(acct ?? {}, "AccountNumber") ?? "",
2385
+ accountName: getString(acct ?? {}, "Name") ?? "",
2386
+ chargeAmount: getNumber(item, "ChargeAmount") ?? 0,
2387
+ chargeName: getString(item, "ChargeName") ?? "",
2388
+ ratePlanName: rpNameMap.get(rpcToRp.get(getString(item, "RatePlanChargeId") ?? "") ?? "") ?? "",
2389
+ subscriptionNumber: subNumberMap.get(subId) ?? "",
2390
+ };
2391
+ });
2392
+ const totalChargeAmount = results.reduce((sum, r) => sum + r.chargeAmount, 0);
2393
+ return {
2394
+ success: true,
2395
+ message: `Found ${results.length} invoice item(s) for "${productName}", total charges: $${totalChargeAmount.toFixed(2)}`,
2396
+ data: {
2397
+ invoiceItems: results.slice(0, limit),
2398
+ totalFound: results.length,
2399
+ totalChargeAmount,
2400
+ },
2401
+ };
2402
+ }
2403
+ catch (error) {
2404
+ return {
2405
+ success: false,
2406
+ message: `Failed to find invoices by product: ${error instanceof Error ? error.message : String(error)}`,
2407
+ };
2408
+ }
2409
+ }
2410
+ }
2411
+ export const toolRegistrations = [
2412
+ // Account Tools
2413
+ {
2414
+ name: "get_account",
2415
+ description: "Get Zuora account details by account key (ID or account number). " +
2416
+ "Returns name, status, currency, balance, and billing info.",
2417
+ inputSchema: schemas.getAccount,
2418
+ invoke: (handlers, args) => handlers.getAccount(args),
2419
+ },
2420
+ {
2421
+ name: "get_account_summary",
2422
+ description: "Get a comprehensive summary of a Zuora account including balances, " +
2423
+ "active subscriptions, recent invoices, and payments in a single call.",
2424
+ inputSchema: schemas.getAccountSummary,
2425
+ invoke: (handlers, args) => handlers.getAccountSummary(args),
2426
+ },
2427
+ // Invoice Tools
2428
+ {
2429
+ name: "get_invoice",
2430
+ description: "Get full details of a Zuora invoice including line items, amount, " +
2431
+ "due date, status, and payment status.",
2432
+ inputSchema: schemas.getInvoice,
2433
+ invoke: (handlers, args) => handlers.getInvoice(args),
2434
+ },
2435
+ {
2436
+ name: "list_invoices",
2437
+ description: "List invoices for a specific Zuora account with pagination. " +
2438
+ "Returns invoice numbers, amounts, dates, and statuses.",
2439
+ inputSchema: schemas.listInvoices,
2440
+ invoke: (handlers, args) => handlers.listInvoices(args),
2441
+ },
2442
+ // Subscription Tools
2443
+ {
2444
+ name: "get_subscription",
2445
+ description: "Get details of a Zuora subscription by subscription key or ID. " +
2446
+ "Returns plan info, status, term dates, and rate plan charges.",
2447
+ inputSchema: schemas.getSubscription,
2448
+ invoke: (handlers, args) => handlers.getSubscription(args),
2449
+ },
2450
+ {
2451
+ name: "list_subscriptions",
2452
+ description: "List all subscriptions for a Zuora account. " +
2453
+ "Returns subscription names, statuses, and term dates.",
2454
+ inputSchema: schemas.listSubscriptions,
2455
+ invoke: (handlers, args) => handlers.listSubscriptions(args),
2456
+ },
2457
+ // Payment Tools
2458
+ {
2459
+ name: "get_payment",
2460
+ description: "Get details of a specific Zuora payment including amount, date, " +
2461
+ "status, and which invoices it was applied to.",
2462
+ inputSchema: schemas.getPayment,
2463
+ invoke: (handlers, args) => handlers.getPayment(args),
2464
+ },
2465
+ {
2466
+ name: "list_payments",
2467
+ description: "List payments with optional account filter and pagination. " +
2468
+ "Returns payment amounts, dates, methods, and statuses.",
2469
+ inputSchema: schemas.listPayments,
2470
+ invoke: (handlers, args) => handlers.listPayments(args),
2471
+ },
2472
+ // ZOQL Query Tools
2473
+ {
2474
+ name: "execute_zoql_query",
2475
+ description: "Execute a ZOQL (Zuora Object Query Language) query for ad-hoc data retrieval. " +
2476
+ "Syntax: SELECT field1, field2 FROM ObjectName WHERE condition. " +
2477
+ "Key objects: Account, Invoice, Payment, Subscription, RatePlan, " +
2478
+ "RatePlanCharge, Product, ProductRatePlan, Contact. " +
2479
+ "Limitations: No JOINs supported. Max 2000 records per call. " +
2480
+ "Use continue_zoql_query with queryLocator for pagination. " +
2481
+ "Example: SELECT Id, AccountNumber, Name, Balance FROM Account WHERE Status = 'Active'",
2482
+ inputSchema: schemas.executeZoqlQuery,
2483
+ invoke: (handlers, args) => handlers.executeZoqlQuery(args),
2484
+ },
2485
+ {
2486
+ name: "continue_zoql_query",
2487
+ description: "Continue a previous ZOQL query that returned a queryLocator " +
2488
+ "(indicates more records available). Returns the next batch of up to 2000 records.",
2489
+ inputSchema: schemas.continueZoqlQuery,
2490
+ invoke: (handlers, args) => handlers.continueZoqlQuery(args),
2491
+ },
2492
+ // Product Catalog
2493
+ {
2494
+ name: "list_products",
2495
+ description: "List products from the Zuora product catalog with rate plans and charges. " +
2496
+ "Useful for understanding available products and pricing.",
2497
+ inputSchema: schemas.listProducts,
2498
+ invoke: (handlers, args) => handlers.listProducts(args),
2499
+ },
2500
+ // Phase 2: Extended Read Tools
2501
+ // Invoice Files
2502
+ {
2503
+ name: "get_invoice_files",
2504
+ description: "Get PDF file references for a specific Zuora invoice. " +
2505
+ "Returns file IDs and authenticated URLs for invoice PDFs. " +
2506
+ "Note: URLs require a valid Bearer token to access.",
2507
+ inputSchema: schemas.getInvoiceFiles,
2508
+ invoke: (handlers, args) => handlers.getInvoiceFiles(args),
2509
+ },
2510
+ // Credit Memo Tools
2511
+ {
2512
+ name: "get_credit_memo",
2513
+ description: "Get full details of a Zuora credit memo including amount, status, " +
2514
+ "applied/unapplied amounts, and line items.",
2515
+ inputSchema: schemas.getCreditMemo,
2516
+ invoke: (handlers, args) => handlers.getCreditMemo(args),
2517
+ },
2518
+ {
2519
+ name: "list_credit_memos",
2520
+ description: "List credit memos with optional account filter and pagination. " +
2521
+ "Returns credit memo numbers, amounts, statuses, and dates.",
2522
+ inputSchema: schemas.listCreditMemos,
2523
+ invoke: (handlers, args) => handlers.listCreditMemos(args),
2524
+ },
2525
+ // Account Search
2526
+ {
2527
+ name: "search_accounts",
2528
+ description: "Search Zuora accounts by field value. Supports searching by Name, " +
2529
+ "AccountNumber, Status, Currency, or Balance. Use LIKE operator with % " +
2530
+ "wildcard for partial name matching (e.g., field='Name', operator='LIKE', " +
2531
+ "value='Acme%'). Returns Id, AccountNumber, Name, Status, Balance, Currency. " +
2532
+ "For additional fields, use execute_zoql_query directly.",
2533
+ inputSchema: schemas.searchAccounts,
2534
+ invoke: (handlers, args) => handlers.searchAccounts(args),
2535
+ },
2536
+ // Usage
2537
+ {
2538
+ name: "list_usage",
2539
+ description: "List usage records for a Zuora account with pagination. " +
2540
+ "Returns usage quantities, units of measure, dates, and status.",
2541
+ inputSchema: schemas.listUsage,
2542
+ invoke: (handlers, args) => handlers.listUsage(args),
2543
+ },
2544
+ // User Management
2545
+ {
2546
+ name: "list_users",
2547
+ description: "List Zuora platform users with optional SCIM filter (e.g., status eq 'Active'). " +
2548
+ "Uses REST API, not ZOQL — the User object is not ZOQL-queryable. " +
2549
+ "Supports pagination via startIndex/count. " +
2550
+ "Returns user names, emails, statuses, roles, and last login times.",
2551
+ inputSchema: schemas.listUsers,
2552
+ invoke: (handlers, args) => handlers.listUsers(args),
2553
+ },
2554
+ {
2555
+ name: "get_user",
2556
+ description: "Get details of a specific Zuora platform user by their user ID (UUID). " +
2557
+ "Returns user name, email, status, role, profile, and last login time.",
2558
+ inputSchema: schemas.getUser,
2559
+ invoke: (handlers, args) => handlers.getUser(args),
2560
+ },
2561
+ // Bill Run Tools
2562
+ {
2563
+ name: "get_bill_run",
2564
+ description: "Get details and status of a Zuora bill run by bill run ID. " +
2565
+ "Returns bill run number, status (Pending/Processing/Completed/Error/Canceled/Posted), " +
2566
+ "target date, invoice date, and auto-post/email settings.",
2567
+ inputSchema: schemas.getBillRun,
2568
+ invoke: (handlers, args) => handlers.getBillRun(args),
2569
+ },
2570
+ {
2571
+ name: "list_bill_runs",
2572
+ description: "List bill runs in Zuora with pagination. " +
2573
+ "Returns bill run numbers, statuses, target dates, and settings.",
2574
+ inputSchema: schemas.listBillRuns,
2575
+ invoke: (handlers, args) => handlers.listBillRuns(args),
2576
+ },
2577
+ // Contact Tools
2578
+ {
2579
+ name: "get_contact",
2580
+ description: "Get full details of a Zuora contact by contact ID. " +
2581
+ "Returns name, emails, phone numbers, address, tax region, and timestamps. " +
2582
+ "Use ZOQL to find contact IDs: SELECT Id, FirstName, LastName FROM Contact WHERE AccountId = '...'",
2583
+ inputSchema: schemas.getContact,
2584
+ invoke: (handlers, args) => handlers.getContact(args),
2585
+ },
2586
+ // Describe API
2587
+ {
2588
+ name: "describe_object",
2589
+ description: "Get field metadata for any Zuora object type. Returns all fields with names, types, " +
2590
+ "and properties (selectable, createable, updateable, filterable, required). " +
2591
+ "Essential for building correct ZOQL queries — use this to discover available fields " +
2592
+ "before writing SELECT statements. Object type must be PascalCase (e.g., Account, " +
2593
+ "Invoice, Subscription, Payment, RatePlan, RatePlanCharge, BillRun, Contact).",
2594
+ inputSchema: schemas.describeObject,
2595
+ invoke: (handlers, args) => handlers.describeObject(args),
2596
+ },
2597
+ // Phase 3: Write Operations (require confirmation before calling)
2598
+ // Payment Creation (HIGH risk)
2599
+ {
2600
+ name: "create_payment",
2601
+ description: "FINANCIAL WRITE OPERATION (HIGH RISK): Create a payment record in Zuora. " +
2602
+ "This charges the customer or records an external payment. " +
2603
+ "IMPORTANT: Confirm the account ID, amount, and effective date with the user before calling. " +
2604
+ "Requires a UUID idempotency key to prevent duplicate payments. " +
2605
+ "Electronic payments require a paymentMethodId and process through the payment gateway. " +
2606
+ "External payments are manual records (check, wire, etc.). " +
2607
+ "This action cannot be easily undone - refunds require a separate operation.",
2608
+ inputSchema: schemas.createPayment,
2609
+ invoke: (handlers, args) => handlers.createPayment(args),
2610
+ },
2611
+ // Payment Application (HIGH risk)
2612
+ {
2613
+ name: "apply_payment",
2614
+ description: "FINANCIAL WRITE OPERATION (HIGH RISK): Apply an unapplied payment to one or more invoices. " +
2615
+ "This reduces invoice balances and allocates payment funds. " +
2616
+ "IMPORTANT: Confirm the payment ID, invoice IDs, and amounts with the user before calling. " +
2617
+ "The total applied amount across all invoices must not exceed the payment's unapplied amount. " +
2618
+ "ALWAYS use get_payment first to verify the unapplied balance before applying.",
2619
+ inputSchema: schemas.applyPayment,
2620
+ invoke: (handlers, args) => handlers.applyPayment(args),
2621
+ },
2622
+ // Invoice Creation (MEDIUM risk)
2623
+ {
2624
+ name: "create_invoice",
2625
+ description: "FINANCIAL WRITE OPERATION (MEDIUM RISK): Create a draft invoice for a Zuora account. " +
2626
+ "The invoice is created in Draft status and must be posted separately using post_invoice. " +
2627
+ "Requires a UUID idempotency key to prevent duplicate invoices. " +
2628
+ "Confirm the account ID and dates with the user before calling.",
2629
+ inputSchema: schemas.createInvoice,
2630
+ invoke: (handlers, args) => handlers.createInvoice(args),
2631
+ },
2632
+ // Invoice Posting (MEDIUM risk)
2633
+ {
2634
+ name: "post_invoice",
2635
+ description: "FINANCIAL WRITE OPERATION (MEDIUM RISK): Post a draft invoice, changing its status to Posted. " +
2636
+ "IMPORTANT: Once posted, the invoice is IMMUTABLE - it cannot be edited or deleted. " +
2637
+ "Posted invoices affect the account balance and are visible to the customer. " +
2638
+ "ALWAYS use get_invoice to review the draft invoice before posting. " +
2639
+ "To reverse a posted invoice, you must create a credit memo.",
2640
+ inputSchema: schemas.postInvoice,
2641
+ invoke: (handlers, args) => handlers.postInvoice(args),
2642
+ },
2643
+ // Subscription Cancellation (HIGH risk)
2644
+ {
2645
+ name: "cancel_subscription",
2646
+ description: "DESTRUCTIVE FINANCIAL OPERATION (HIGH RISK): Cancel a Zuora subscription. " +
2647
+ "IMPORTANT: This action may be irreversible depending on the cancellation policy. " +
2648
+ "Confirm the subscription key and cancellation policy with the user before calling. " +
2649
+ "EndOfCurrentTerm: Cancels at the end of the current billing term. " +
2650
+ "EndOfLastInvoicePeriod: Cancels at the end of the last invoiced period. " +
2651
+ "SpecificDate: Cancels on a specific date (requires cancellationEffectiveDate). " +
2652
+ "ALWAYS use get_subscription to review the subscription details before cancelling.",
2653
+ inputSchema: schemas.cancelSubscription,
2654
+ invoke: (handlers, args) => handlers.cancelSubscription(args),
2655
+ },
2656
+ // Phase 4: Advanced Operations
2657
+ // Subscription Creation (HIGH risk)
2658
+ {
2659
+ name: "create_subscription",
2660
+ description: "FINANCIAL WRITE OPERATION (HIGH RISK): Create a new subscription for a Zuora account. " +
2661
+ "IMPORTANT: Confirm the account, rate plans, term type, and effective date with the user before calling. " +
2662
+ "Requires a UUID idempotency key to prevent duplicate subscriptions. " +
2663
+ "Use list_products to find available productRatePlanIds before creating. " +
2664
+ "TERMED subscriptions require initialTerm and initialTermPeriodType. " +
2665
+ "EVERGREEN subscriptions have no fixed end date. " +
2666
+ "This creates a binding billing relationship that affects invoicing.",
2667
+ inputSchema: schemas.createSubscription,
2668
+ invoke: (handlers, args) => handlers.createSubscription(args),
2669
+ },
2670
+ // Subscription Update (MEDIUM risk)
2671
+ {
2672
+ name: "update_subscription",
2673
+ description: "FINANCIAL WRITE OPERATION (MEDIUM RISK): Update a Zuora subscription's renewal terms or settings. " +
2674
+ "Can modify autoRenew, renewalTerm, renewalTermPeriodType, and notes. " +
2675
+ "Confirm the subscription key and changes with the user before calling. " +
2676
+ "ALWAYS use get_subscription to review current settings before updating. " +
2677
+ "Changes to renewal terms take effect at the next renewal.",
2678
+ inputSchema: schemas.updateSubscription,
2679
+ invoke: (handlers, args) => handlers.updateSubscription(args),
2680
+ },
2681
+ // Account Creation (MEDIUM risk)
2682
+ {
2683
+ name: "create_account",
2684
+ description: "FINANCIAL WRITE OPERATION (MEDIUM RISK): Create a new billing account in Zuora. " +
2685
+ "Requires account name, currency, bill cycle day, and bill-to contact information. " +
2686
+ "Confirm all details with the user before calling. " +
2687
+ "Requires a UUID idempotency key to prevent duplicate accounts. " +
2688
+ "The account is created in Active status and can receive subscriptions immediately. " +
2689
+ "Currency cannot be changed after account creation.",
2690
+ inputSchema: schemas.createAccount,
2691
+ invoke: (handlers, args) => handlers.createAccount(args),
2692
+ },
2693
+ // Account Update (LOW risk)
2694
+ {
2695
+ name: "update_account",
2696
+ description: "WRITE OPERATION (LOW RISK): Update a Zuora account's name, notes, payment terms, " +
2697
+ "auto-pay setting, or contact information. " +
2698
+ "Confirm the account key and changes with the user before calling. " +
2699
+ "ALWAYS use get_account to review current settings before updating. " +
2700
+ "Only include fields that need to change.",
2701
+ inputSchema: schemas.updateAccount,
2702
+ invoke: (handlers, args) => handlers.updateAccount(args),
2703
+ },
2704
+ // Refund Creation (HIGH risk)
2705
+ {
2706
+ name: "create_refund",
2707
+ description: "FINANCIAL WRITE OPERATION (HIGH RISK): Create a refund against a specific payment in Zuora. " +
2708
+ "IMPORTANT: Confirm the payment ID, amount, and refund type with the user before calling. " +
2709
+ "ALWAYS use get_payment to verify the payment exists and has sufficient applied amount. " +
2710
+ "The refund amount must not exceed the original payment's applied amount. " +
2711
+ "Electronic refunds are processed through the payment gateway and may take days to settle. " +
2712
+ "External refunds are manual records (check, wire, etc.). " +
2713
+ "Requires a UUID idempotency key to prevent duplicate refunds. " +
2714
+ "This action cannot be easily undone.",
2715
+ inputSchema: schemas.createRefund,
2716
+ invoke: (handlers, args) => handlers.createRefund(args),
2717
+ },
2718
+ // Bill Run Creation (MEDIUM risk)
2719
+ {
2720
+ name: "create_bill_run",
2721
+ description: "FINANCIAL WRITE OPERATION (MEDIUM RISK): Create a bill run to generate invoices for a billing cycle. " +
2722
+ "Bill runs process all eligible charges through the target date and generate draft invoices. " +
2723
+ "Confirm the target date with the user before calling. " +
2724
+ "Set autoPost=true to automatically post generated invoices. " +
2725
+ "Set autoEmail=true (requires autoPost) to email invoices to customers. " +
2726
+ "Requires a UUID idempotency key to prevent duplicate bill runs. " +
2727
+ "The bill run starts in Pending status and progresses through Processing to Completed.",
2728
+ inputSchema: schemas.createBillRun,
2729
+ invoke: (handlers, args) => handlers.createBillRun(args),
2730
+ },
2731
+ // Contact Creation (LOW risk)
2732
+ {
2733
+ name: "create_contact",
2734
+ description: "WRITE OPERATION (LOW RISK): Create a new contact for a Zuora account. " +
2735
+ "Requires account ID, first name, and last name. " +
2736
+ "Contacts can be used as bill-to or sold-to addresses via update_account. " +
2737
+ "Requires a UUID idempotency key to prevent duplicate contacts.",
2738
+ inputSchema: schemas.createContact,
2739
+ invoke: (handlers, args) => handlers.createContact(args),
2740
+ },
2741
+ // Contact Update (LOW risk)
2742
+ {
2743
+ name: "update_contact",
2744
+ description: "WRITE OPERATION (LOW RISK): Update an existing Zuora contact's information. " +
2745
+ "Only include fields that need to change. " +
2746
+ "ALWAYS use get_contact to review current details before updating.",
2747
+ inputSchema: schemas.updateContact,
2748
+ invoke: (handlers, args) => handlers.updateContact(args),
2749
+ },
2750
+ // ==================== Composite Tools (Read-Only) ====================
2751
+ {
2752
+ name: "find_accounts_by_product",
2753
+ description: "Find accounts with active subscriptions for a specific product. " +
2754
+ "Answers questions like 'Which accounts have Security Analytics?' " +
2755
+ "Internally chains: ProductRatePlan → RatePlan → Subscription → Account. " +
2756
+ "Returns account details with subscription info and rate plan names.",
2757
+ inputSchema: schemas.findAccountsByProduct,
2758
+ invoke: (handlers, args) => handlers.findAccountsByProduct(args),
2759
+ },
2760
+ {
2761
+ name: "get_overdue_invoices",
2762
+ description: "List all overdue invoices across all accounts, sorted by days past due. " +
2763
+ "Answers 'Show overdue invoices' or 'What invoices are past due?' " +
2764
+ "Filters: Posted invoices with balance > 0 and due date in the past. " +
2765
+ "Returns invoice details with account info and days past due.",
2766
+ inputSchema: schemas.getOverdueInvoices,
2767
+ invoke: (handlers, args) => handlers.getOverdueInvoices(args),
2768
+ },
2769
+ {
2770
+ name: "get_expiring_subscriptions",
2771
+ description: "Find subscriptions expiring within a time window. " +
2772
+ "Answers 'What subscriptions expire this month?' or 'Renewal pipeline for Q1.' " +
2773
+ "Shows MRR at risk (non-auto-renewing subscriptions). " +
2774
+ "Returns subscription details with account info and days until expiry.",
2775
+ inputSchema: schemas.getExpiringSubscriptions,
2776
+ invoke: (handlers, args) => handlers.getExpiringSubscriptions(args),
2777
+ },
2778
+ {
2779
+ name: "get_account_billing_overview",
2780
+ description: "Comprehensive billing summary for a single account in one call. " +
2781
+ "Answers 'What's the billing status for account X?' " +
2782
+ "Combines: account details + recent invoices + payments + active subscriptions. " +
2783
+ "Shows outstanding balance, overdue amounts, MRR, and recent activity.",
2784
+ inputSchema: schemas.getAccountBillingOverview,
2785
+ invoke: (handlers, args) => handlers.getAccountBillingOverview(args),
2786
+ },
2787
+ {
2788
+ name: "get_revenue_by_product",
2789
+ description: "MRR breakdown by product across all active subscriptions. " +
2790
+ "Answers 'What's our revenue by product?' or 'MRR breakdown.' " +
2791
+ "Chains: Subscription → RatePlan → ProductRatePlan → Product. " +
2792
+ "Returns products sorted by MRR with subscription details.",
2793
+ inputSchema: schemas.getRevenueByProduct,
2794
+ invoke: (handlers, args) => handlers.getRevenueByProduct(args),
2795
+ },
2796
+ {
2797
+ name: "get_payment_reconciliation",
2798
+ description: "Payment reconciliation report for a date range. " +
2799
+ "Answers 'What payments were received this month?' " +
2800
+ "Returns payment list with account details, summary stats, and status breakdown.",
2801
+ inputSchema: schemas.getPaymentReconciliation,
2802
+ invoke: (handlers, args) => handlers.getPaymentReconciliation(args),
2803
+ },
2804
+ {
2805
+ name: "get_recently_cancelled_subscriptions",
2806
+ description: "Find subscriptions cancelled within a recent time window. " +
2807
+ "Answers 'What churned last month?' or 'Recent cancellations.' " +
2808
+ "Shows lost MRR and affected products. " +
2809
+ "Returns cancelled subscription details with account info.",
2810
+ inputSchema: schemas.getRecentlyCancelledSubscriptions,
2811
+ invoke: (handlers, args) => handlers.getRecentlyCancelledSubscriptions(args),
2812
+ },
2813
+ {
2814
+ name: "get_invoice_aging_report",
2815
+ description: "AR aging report: all outstanding invoices bucketed by days past due. " +
2816
+ "Answers 'Give me an AR aging report' or 'What does our accounts receivable look like?' " +
2817
+ "Buckets: Current, 1-30, 31-60, 61-90, 90+ days. " +
2818
+ "Returns aging buckets with invoice and account details.",
2819
+ inputSchema: schemas.getInvoiceAgingReport,
2820
+ invoke: (handlers, args) => handlers.getInvoiceAgingReport(args),
2821
+ },
2822
+ {
2823
+ name: "get_account_health_scorecard",
2824
+ description: "Identify at-risk accounts using multiple signals: overdue invoices, " +
2825
+ "expiring subscriptions (non-auto-renewing), and recent payment failures. " +
2826
+ "Answers 'Which accounts are at risk?' " +
2827
+ "Returns accounts sorted by health score (0=critical, 100=healthy) with risk factors.",
2828
+ inputSchema: schemas.getAccountHealthScorecard,
2829
+ invoke: (handlers, args) => handlers.getAccountHealthScorecard(args),
2830
+ },
2831
+ {
2832
+ name: "find_invoices_by_product",
2833
+ description: "Find invoice line items for a specific product. " +
2834
+ "Answers 'Show invoices for Security Analytics' or 'What was billed for product X?' " +
2835
+ "Chains: ProductRatePlan → RatePlan → RatePlanCharge → InvoiceItem → Invoice → Account. " +
2836
+ "Returns invoice items with charge amounts, dates, and account details.",
2837
+ inputSchema: schemas.findInvoicesByProduct,
2838
+ invoke: (handlers, args) => handlers.findInvoicesByProduct(args),
2839
+ },
2840
+ ];
2841
+ //# sourceMappingURL=tools.js.map